网站首页 全球最实用的IT互联网站!

人工智能P2P分享Wind搜索发布信息网站地图标签大全

当前位置:诺佳网 > 软件工程 > 后端开发 > .Net >

为什么 退出登录 或 修改密码 无法使 token 失效

时间:2025-03-05 09:19

人气:

作者:admin

标签:

导读:前文说过 token 由 3 个部分组成,分别是 metadata:加密方式; payload:token 业务层级的内容,例如 用户id,token 过期时间,token 签发人,token 签发时间等内容; signature:对 payload 加密后的...
前文说过 token 由 3 个部分组成,分别是 metadata:加密方式; payload:token 业务层级的内容,例如 用户id,token 过期时间,token 签发人,token 签发时间等内容; signature:对 payload 加密后的密文。   一般地,当服务端接收到一个 token,是这样校验的: 首先取出 signature 部分解密得到明文,再将明文比较于 token 中的 payload,如果二者一致,且当前时间处于 token 有效期内,则认为这是一个有效的 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,还可以衍生出非常多的问题,此处暂且不表。     ..  
温馨提示:以上内容整理于网络,仅供参考,如果对您有帮助,留下您的阅读感言吧!
相关阅读
本类排行
相关标签
本类推荐

CPU | 内存 | 硬盘 | 显卡 | 显示器 | 主板 | 电源 | 键鼠 | 网站地图

Copyright © 2025-2035 诺佳网 版权所有 备案号:赣ICP备2025066733号
本站资料均来源互联网收集整理,作品版权归作者所有,如果侵犯了您的版权,请跟我们联系。

关注微信