ASP.NET Core 8中OIDC与JWT共存时IAuthenticateResultFeature丢失问题
解决方案:ASP.NET Core 8 同时支持OIDC与JWT登录及登出
一、核心思路
当配置多认证方案的授权策略后,IAuthenticateResultFeature不会自动填充——因为授权阶段会尝试所有指定方案,但不会记录具体哪个方案认证成功。我们需要手动追踪用户实际使用的认证方案,或通过其他方式获取有效认证方案来执行登出。
二、具体实现方案
方案1:自定义中间件追踪认证方案
在认证中间件之后、授权中间件之前,添加自定义中间件,捕获每个请求的认证结果并记录成功的方案:
app.UseAuthentication(); // 自定义中间件追踪认证方案 app.Use(async (context, next) => { // 遍历所有已认证的身份标识,提取对应的认证方案 var authenticatedSchemes = context.User.Identities .Where(id => id.IsAuthenticated) .Select(id => id.AuthenticationType) .Distinct(); if (authenticatedSchemes.Any()) { // 将成功的方案存入HttpContext.Items,后续登出时直接读取 context.Items["AuthenticatedScheme"] = authenticatedSchemes.First(); } await next(); }); app.UseAuthorization();
登出时,从HttpContext.Items中获取记录的方案,执行对应逻辑:
[HttpPost("logout")] public async Task<IActionResult> Logout() { if (HttpContext.Items.TryGetValue("AuthenticatedScheme", out var schemeObj) && schemeObj is string scheme) { if (scheme == OpenIdConnectDefaults.AuthenticationScheme) { // OIDC登出:同时清除Cookie并触发身份提供商登出 await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } else if (scheme == JwtBearerDefaults.AuthenticationScheme) { // JWT登出:无状态认证只需前端清除本地Token,后端返回提示即可 return Ok(new { message = "已退出登录" }); } } return RedirectToAction("Index", "Home"); }
方案2:利用认证事件钩子记录方案
在配置OIDC和JWT认证时,通过各自的OnTokenValidated事件,直接将当前认证方案存入HttpContext.Items:
services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(options => { // 你的OIDC基础配置 options.ClientId = "your-client-id"; options.Authority = "your-authority"; // ...其他配置 // OIDC认证成功后记录方案 options.Events = new OpenIdConnectEvents { OnTokenValidated = context => { context.HttpContext.Items["AuthenticatedScheme"] = OpenIdConnectDefaults.AuthenticationScheme; return Task.CompletedTask; } }; }) .AddJwtBearer(options => { // 你的JWT基础配置 options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "your-issuer", // ...其他配置 }; // JWT认证成功后记录方案 options.Events = new JwtBearerEvents { OnTokenValidated = context => { context.HttpContext.Items["AuthenticatedScheme"] = JwtBearerDefaults.AuthenticationScheme; return Task.CompletedTask; } }; }); // 配置授权策略,允许两种认证方案 services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); });
这种方式直接在认证成功的节点记录方案,逻辑更精准,后续登出时直接读取HttpContext.Items即可。
方案3:通过Claims持久化认证方案
在认证成功时,给用户添加一个记录认证方案的Claim,后续从User.Claims中读取:
OIDC配置示例:
options.Events = new OpenIdConnectEvents { OnTokenValidated = context => { context.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim("auth_scheme", OpenIdConnectDefaults.AuthenticationScheme) })); return Task.CompletedTask; } };
JWT配置示例:
options.Events = new JwtBearerEvents { OnTokenValidated = context => { context.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim("auth_scheme", JwtBearerDefaults.AuthenticationScheme) })); return Task.CompletedTask; } };
登出时读取Claim执行逻辑:
[HttpPost("logout")] public async Task<IActionResult> Logout() { var schemeClaim = User.Claims.FirstOrDefault(c => c.Type == "auth_scheme"); if (schemeClaim != null) { if (schemeClaim.Value == OpenIdConnectDefaults.AuthenticationScheme) { await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } else if (schemeClaim.Value == JwtBearerDefaults.AuthenticationScheme) { return Ok(new { message = "已退出登录" }); } } return RedirectToAction("Index", "Home"); }
三、注意事项
- JWT是无状态认证,后端无需存储会话,登出核心逻辑是前端清除本地存储的Token;OIDC登出需要同时清除Cookie并通知身份提供商完成全局登出。
- 若出现极端情况(比如请求同时携带OIDC Cookie和JWT Token),需根据业务逻辑设置优先级(比如优先处理OIDC方案)。
内容的提问来源于stack exchange,提问作者Eric
相关产品推荐
相关产品推荐

