未授权访问API返回401错误及多认证方案配置疑问
我有一个标记了[Authorize]特性的API控制器方法,目前携带Bearer令牌访问一切正常,但无认证(比如Postman不携带授权信息)时,会抛出错误:
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
我希望未认证/授权用户访问时返回401错误,同时应用需要支持多种认证方案。另外我有几个困惑:
- 每次带options参数调用
.AddAuthentication()都会向IServiceCollection添加新的AuthenticationOptions,我多次设置了DefaultChallengeScheme,但系统仍无法识别默认值,这是为什么? - 是否需要设置一个真正的默认
AuthenticationOptions? - 由于
.AddJwtBearer等调用也会添加AuthenticationOptions,无法避免多个该选项,多AuthenticationOptions下如何设置默认挑战方案?
附上我的Startup.cs中ConfigureServices的相关代码:
//services var = IServiceCollection services // Adding authentication var firstUserpoolName = userpools.First().Key; var authenticationBuilder = services.AddAuthentication(options => { options.DefaultScheme = firstUserpoolName; options.DefaultChallengeScheme = firstUserpoolName; }); // Adding authentication for each userpool name foreach (var userpool in userpools) { var userpoolDetails = userpool.GetChildren().ToList(); var userpoolAuthenticationBuilder = services.AddAuthentication(); userpoolAuthenticationBuilder.AddJwtBearer(userpool.Key, options => { options.Audience = userpoolDetails.FirstOrDefault(d => d.Key == "Audience")?.Value; // More options set here in actual code }); } string prioritySystem = Configuration["Priority_System"].ToLower(); string loginPath = string.Format("/{0}/login", prioritySystem); //Adding cookie authentication var cookiesAuthenticationBuilder = services.AddAuthentication(options => { // Adding these options allows for the correct HttpContext to be accessible in any middleware options.DefaultChallengeScheme = AuthenticationSchemes.CookieAuthenticationDefault; options.DefaultAuthenticateScheme = AuthenticationSchemes.CookieAuthenticationDefault; }); cookiesAuthenticationBuilder.AddCookie(options => { options.LoginPath = loginPath; options.EventsType = typeof(CustomCookieAuthenticationEvents); // More options set here in actual code }); // Adding SSO authentication var ssoConfig = Configuration.GetSection(AuthenticationSchemes.OIDC_SSO); if (ssoConfig.Exists()) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = AuthenticationSchemes.CookieAuthenticationDefault; options.DefaultSignInScheme = AuthenticationSchemes.CookieAuthenticationDefault; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }); } else { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = AuthenticationSchemes.CookieAuthenticationDefault; options.DefaultSignInScheme = AuthenticationSchemes.CookieAuthenticationDefault; }); } if (ssoConfig.Exists()) { // Setup SSO auth services.Configure<OpenIdConnectOptions>(builderSSOConfig); var serviceProvider = services.BuildServiceProvider(); var builderAuthOptions = serviceProvider.GetService<IOptions<OpenIdConnectOptions>>(); services.AddAuthentication().AddOpenIdConnect(AuthenticationSchemes.OIDC_SSO, options => { options.CallbackPath = "/signin-oidc"; // More options set here in actual code }); }
核心问题:重复调用AddAuthentication()覆盖了默认配置
每次调用services.AddAuthentication(options => { ... })都会重新注册全局的AuthenticationOptions,后续调用会直接覆盖之前的配置。你多次调用这个方法,导致最初设置的默认Scheme被后续的Cookie、SSO配置覆盖,最终可能因为逻辑分支问题,没有一个有效的DefaultChallengeScheme被保留,才会出现找不到默认挑战方案的错误。
而且,添加认证方案(比如AddJwtBearer、AddCookie)不需要每次都重新调用AddAuthentication(),正确的做法是只调用一次AddAuthentication()设置全局默认配置,然后通过返回的AuthenticationBuilder添加所有认证方案。
解决方案步骤
1. 统一认证配置入口,只调用一次AddAuthentication()
先确定你的全局默认Scheme:如果是API为主,优先JWT;如果是混合Web页面,优先Cookie。这里假设你需要API返回401,所以默认挑战方案设置为第一个用户池的JWT Scheme。
修改后的代码结构:
// 第一步:只调用一次AddAuthentication,设置全局默认配置 var firstUserpoolName = userpools.First().Key; var authBuilder = services.AddAuthentication(options => { // 设置默认认证方案(可选,根据业务场景调整) options.DefaultAuthenticateScheme = firstUserpoolName; // 关键:设置默认挑战方案,确保未认证时触发401 options.DefaultChallengeScheme = firstUserpoolName; }); // 第二步:添加所有JWT方案(每个用户池) foreach (var userpool in userpools) { var userpoolDetails = userpool.GetChildren().ToList(); authBuilder.AddJwtBearer(userpool.Key, options => { options.Audience = userpoolDetails.FirstOrDefault(d => d.Key == "Audience")?.Value; // 其他JWT配置... }); } // 第三步:添加Cookie认证方案 string prioritySystem = Configuration["Priority_System"].ToLower(); string loginPath = $"/{prioritySystem}/login"; authBuilder.AddCookie(AuthenticationSchemes.CookieAuthenticationDefault, options => { options.LoginPath = loginPath; options.EventsType = typeof(CustomCookieAuthenticationEvents); // 针对API场景,关闭登录跳转,返回401 options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; }; // 其他Cookie配置... }); // 第四步:添加SSO(OIDC)认证方案 var ssoConfig = Configuration.GetSection(AuthenticationSchemes.OIDC_SSO); if (ssoConfig.Exists()) { authBuilder.AddOpenIdConnect(AuthenticationSchemes.OIDC_SSO, options => { options.CallbackPath = "/signin-oidc"; // 其他OIDC配置... }); // 如果需要针对SSO场景调整默认Scheme,用Configure修改全局配置,而非重新调用AddAuthentication services.Configure<AuthenticationOptions>(options => { options.DefaultSignInScheme = AuthenticationSchemes.CookieAuthenticationDefault; }); }
2. 让[Authorize]支持所有认证方案
默认[Authorize]会使用DefaultAuthenticateScheme验证身份,要让它接受所有配置的认证方案,可自定义全局授权策略:
services.AddAuthorization(options => { // 收集所有已配置的认证方案 var allSchemes = new List<string>(); allSchemes.AddRange(userpools.Select(u => u.Key)); allSchemes.Add(AuthenticationSchemes.CookieAuthenticationDefault); if (ssoConfig.Exists()) { allSchemes.Add(AuthenticationSchemes.OIDC_SSO); } // 设置默认授权策略,包含所有认证方案 options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(allSchemes.ToArray()) .RequireAuthenticatedUser() .Build(); });
这样所有标记[Authorize]的方法会自动尝试所有配置的认证方案,只要其中一个验证通过就允许访问。
3. 确保未认证时返回401
只要正确设置DefaultChallengeScheme为JWT方案,未认证时JWT中间件会自动返回401响应。如果是Cookie方案,通过上面配置的OnRedirectToLogin事件,也会在API场景下返回401而非跳转登录页。
关于多AuthenticationOptions的疑问
- 你不需要多个
AuthenticationOptions,它是全局配置,整个应用应该只有一个。重复调用AddAuthentication(options => {})会覆盖之前的配置,这就是你多次设置DefaultChallengeScheme却无效的原因。 - 添加认证方案(
AddJwtBearer、AddCookie等)不会新增AuthenticationOptions,它们只是注册对应的认证处理程序。全局配置由第一次AddAuthentication()设置,后续可通过services.Configure<AuthenticationOptions>(...)修改,无需重新调用AddAuthentication()。
内容的提问来源于stack exchange,提问作者axel




