如果 JSON Web 令牌(JWT)过期,从 Web 服务抛出错误的标准方法?

Posted

技术标签:

【中文标题】如果 JSON Web 令牌(JWT)过期,从 Web 服务抛出错误的标准方法?【英文标题】:Standard way to throw error from web service if JSON Web Token (JWT) is expired? 【发布时间】:2021-05-03 05:44:18 【问题描述】:

我正在编写针对 Azure 云运行的 C# 代码。我的应用程序是一个 ASP.NET Core Web 服务,它公开方法但没有 UI。

我的大多数(不是全部)方法都需要 JSON Web Token (JWT) 进行授权。我的Startup.cs 中有这个:

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options =>
    
        options.Authority = Configuration["AppSettings:IdentityAuthEndpoint"];
        options.RequireHttpsMetadata = bool.Parse(Configuration["AppSettings:RequireHttps"]);
        options.ApiName = "data";
        options.EnableCaching = true;
    );

如果 JWT 丢失或完全损坏,则会引发错误。但是如果 JWT 刚刚过期,上面的内容会让用户通过。

根据这个 Stack Overflow 答案,我可以检查 context.User.Claims 是否为空:https://***.com/a/62289801/4290962

这似乎是真的。但是是否有标准的最佳实践方法来检测这一点并抛出标准化错误?还是我需要编写自定义代码来检查声明?我当然可以这样做。我只是在想应该有一个更漂亮的解决方案。

提前致谢!

【问题讨论】:

【参考方案1】:

我建议您为此编写一个中间件,这将使您更好地控制您想要实现的目标。正如您所说,寻找现有解决方案很好,但这是我在我的一个企业应用程序中使用的解决方案。

拦截令牌 - 在每个请求上使用此中间件

public class JwtMiddleware

    private readonly RequestDelegate _next;

    public JwtMiddleware(RequestDelegate next)
    
        _next = next;
    

    public async Task Invoke(HttpContext context, IUserService userService)
    
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        if (token != null) AttachUserToContext(context, userService, token);
        await _next(context);
    

    private void AttachUserToContext(HttpContext context, IUserService userService, string token)
    
        try
        
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(AppConfigBuilder.Build().Security.JwtSecret);
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            , out SecurityToken validatedToken);
            var jwtToken = (JwtSecurityToken)validatedToken;
            var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
            context.Items["User"] = userService.GetUserById(userId);
        
        catch
        
        
    

在启动时注册

app.UseMiddleware<JwtMiddleware>();

然后创建一个身份验证过滤器

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter

    public void OnAuthorization(AuthorizationFilterContext context)
    
        var user = (User)context.HttpContext.Items["User"];
        if (user == null)
        
            context.Result = new JsonResult(new Response 
                IsSuccess = false,
                ResponseStatus = ResponseStatus.ERROR,
                Message="Request terminated. Unauthorized access to protected resource.", 
                Info=new List<string>()  
                    "Verify the auth token sending through this request",
                    "Verify if your token is invalid or expired", 
                    "Request for a new token by logging in again"  
                )
             StatusCode = StatusCodes.Status401Unauthorized ;
        
    

这就是所有需要做的。现在要保护任何端点,只需像这样使用 [Authorize] 属性。这将验证您的令牌并发出适当的标准响应。

    [HttpGet]
    [Authorize]
    public IActionResult Get()
    
        //Boilerplate code
        var response = userService.GetAllUsers();
        return (response.IsSuccess) ? Ok(response) : BadRequest(response);
    

仅供参考:您可以在授权过滤器上定义自己的标准响应类。我使用的是 ExpressGlobalExceptionHandler 库。

这是我创建的 JWT 令牌生成逻辑

    private string GenerateJwtToken(User user)
    
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(appConfig.Security.JwtSecret);
        var tokenDescriptor = new SecurityTokenDescriptor
        
            Subject = new ClaimsIdentity(new[]  new Claim("id", user.Id.ToString()) ),
            Expires = DateTime.UtcNow.AddDays(7),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        ;
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    

【讨论】:

谢谢。实际上,这与我所拥有的相似。您能否准确解释您在代码中的哪个位置验证令牌未过期?是打电话给tokenHandler.ValidateToken吗? 是的。 ValidateToken 是 Microsoft 的 JwtSecurityTokenHandler 中的一个 InBuilt 函数。它检查令牌验证、到期和签名,因此您无需担心手动编写验证 @ClausAppel 更具体地说,在创建 JWT 令牌期间,我正在设置到期时间。 validate 方法将在内部对其进行验证。我已经编辑了答案并添加了 JWT 创建方法供您参考 现在它抛出一个异常说:“签名验证失败。无法匹配密钥”。它如何知道如何验证签名?我是否需要告诉它在哪里可以找到预期的签名密钥(就像我在上面对option.Authority 所做的那样)?我该怎么做? “签名验证失败。无法匹配密钥” - 此错误表示它无法验证令牌。当你生成一个 JWT 令牌时,你应该传递一个“JwtSecret”。那应该是126位。你能分享你的 JWT 生成和验证逻辑吗?我假设那里有问题。对我来说,我正在从 appsettings.json 加载 JWT 机密【参考方案2】:

根据这个答案,我最终做了以下事情:https://***.com/a/34423434/4290962

请注意,_rejectExpiredJwt 是一个可配置的布尔值。

public JwtSecurityToken ValidateToken(string tokenStr)

    var rsa = new RSACryptoServiceProvider();
    rsa.ImportParameters(
        new RSAParameters
        
            Modulus = FromBase64Url(_modulus),
            Exponent = FromBase64Url(_exponent)
        );

    var validationParameters = new TokenValidationParameters
    
        RequireExpirationTime = true,
        RequireSignedTokens = true,
        ValidateAudience = false,
        ValidateIssuer = true,
        ValidIssuer = _issuer,
        ValidateLifetime = _rejectExpiredJwt,
        IssuerSigningKey = new RsaSecurityKey(rsa)
    ;

    var handler = new JwtSecurityTokenHandler();
    handler.ValidateToken(tokenStr, validationParameters, out SecurityToken validatedSecurityToken);
    var validatedJwt = validatedSecurityToken as JwtSecurityToken;
    return validatedJwt ?? throw new UnauthorizedException(
        $"Token is not a nameof(JwtSecurityToken) but a validatedSecurityToken.GetType().Name.");

    static byte[] FromBase64Url(string base64Url)
    
        string padded = base64Url.Length % 4 == 0
            ? base64Url
            : base64Url + "====".Substring(base64Url.Length % 4);
        string base64 = padded.Replace("_", "/")
            .Replace("-", "+");
        return Convert.FromBase64String(base64);
    

【讨论】:

以上是关于如果 JSON Web 令牌(JWT)过期,从 Web 服务抛出错误的标准方法?的主要内容,如果未能解决你的问题,请参考以下文章

使用 JWT(JSON Web 令牌)设置令牌到期的 RESTful API

如何设置 json web 令牌过期和验证

Json Web 令牌不会过期

Xamarin.iOS / Web API JWT 刷新令牌(如果过期)

JSON Web 令牌过期和记住我功能

如果用户想从不同的机器上退出帐户,JSON Web 令牌 (JWT) 是不是会被拒绝或列入黑名单?