前文说过 token 由 3 个部分组成:分别是 token metadata,payload,signature,
其中 signature 部分是对 payload 的加密,而 payload 当中会包含 token 的过期时间。
 
这样做有个好处:服务端不需要另外去存储这个 session 的状态,只要校验过 signature,就可以知道过期时间,简化后端逻辑,做到 “无状态化”。
但这样做也会有一个缺点,只要 token 签发了,就无法更改这个 token 的内容。
 

问题场景

一个用户登录,获得一个有效的 token 之后,他点击退出登录。
此时理想状态下,我们希望这个 token 不再生效,但实际是,只要拿着这个 token 去访问服务端 Authenticated 的资源,token 仍然会校验通过。
因为 [退出登录] 的操作本身不能改变 token。
 
 

解决方案

一、把 token 的有效期缩短,例如半小时或者五分钟,但有个明显的缺陷:用户需要频繁重新登录。

 

二、把 退出登录 的用户添加到 token black list 当中。

简单地,在调 sign out (退出登录) 的 api 时,把用户的 access token 添加到 token black list 当中;后端校验 jwt 时,添加校验 token 是否存在于 token black list 当中。
下面展开设计过程:
 

1. token black list 的设计

是否持久化?

第一个问题是:这种 sign out 的 token 要不要持久化?
首先,token 本身就是会过期的;
其次,这个新的校验方法会作用到每一个通过了 token 有效验证的请求,这个方法一定是高频访问的;
所以,此处选择通过 redis 缓存 tokenBlackList 。
(Redis 是内存数据库,支持高并发读写和自动过期(TTL),适合存储临时性黑名单数据。即使服务重启,黑名单数据可能丢失,但 Token 本身有过期时间,因此不影响最终一致性。)
每次 sign out,都将 set 到 redis 中;每次校验 token,都查询这个 redis value。
 

数据结构设计

REDIS 是 key,value 的键值对方式,value 可能是 string,list,hash..
对于 value,可以直接粗暴的存储整个 token json;
那么 key 应该如何设计?使用 userId,那大概率会和其他业务的 redis key 重叠,在这里最好加上业务场景,形如,“TOKEN_BLACK_LIST_userId”。
 
a. 多设备登录场景
假设:用户 A 在设备 D1 上登录后,在设备 D2 上同时登陆(这种场景当前是允许的);
此时用户 A 在设备 D1 上点击 退出登录,服务端会把 TOKEN_BLACK_LIST_AId : tokenJson 写进了redis。
此时用户 A 再于设备 D2 上操作,校验 token 时会去 redis 捞取数据,找到了 TOKEN_BLACK_LIST_AId,此时认为用户 A 的 token 无效。
 
如果这不是我们期望的场景,那应该如何让同个账号的多个 token 互不影响呢?此处 userId 就不适合作为 redis key。
是否每个 token 有自己特有的唯一的 id 呢?这又到 token payload 的组成,它确实存在唯一标识的 id,jti
{
"jti": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"iss": "the issuer",
"aud": "the audience",
"exp": 1630003600,
"iat": 1630000000,
..
}
此故,这里把 key 设计成 [TOKEN_BLACK_LIST_tokenId],形如 "TOKEN_BLACK_LIST_a1b2c3d4-5678-90ef-ghij-klmnopqrstuv"。
 
 
b. 修改密码场景
假设:用户 A 在设备 D1、D2、D3 .. 多设备上均操作登录,此时每个设备都持有一个独立的 token;
如果此时用户在设备 D1 上 “修改密码”,如何让 D2、D3 等所有设备的登录失效?
 
后端可以把提出 “修改密码” 的设备 D1 的 token 加入到 token_black_list 当中,但是如何知道这个用户当前持有多少 token 呢?
是否需要每次登录都把 token 存储起来?但这样显然会增加复杂度。
 
我们可以重新审视一下 token 的结构,是否能找到一些属性来使用?
{
"jti": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"iss": "the issuer",
"aud": "the audience",
"exp": 1630003600,
"iat": 1630000000,
..
}
这里有一个非常巧妙而简单的方式:
每一个 token 上都会持有 iat 签发时间,假设 用户 A 在设备 D1 上确定 “修改密码” 的时间是 changedPasswordDate
服务端处理完 “修改密码” 之后,可以把 changedPasswordDate 这个时间存储到 redis 上,设其 key 为 TOKEN_INVALIDATION_userId,value 为 changedPasswordDate
 
那么在服务端校验 token 需要多添加这两项校验:
    查询当前 token 是否存在于 TOKEN_BLACK_LIST 中
    查询是否存在 TOKEN_INVALIDATION_userId,如果存在,
        比较当前 token 的 iat 时间是否早于 changedPasswordDate,如果是,该 token 无效
 
 

2. code implement

sign-out / change-password

redis key-value 的过期时间取 token 的有效周期。本文设定 token 有效期为24小时,也即 1440 分钟。
    public async Task<GlobalSignOutResponse> SignOutAsync(string accessToken)
{
var response = await _authService.SignOutAsync(accessToken);
await _redisCacheService.SetCache(TokenHelper.GetRedisKeyForBlackAccessToken(accessToken), accessToken, 1440); // 分钟单位
return response;
}
"修改密码" 的处理同理。
 

jwt authentication

startUp.cs

        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
..
options.Events = new JwtBearerEvents
{
..
OnTokenValidated = async context =>
{
if (await IsAccessTokenExpired(context, services))
{
Log.Information($"The access token is expired as user already signed out or changed password.");
context.Fail(GetTokenExpiredResponse(context.Response));
}
await Task.CompletedTask;
}
};
});
    private string GetTokenExpiredResponse(HttpResponse response)
{
if (ApiResponseCodes.AccessTokenExpired.BuildHttpResponse() is ObjectResult result)
{
var payload = JObject.FromObject(result.Value);
response.ContentType = "application/json";
response.StatusCode = 401; return payload.ToString();
}
return string.Empty;
} private async Task<bool> IsAccessTokenExpired(TokenValidatedContext context, IServiceCollection services)
{
try
{
var requestHeader = context.Request.Headers["Authorization"];
var accessToken = requestHeader.Count > 0 ? requestHeader[0].Split(" ")[1] : String.Empty;
var redisService = context.HttpContext.RequestServices.GetRequiredService<IRedisCacheService>()
var blackToken = await redisService.GetCache(TokenHelper.GetRedisKeyForBlackAccessToken(accessToken));
return blackToken == accessToken;
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to validate access token: {ex.Message}");
return true;
}
}
 
 
* 由 [退出登录] 无效化 token,还可以衍生出非常多的问题,此处暂且不表。
 
 
..
 

