.NET 6 MVC JWT认证SignInAsync报错问题及解决
.NET 6 MVC中JWT认证(结合OIDC)的问题与解决
需求背景
基于.NET 6和ASP.NET MVC实现JWT认证:通过OpenID Connect对接公司身份提供商完成认证,获取JWT访问令牌并自动添加到请求头,用于后续内部服务授权调用。所有控制器均添加[Authorize]特性,不接受基于Cookie的认证方案。
初始实现代码
以下是Program.cs中的初始认证配置代码:
const string AUTH_COOKIE_NAME = "access_token"; const string AUTH_SCHEMA_NAME = "OPENIDC_JWT"; const string BEARER_AUTH_SCHEMA_NAME = "bearer"; const string OIDC_AUTH_SCHEMA_NAME = "oidc"; builder.Services.AddAuthentication(options => { options.DefaultScheme = AUTH_SCHEMA_NAME; options.DefaultAuthenticateScheme = OIDC_AUTH_SCHEMA_NAME; options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME; }) .AddOpenIdConnect(OIDC_AUTH_SCHEMA_NAME, o => { o.MetadataAddress = builder.Configuration["SingleSignOn:MetaData"]; o.Authority = builder.Configuration["SingleSignOn:Authority"]; o.ClientId = builder.Configuration["SingleSignOn:ClientId"]; o.ClientSecret = builder.Configuration["SingleSignOn:ClientSecret"]; o.ResponseType = OpenIdConnectResponseType.Code; o.SaveTokens = true; o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("email"); o.CallbackPath = "/signin-oidc"; o.AccessDeniedPath = "/Account/AccessDenied"; o.SignInScheme = BEARER_AUTH_SCHEMA_NAME; o.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { var accessToken = context.SecurityToken as JwtSecurityToken; if (accessToken != null) { // 将访问令牌存储到认证属性中 context.Properties.StoreTokens(new[] { new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData }, }); } context.Request.Headers.Append("Authorization", $"Bearer {accessToken.RawData}"); var roleService = context.HttpContext.RequestServices.GetRequiredService<IUserRolesService>(); roleService.ManageUserRoles(context); }, }; }) .AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options => { options.MetadataAddress = "https://access-staging.epam.com/auth/realms/plusx/.well-known/openid-configuration"; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, // 应设置为true并指定应用的ValidAudience ValidAudience = builder.Configuration["SingleSignOn:ClientId"], ValidateIssuerSigningKey = true, }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME]; // 优先从Authorization头取令牌,没有则从认证Cookie获取 if (!string.IsNullOrEmpty(authCookie)) { context.Token = authCookie; } return Task.CompletedTask; } }; }) .AddPolicyScheme(AUTH_SCHEMA_NAME, AUTH_SCHEMA_NAME, options => { options.ForwardDefaultSelector = context => { string authHeader = context.Request.Headers[HeaderNames.Authorization]; string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME]; // 若请求头或Cookie中有令牌,使用bearer方案;否则使用oidc方案 if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie)) return BEARER_AUTH_SCHEMA_NAME; return OIDC_AUTH_SCHEMA_NAME; }; }); builder.Services.AddAuthorization(); // 其他服务配置 var app = builder.Build(); AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); // 配置HTTP请求管道 if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts();// 确保所有内容均通过HTTPS传输 } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseMiddleware<SecurityHeadersMiddleware>(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Admin}/{action=Index}/{id?}"); app.Run();
角色分配服务示例代码:
public void ManageUserRoles(TokenValidatedContext context) { var user = context.Principal; if (user.Identity.IsAuthenticated) { if (!user.HasClaim(c => c.Type == ClaimTypes.Role)) { if (!string.IsNullOrEmpty(userEmail) && isAdmin) { _logger.LogInformation($"Set the '{RoleNames.Admin}' role to user with email: '{userEmail}'"); identity.AddClaim(new Claim(ClaimTypes.Role, RoleNames.Admin)); } } }
错误信息
离开OnTokenValidated事件后触发错误:
InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT.
堆栈跟踪:
System.InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT. at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties) at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync() at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Middlewares.SecurityHeadersMiddleware.InvokeAsync(HttpContext context) in Middlewares\SecurityHeadersMiddleware.cs:line 23 at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
尝试过的Scheme配置
- 仅将Bearer设为默认Scheme:无效果
- 将
AUTH_SCHEMA_NAME设为默认、默认认证、默认挑战Scheme:报错相同 - 仅设置DefaultScheme为
AUTH_SCHEMA_NAME:报错相同 - 设置DefaultScheme为
BEARER_AUTH_SCHEMA_NAME,DefaultChallengeScheme为OIDC_AUTH_SCHEMA_NAME:报错相同
解决方案
通过调整认证Scheme的核心配置解决问题,若需自定义身份声明,需实现IClaimsTransformation接口。以下是可行的完整配置代码:
// 添加服务到容器 builder.Services.AddTransient<IManageUsersRolesJwtContext, UserRolesService>(); builder.Services.AddTransient<IClaimsTransformation, ShipmentClaimsTransformation>(); // 定义认证方案和Cookie名称常量 const string AUTH_COOKIE_NAME = "access_token"; const string CUSTOM_POLICY_SCHEMA_NAME = "OIDC_JWT"; const string BEARER_AUTH_SCHEMA_NAME = "Bearer"; const string OIDC_AUTH_SCHEMA_NAME = "oidc"; const string METADATA_ADDRESS = "https://***/openid-configuration"; // 配置Cookie策略选项 builder.Services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); // 配置认证服务 builder.Services.AddAuthentication(options => { options.DefaultScheme = CUSTOM_POLICY_SCHEMA_NAME; options.DefaultAuthenticateScheme = BEARER_AUTH_SCHEMA_NAME; options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME; }) // 添加OpenID Connect认证 .AddOpenIdConnect(OIDC_AUTH_SCHEMA_NAME, o => { o.MetadataAddress = builder.Configuration["SingleSignOn:MetaData"]; o.Authority = builder.Configuration["SingleSignOn:Authority"]; o.ClientId = builder.Configuration["SingleSignOn:ClientId"]; o.ClientSecret = builder.Configuration["SingleSignOn:ClientSecret"]; o.GetClaimsFromUserInfoEndpoint = true; o.ResponseType = "code"; o.SaveTokens = true; o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("email"); o.CallbackPath = "/signin-oidc"; o.Events = new OpenIdConnectEvents { // 令牌验证通过后存储访问令牌 OnTokenValidated = context => { var accessToken = context.SecurityToken as JwtSecurityToken; if (accessToken != null) { // 将访问令牌存储到认证属性中 context.Properties.StoreTokens(new[] { new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData }, }); } // 设置认证Cookie context.HttpContext.Response.Cookies.Append(AUTH_COOKIE_NAME, accessToken.RawData, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.None, Expires = context.SecurityToken.ValidTo.AddMinutes(-1) }); context.Response.Headers.Add("Authorization", $"Bearer {accessToken.RawData}"); // 重定向到目标地址并结束响应处理 context.Response.Redirect(context.Properties.RedirectUri); context.HandleResponse(); return Task.CompletedTask; } }; }) // 添加JWT Bearer认证 .AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options => { // 配置JWT选项 options.MetadataAddress = METADATA_ADDRESS; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = builder.Configuration["SingleSignOn:Authority"], ValidateAudience = true, // 应设置为true并指定应用的ValidAudience ValidAudience = builder.Configuration["SingleSignOn:ClientId"], ValidateIssuerSigningKey = true, }; options.Events = new JwtBearerEvents { // 从Authorization头或认证Cookie中获取令牌 OnMessageReceived = context => { string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME]; string authHeader = context.Request.Headers[HeaderNames.Authorization]; // 优先从Authorization头取令牌,没有则从认证Cookie获取 if (!string.IsNullOrEmpty(authCookie)) { context.Token = authCookie; } return Task.CompletedTask; } }; }) // 添加自定义策略方案,用于选择认证方式 .AddPolicyScheme(CUSTOM_POLICY_SCHEMA_NAME, CUSTOM_POLICY_SCHEMA_NAME, options => { options.ForwardDefaultSelector = context => { var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false; string authHeader = context.Request.Headers[HeaderNames.Authorization]; string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME]; // 若请求头或Cookie中有令牌,使用Bearer方案;否则使用OIDC方案 if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie)) return BEARER_AUTH_SCHEMA_NAME; else return OIDC_AUTH_SCHEMA_NAME; }; }); builder.Services.AddAuthorization();
内容的提问来源于stack exchange,提问作者lob_2020
相关产品推荐
相关产品推荐

