繁体   English   中英

密码更改后如何使令牌无效

[英]How to invalidate tokens after password change

我正在开发一个使用 JWT 令牌身份验证的 API。 我在其背后创建了一些逻辑来使用验证码等更改用户密码。

一切正常,密码被更改。 但这里有一个问题:即使用户密码已更改并且我在进行身份验证时获得了一个新的 JWT 令牌......旧令牌仍然有效。

关于如何在密码更改后刷新/使令牌无效的任何提示?

编辑:因为我听说您实际上不能使 JWT 令牌无效,所以我对如何做有了一个想法。 我的想法是创建一个新的用户列,其中包含“accessCode”之类的内容,并将该访问代码存储在令牌中。 每当我更改密码时,我也会更改 accessCode(类似于 6 位随机数),并在执行 API 调用时对该 accessCode 进行检查(如果令牌中使用的访问代码与数据库中的访问代码不匹配 -> 返回未经授权)。

你们认为这是一个好方法还是有其他方法?

撤销/无效的最简单方法可能只是删除客户端上的令牌并祈祷没有人会劫持并滥用它。

您使用“accessCode”列的方法可行,但我会担心性能。

另一种可能更好的方法是将某些数据库中的令牌列入黑名单。 我认为 Redis 是最好的,因为它支持通过EXPIRE超时,因此您可以将其设置为与 JWT 令牌中的值相同的值。 当令牌过期时,它会自动删除。

您将需要快速响应时间,因为您必须检查每个需要授权的请求的令牌是否仍然有效(不在黑名单或不同的访问代码中),这意味着在每个请求上使用无效的令牌调用您的数据库。


刷新令牌不是解决方案

有些人建议使用长期刷新令牌和短期访问令牌。 您可以将访问令牌设置为在 10 分钟后过期,当密码更改时,该令牌仍将在 10 分钟内有效,但随后会过期,您必须使用刷新令牌来获取新的访问令牌。 就个人而言,我对此持怀疑态度,因为刷新令牌也可能被劫持: http : //appetere.com/post/how-to-renew-access-tokens然后您还需要一种方法来使它们无效因此,最终,您无法避免将它们存储在某个地方。


使用 StackExchange.Redis 的 ASP.NET Core 实现

您正在使用 ASP.NET Core,因此您需要找到一种方法来添加自定义 JWT 验证逻辑以检查令牌是否已失效。 这可以通过扩展默认JwtSecurityTokenHandler来完成,您应该能够从那里调用 Redis。

在 ConfigureServices 添加:

services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("yourConnectionString"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opt =>
    {
        opt.SecurityTokenValidators.Clear();
        // or just pass connection multiplexer directly, it's a singleton anyway...
        opt.SecurityTokenValidators.Add(new RevokableJwtSecurityTokenHandler(services.BuildServiceProvider()));
    });

创建您自己的异常:

public class SecurityTokenRevokedException : SecurityTokenException
{
    public SecurityTokenRevokedException()
    {
    }

    public SecurityTokenRevokedException(string message) : base(message)
    {
    }

    public SecurityTokenRevokedException(string message, Exception innerException) : base(message, innerException)
    {
    }
}

扩展默认处理程序

public class RevokableJwtSecurityTokenHandler : JwtSecurityTokenHandler
{
    private readonly IConnectionMultiplexer _redis;

    public RevokableJwtSecurityTokenHandler(IServiceProvider serviceProvider)
    {
        _redis = serviceProvider.GetRequiredService<IConnectionMultiplexer>();
    }

    public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters,
        out SecurityToken validatedToken)
    {
        // make sure everything is valid first to avoid unnecessary calls to DB
        // if it's not valid base.ValidateToken will throw an exception, we don't need to handle it because it's handled here: https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128
        // we have to throw our own exception if the token is revoked, it will cause validation to fail
        var claimsPrincipal = base.ValidateToken(token, validationParameters, out validatedToken); 
        var claim = claimsPrincipal.FindFirst(JwtRegisteredClaimNames.Jti);
        if (claim != null && claim.ValueType == ClaimValueTypes.String)
        {
            var db = _redis.GetDatabase();
            if (db.KeyExists(claim.Value)) // it's blacklisted! throw the exception
            {
                // there's a bunch of built-in token validation codes: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/7692d12e49a947f68a44cd3abc040d0c241376e6/src/Microsoft.IdentityModel.Tokens/LogMessages.cs
                // but none of them is suitable for this
                throw LogHelper.LogExceptionMessage(new SecurityTokenRevokedException(LogHelper.FormatInvariant("The token has been revoked, securitytoken: '{0}'.", validatedToken)));
            }
        }

        return claimsPrincipal;
    }
}

然后在您更改密码或使用令牌的 jti 设置密钥以使其无效时。

限制!: JwtSecurityTokenHandler中的所有方法都是同步的,如果您想要一些 IO 绑定调用,这很糟糕,理想情况下,您可以await db.KeyExistsAsync(claim.Value)使用await db.KeyExistsAsync(claim.Value) 此处跟踪此问题: https : //github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468不幸的是自 2016 年以来没有更新:(

这很有趣,因为验证令牌的函数是异步的: https : //github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs1#L2807-L

暂时的解决办法将是扩展JwtBearerHandler和替换的实施HandleAuthenticateAsyncoverride不调用基所以它会调用验证你的异步版本。 然后使用这个逻辑来添加它。

最受推荐和积极维护的 C# Redis 客户端:

可能会帮助你选择一个: StackExchange.Redis 和 ServiceStack.Redis 的区别

StackExchange.Redis 没有任何限制,并在 MIT 许可下。

所以我会选择 StackExchange 的

最简单的方法是:使用用户当前的密码哈希对 JWT 进行签名,以保证每个已发布令牌的一次性使用。 这是因为密码哈希在成功重置密码后总是会发生变化。

同一个令牌不可能两次通过验证。 签名检查总是会失败。 我们发行的 JWT 成为一次性令牌。

来源- https://www.jbspeakr.cc/howto-single-use-jwt/

以下方法汇集了先前提出的每种方法中的优点:

  1. 在“user”表中创建列“password_id”。
  2. 创建用户时为“password_id”分配一个新的 UUID。
  3. 每次用户更改密码时,都会为“password_id”分配一个新的 UUID。
  4. 使用相应用户的“password_id”对授权 JWT 进行签名。
  5. 如果需要更高的性能,只需将用户的“password_id”存储在 Redis 中。

这种方法的优点:

  • 如果用户更改了他的密码,则在那一刻之前存在的所有 JWT 将自动永远无效。
  • 用户是否将其密码更改为旧密码并不重要。
  • 没有必要将 JWT 存储在服务器端。
  • 无需在 JWT 负载中添加任何额外数据。
  • 使用Redis的实现非常简单。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM