ASP.NET Core 10 JwtBearer + Keycloak OIDC 本地开发 401 循环跳转排查全记录

发布时间:2026/7/2 10:34:06
ASP.NET Core 10 JwtBearer + Keycloak OIDC 本地开发 401 循环跳转排查全记录 记一次.NET 10 JwtBearerKeycloak登录死循环的完整排查背景项目使用.NET 10 Next.js的前后端分离架构认证方案是Keycloak SSOAuthorization Code Flow。前端调整设计规范统一走Keycloak登录后本地开发环境出现了经典的「登录死循环」点 Keycloak 登录 → 跳 Keycloak 授权页 → 回调保存 token → 跳首页 → 调/me接口 →401→ 清 session → 跳回登录页 → 再点登录 → ♾️这篇文章记录整个排查过程重点是三个藏得很深的坑以及如何用断点堆栈逐层定位。一、先看清死循环的机制前端链路AuthGate(/)→ 无 token → /login /login → 点 Keycloak 登录 → Keycloak 授权已有 session 秒回 code /auth/callback → 换 token → 存 localStorage → redirect / AuthGate(/)→ 有 token → 调 GET /me /me →401→ client.ts401handler: userManager.removeUser()// 清 localStorage window.location.assign(/login)// 跳登录 → Keycloak 又有 session → 再签 code → 死循环关键代码client.ts的 401 全局拦截器if(res.status401typeofwindow!undefined){const{userManager}awaitimport(/lib/auth/oidc);awaituserManager.removeUser();// 清 tokenwindow.location.assign(/login?redirect${encodeURIComponent(path)});thrownewApiError(身份认证已失效正在跳转登录…,401);}这个拦截器的本意是正确的——token过期或无效就踢回登录。但当后端每次都返回401时它就和Keycloak的session缓存形成完美死循环。排查技巧前端Network面板勾选「Preserve log」页面跳转后翻/me的请求确认状态码就是401。二、第一个坑PostConfigure 被注释了项目架构认证配置的JwtBearer参数不是写在AddJwtBearer()回调里而是通过IPostConfigureOptionsJwtBearerOptions推迟到DI容器构建完成后执行// JwtBearerPostConfigure.cspublicvoidPostConfigure(string?name,JwtBearerOptionsoptions){if(name!JwtBearerDefaults.AuthenticationScheme)return;usingvarscopeserviceProvider.CreateScope();varproviderscope.ServiceProvider.GetRequiredServiceIIdentityProvider();provider.ConfigureJwtBearer(options,env);// ←-- 核心调用}注册代码在AuthExtensions.csservices.TryAddEnumerable(ServiceDescriptor.SingletonIPostConfigureOptionsJwtBearerOptions,JwtBearerPostConfigure());这行被注释掉了。后果是ConfigureJwtBearer从头到尾没被执行JwtBearer中间件拿到的是纯默认JwtBearerOptionsAuthority null→ 拉不到JWKSValidateIssuerSigningKey true默认→ 需要签名SignatureValidator null→ 没有绕过逻辑Keycloak的RS256签名token在没有任何密钥配置的环境下当然验不过 →401。教训当怀疑「配置为什么没生效」时在PostConfigure / ConfigureJwtBearer方法入口打断点确认整个调用链是通的。三、第二个坑Authority 设在了 if/else 之前取消注释后PostConfigure执行了Dev模式的TokenValidationParameters也设了publicvoidConfigureJwtBearer(JwtBearerOptionsoptions,IHostEnvironmentenv){options.AuthorityKc.Issuer;// ← 这行在 if/else 前面options.RequireHttpsMetadatafalse;if(env.IsDevelopment()){// 看似跳过了所有验证……options.TokenValidationParametersnewTokenValidationParameters{ValidateIssuerSigningKeyfalse,RequireSignedTokensfalse,SignatureValidator(token,_)newJwtSecurityToken(token),// ←-- 第3个坑,请看后续说明};}}但401依旧。堆栈显示走了JsonWebTokenHandler.ValidateSignature→ValidateAfterSignatureFailed→ValidateIssuer。根因options.Authority Kc.Issuer在分支之前就设了。JwtBearer中间件发现Authority有值自动触发追加/.well-known/openid-configuration拉发现文档从jwks_uri拉JWKS公钥列表用配置里的Issuer/Audience/SigningKeys覆盖TokenValidationParameters里的自定义参数SignatureValidator被配置推导出的签名逻辑完全绕过教训Authority必须只在需要JWKS自动发现的环境生产才设置。Dev模式不设防止配置覆盖。修复publicvoidConfigureJwtBearer(JwtBearerOptionsoptions,IHostEnvironmentenv){options.RequireHttpsMetadatafalse;if(env.IsDevelopment()||env.IsEnvironment(Dev)){// ★ Dev不设 AuthoritySignatureValidator 完全接管options.TokenValidationParametersnewTokenValidationParameters{...};}else{// ★ 生产设 Authority走完整 JWKS 校验options.AuthorityKc.Issuer;options.TokenValidationParametersnewTokenValidationParameters{...};}}四、第三个坑JwtSecurityToken vs JsonWebTokenAuthority问题修复后堆栈从ValidateSignature变成了ValidateSignatureUsingDelegates——说明SignatureValidator终于被认识了。断点确认了委托被调用token值正确new JwtSecurityToken(token)也没有抛异常。但还是 401。真正原因类型不匹配.NET 10的JwtBearer中间件默认使用JsonWebTokenHandler而不是老版JwtSecurityTokenHandler。JsonWebTokenHandler.ValidateSignatureUsingDelegates内部调用SignatureValidator后期望返回的是JsonWebToken类型而不是JwtSecurityToken。错误方式usingSystem.IdentityModel.Tokens.Jwt;// ❌ 错误返回 JwtSecurityTokenJsonWebTokenHandler 后续处理失败SignatureValidator(token,_)newJwtSecurityToken(token),// or 使用这种写法方便调试SignatureValidator(token,_){returnnewJsonWebToken(token);// ←-- F9 断点打这里},正确方式// 需要加 usingusingMicrosoft.IdentityModel.JsonWebTokens;// ✅ 正确返回 JsonWebToken (如需调试代码写法同上)SignatureValidator(token,_)newJsonWebToken(token),为什么 JwtSecurityToken 不抛异常却导致 401JwtSecurityToken和JsonWebToken都继承自基类ValidateSignatureUsingDelegates的返回类型没有强制约束为JsonWebToken所以编译器不会报错。但JsonWebTokenHandler内部的后续处理claims 提取、配置校验等强依赖JsonWebToken的内部结构拿到JwtSecurityToken后默默地走了失败分支最终产生401。教训在.NET 8 / .NET 10项目中使用SignatureValidator回调时必须返回JsonWebToken不要想当然用老的JwtSecurityToken。两个类虽名字相似但内部实现完全不同。五、最终修复方案总结文件 1KeycloakIdentityProvider.csusingMicrosoft.IdentityModel.JsonWebTokens;// ←-- 新增命名空间publicvoidConfigureJwtBearer(JwtBearerOptionsoptions,IHostEnvironmentenv){options.RequireHttpsMetadatafalse;if(env.IsDevelopment()||env.IsEnvironment(Dev)){// Dev不设 Authority完全绕过签名校验options.TokenValidationParametersnewTokenValidationParameters{ValidateIssuerfalse,ValidateAudiencefalse,ValidateLifetimefalse,ValidateIssuerSigningKeyfalse,RequireSignedTokensfalse,SignatureValidator(token,_)newJsonWebToken(token),// ←-- 使用 JsonWebTokenClockSkewTimeSpan.Zero};}else{// 生产完整校验options.AuthorityKc.Issuer;options.TokenValidationParametersnewTokenValidationParameters{ValidIssuerKc.Issuer,ValidAudiencesvalidAudiences,ValidateIssuertrue,ValidateAudiencetrue,ValidateIssuerSigningKeytrue,RequireSignedTokenstrue,ClockSkewTimeSpan.Zero};}}文件 2AuthExtensions.cs// 确保这行没有被注释services.TryAddEnumerable(ServiceDescriptor.SingletonIPostConfigureOptionsJwtBearerOptions,JwtBearerPostConfigure());六、排查方法论总结步骤做什么用到什么1看清循环链路Network → Preserve log → /me状态码2确认配置是否生效在PostConfigure方法入口打断点3确认Dev/生产分支悬停env.IsDevelopment()、Kc.Issuer4确认SignatureValidator是否被调用在委托内部打断点看token值5看堆栈走的具体路径ValidateSignature忽视委托vsValidateSignatureUsingDelegates使用委托6确认返回类型JwtSecurityToken→ 换JsonWebToken核心原则不要只依赖ValidateIssuerSigningKey false和RequireSignedTokens false来绕过Dev模式验证。这两个flag只是关闭了可选的校验步骤JwtBearer中间件在Authority已设置或配置已加载的情况下依然会做底层的签名密码学计算。要彻底绕过必须用SignatureValidator接管整个签名流程且返回类型要与当前 Token Handler 匹配。七、总结本文记录了在.NET 10 Keycloak SSO认证中遇到的登录死循环问题及其排查过程。前端登录后调用/me接口返回401触发401拦截器清除token并重定向形成死循环。排查发现三个关键问题IPostConfigureOptions被注释导致JwtBearer配置未生效无法验证Keycloak的RS256签名Token。Authority设置位置错误开发模式下提前设置Authority导致自动拉取JWKS覆盖了自定义的TokenValidationParameters。类型不匹配SignatureValidator返回JwtSecurityToken但.NET 10的JsonWebTokenHandler需要JsonWebToken类型引发静默失败。解决方法修正配置加载顺序、隔离开发/生产环境的Authority设置并确保返回正确的Token类型。通过断点调试和堆栈分析逐步定位问题根源最终解决了登录401循环问题。