ASP.Net8 中使用 JWT 鉴权的异常处理

.Net8 中使用 JWT 鉴权的异常处理

自己搭了个学习 Demo 想用 JWT 給后端做鉴权,结果一直报一些奇奇怪怪的异常,最主要是和写业务代码不一样,因为用了官方提供的包很难排查出问题所在,这次有点像以前学Spring的时候,也是一点一点摸着石头过河,最后还是同事帮忙看出来问题在哪的。


问题1:IDX14100: JWT is not well formed, there are no dots (.). The token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializati

描述的是 JWT 缺少 . 符号,正常来说 JWT 由三部分组成,分别是 Header 、 Payload 和 Sign 组成,因此也就由两个 . 将字符串分割开来。明明我携带的 Token 是符合要求的为啥还报这个问题呢,后来通过 Github 的 Issue 找到了解决方案,参考:更新到 .NET 8 后的 SecurityTokenMalformedException ·问题 #52286 ·dotnet/aspnetcore (github.com)judilsteve回答了解决方法也就是在配置中开启

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
       .AddJwtBearer(opts =>
       {
            opts.UseSecurityTokenValidators = true;
       });

答主也说了这个 exception 真的很容易产生误解,也可能是我太菜了没看开发文档直接上手撸的原因。


问题2:Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace

这里提供一个解析 JWT 的地址jwt.ms: Welcome!,如果解析完的 JWT 确实缺少这几个必要的参数,那需要补全了再进行鉴权。

这里有一个非常疑惑的点,也就是 issuer 在正常的 JWT中解析完应该是 iss,audiences 应该是 aud,过期时间应该是exp才对,但这里报错确是全称,于是我就将这几个参数以全程的形式全都显式的添加到了 Claims 里面,没想到还是报这个 error,为了解决这个问题查了很多文章和 issue 都没解决,后来同事在她的电脑上帮我测试了下发现编写的代码一定点问题都没有,权限也OK。那我项目怎么解析 JWT 的时候 Claims 总会少那么几个,后来把思路放到包管理上了,发现自己不知道什么时候装了个包,名称为Microsoft.IdentityModel.Tokens,如果你恰好也安装了Microsoft.AspNetCore.Authentication.JwtBearer这个包,那么可能倒霉的踩到这个坑了,我开始还看说都是微软开发的包应该没什么问题,结果就是。。。,貌似是 JwtBearer 这个包中包含了 Tokens 这个包,导致装了两个 Tokens 这个包产生冲突了(我猜的),最后解决办法就是卸载了Microsoft.IdentityModel.Tokens这个包就解决了 Claims 缺失的问题。怪自己太菜了,以后有机会看看源码是怎么造成的这种现象。


最后附上生成 JWT 的代码及排查异常时的监听事件

// 解析 JWT
var handler = new JwtSecurityTokenHandler();
                var jwtToken = handler.ReadToken("<YOUR JWT TOKEN >") as JwtSecurityToken;
                if (jwtToken != null)
                {
                    Console.WriteLine(jwtToken.Claims.First(c => c.Type == JwtRegisteredClaimNames.Exp).Value);
                }

var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],	// 从appsetting中读取配置文件
            audience: _configuration["Jwt:Audience"],
            claims: new List<Claim>
            {
                new Claim(ClaimTypes.Name, user.Username.ToString()),
                new Claim(ClaimTypes.Role, "Permission"),
                new Claim("Permission", "1"),
                
                // 显式添加 exp 声明
                // new Claim(JwtRegisteredClaimNames.Exp, ((DateTime.UtcNow.AddDays(1) - new DateTime(1970, 1, 1)).TotalSeconds).ToString("F0"))
            },
            expires:DateTime.UtcNow.AddDays(1),
            notBefore: DateTime.Now,
            signingCredentials: credentials);
// 配置身份验证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true, // 发行人是否合法
            ValidateAudience = true, // 目标受众是否正确
            ValidateLifetime = true, // 有效期是否过期
            ValidateIssuerSigningKey = true, // 签名密钥是否正确
            ValidIssuer = builder.Configuration["Jwt:Issuer"], // 从应用配置中读取发行人
            ValidAudience = builder.Configuration["Jwt:Audience"], // 从应用配置中读取标识符
            IssuerSigningKey =
                new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecurityKey"] ?? "")), // 通过应用程序配置中获取密钥字符串
            ClockSkew = TimeSpan.FromSeconds(30), // 允许的时间偏差
            RequireExpirationTime = true // JWT是否包含过期时间
        };

        options.UseSecurityTokenValidators = true;
        // 捕获并处理认证事件
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                var exception = context.Exception;
            
                if (exception is SecurityTokenExpiredException)
                {
                    context.Response.Headers.Add("Token-Expired", "true");
                }
                else if (exception is SecurityTokenInvalidSignatureException)
                {
                    context.Response.Headers.Add("Token-Invalid-Signature", "true");
                }
                else if (exception is SecurityTokenInvalidAudienceException)
                {
                    context.Response.Headers.Add("Token-Invalid-Audience", "true");
                }
                else if (exception is SecurityTokenInvalidIssuerException)
                {
                    context.Response.Headers.Add("Token-Invalid-Issuer", "true");
                }
                else if (exception is SecurityTokenNoExpirationException)
                {
                    context.Response.Headers.Add("Token-No-Expiration", "true");
                }
                else
                {
                    // 添加日志记录
                    Console.WriteLine(exception.ToString(), "An unhandled exception occurred during authentication.");
                }
            
                return Task.CompletedTask;
            },
            OnTokenValidated = context =>
            {
                var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            
                if (claimsIdentity != null && !claimsIdentity.HasClaim(c => c.Type == "required_claim"))
                {
                    context.Fail("Unauthorized"); // 如果校验失败,终止请求
                }
            
                return Task.CompletedTask;
            },
            OnChallenge = context =>
            {
                // 自定义处理失败挑战的响应
                context.HandleResponse();
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";

                // 获取异常信息
                var errorInfo = context.Error;
                var errorDescription = context.ErrorDescription;

                var result =
                    JsonConvert.SerializeObject(new { error = errorInfo, error_description = errorDescription });
                context.Response.WriteAsync(result);
                return Task.CompletedTask;
            }
        };
    });
posted @   颜骏  阅读(498)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示