【问题标题】:Standard way to throw error from web service if JSON Web Token (JWT) is expired?如果 JSON Web 令牌(JWT)过期,从 Web 服务抛出错误的标准方法?
【发布时间】: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://stackoverflow.com/a/62289801/4290962

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

提前致谢!

【问题讨论】:

    标签: c# azure asp.net-core jwt authorization


    【解决方案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://stackoverflow.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);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2017-03-07
      • 1970-01-01
      • 1970-01-01
      • 2016-08-01
      • 1970-01-01
      • 2017-11-01
      • 2014-06-30
      • 2016-02-07
      • 2016-07-20
      相关资源
      最近更新 更多