为什么 退出登录 或 修改密码 无法使 token 失效的更多相关文章

  1. Android基于XMPP Smack Openfire下学习开发IM(一)实现用户注册、登录、修改密码和注销等

    http://blog.csdn.net/h7870181/article/details/8653865 以前学习过用Scoket 建立聊天,简单的建立聊天是没问题的,但如果要实现多人复杂的聊天,后 ...

  2. openfire Android学习(一)----实现用户注册、登录、修改密码和注销等

    以前学习过用Scoket 建立聊天,简单的建立聊天是没问题的,但如果要实现多人复杂的聊天,后台服务器代码就比较复杂,对于我这新手来讲就比较难了.后来在网上看到用openfire做服务器,利用强大的Sm ...

  3. node+mysql+express实现登录/注册/修改密码/删除用户 接口

    实现用户的注册.登录.修改密码.删除用户操作 用到的数据库:nodecms:表:user 目录结构: db目录下存放数据库操作语句: userSQL.js 用户有关的操作语句 router目录 接口路 ...

  4. 实战!退出登录时如何借助外力使JWT令牌失效?

    大家好,我是不才陈某~ 今天这篇文章介绍一下如何在修改密码.修改权限.注销等场景下使JWT失效. 文章的目录如下: 解决方案 JWT最大的一个优势在于它是无状态的,自身包含了认证鉴权所需要的所有信息, ...

  5. Mysql 免密码登录,修改密码及忘记密码操作

    ----免密码登陆 方式一 my.cnf增加[client]标签 [client] user="root" password="你的密码" 单对定义不同的客户端 ...

  6. 使用手机登录OWA修改密码的问题

    最近发现使用手机端登录OWA,安卓手机是可以修改密码的,如图1,但是iPhone就不成,safari和第三方都不可以,如图二. 图一 图二

  7. 测试点常用用例设计(登录、修改密码、输入框、上传视频、XSS、URL篡改)

    1.无效-视频文件测试点: 视频大小过大 视频大小过小 视频名称过长 视频名称包含特殊字符 视频名称包含中文.中英混合 视频文件格式错误 视频文件重复性上传 2.有效-视频文件测试点: 选择符合要求的 ...

  8. Web实现数据库链接的登录注册修改密码功能

    /** * Copyright (C), 2017-2017 * FileName: User * Author: ichimoku * Date: 2017/12/5 14:31 * version ...

  9. linux修改密码出现Authentication token manipulation error的解决办法

    转自 :http://blog.163.com/junwu_lb/blog/static/1916798920120103647199/ Authentication token manipulati ...

  10. Email接收验证码,以实现登录/注册/修改密码

    要求 1)实现Email形式的注册功能和相应的登录功能:2)实现忘记密码时的密码找回功能:3)存在数据库中的密码不能以明文形式存放,即建议在浏览器端发送请求前,调用js代码对用户的密码做md5加密 分 ...

随机推荐

  1. Ubuntu sudo nopasswd方法

    1 运行 visudo 2 假设用户名为yourname, 则在最后一行添加: # User privilege specificationroot ALL=(ALL:ALL) ALL# Member ...

  2. shell判断字符串结尾

    下面围绕"判断字符串是否以.txt结尾"展开.转变一下也同样适用于"判断字符串是否以.txt开头". 通用的方法 # 方法一.使用grep命令 #!/bin/s ...

  3. jQuery ajax - serializeArray() 方法 实例表单提交

    serializeArray()在ajax表单提交时候非常方便获取元素 定义和用法 serializeArray() 方法通过序列化表单值来创建对象数组(名称和值). 您可以选择一个或多个表单元素(比 ...

  4. Qt/C++加载不同的地图控件/地图类型/缩放标尺/缩略图/比例尺/实时路况/全景视图等

    一.前言说明 在展示地图的时候,有些常规的操作,比如调整地图的缩放级别,切换到卫星图等,希望能够在地图上直接操作实现,于是就有了一堆地图控件,可以根据自己的需求动态的添加和删除,这样就更直接更快捷,而 ...

  5. Qt Creator 5.0 发布

    我们很高兴地宣布 Qt Creator 5.0 的发布! 正如4.15 发布博文中所宣布的,我们将切换到语义版本控制方案,因此这是 Qt Creator 很长一段时间以来的第一次主要版本更新!不过不要 ...

  6. [转]Node.js安装详细步骤教程(Windows版)

    什么是Node.js? 简单的说 Node.js 就是运行在服务端的 JavaScript. Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行环境: Node.js使用 ...

  7. 深入Python胶水语言的本质:从CPython到各类扩展机制

    在开始深入讲解Python如何作为胶水语言之前,我们需要先了解Python语言本身的实现机制.这对于理解Python如何与C语言交互至关重要. CPython:Python的默认实现 当我们谈论Pyt ...

  8. Transformers in Vision

    Transformers in Vision 介绍 最初引入现在著名的Attention is all you need1,Transformer 多年来一直主导着自然语言处理 (NLP) 领域.特别 ...

  9. [软件工具使用记录] windows离线ollama部署本地模型并配置continue实现离线代码补全

    qwen2.5coder发布之后,觉得差不多可以实现离线模型辅助编程了,所以尝试在公司内网部署模型,配合vsocde插件continue实现代码提示.聊天功能. 目前使用qwen2.5coder的32 ...

  10. w3cschool-JUnit测试框架

    什么是 Junit 测试框架? JUnit 是一个回归测试框架,被开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量.JUnit 测试框架能够轻松完成以下任意两种结合: Ecli ...