.NET 8自定义授权处理器触发DenyAnonymousAuthorizationRequirement错误排查
ASP.NET Core 8 自定义授权处理器返回403问题排查
问题现象
调试时自定义授权处理器代码能被命中,且已成功标记需求通过,但接口仍返回403错误。错误原因显示PassthroughAuthorizationHandler的DenyAnonymousAuthorizationRequirement未被满足。
已确认认证流程正常:UI能显示用户名,其他使用[Authorize]角色属性的端点工作正常,仅自定义处理器的接口出现该问题。
相关代码
认证配置代码
public static IServiceCollection ConfigureAuth(this IServiceCollection services) { services .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var userManager = context.HttpContext.RequestServices.GetRequiredService<ICurrentUserManager>(); context.Principal = userManager.LoginUserWithCertificate(context .ClientCertificate); context.Success(); return Task.CompletedTask; } }; }); services.AddAuthorization(); services.AddScoped<IAuthorizationHandler, AppRolesAuthorizationHandler>(); return services; }
自定义授权处理器代码
public class AppRolesAuthorizationHandler:AuthorizationHandler<AppRolesAuthorizeAttribute> { IHttpContextAccessor httpContextAccessor; public AppRolesAuthorizationHandler(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AppRolesAuthorizeAttribute requirement) { string rolesString = requirement.Roles; string[] roles = rolesString.Split(","); foreach (string entry in roles) { string role = entry.Trim(); if (!context.User.IsInRole(role)) { continue; } if (role != Role.Project.ToString()) { context.Succeed(requirement); return Task.CompletedTask; } long appId = getAppId(context); if (appId == -1) { context.Fail(); return Task.CompletedTask; } User user = context.User as User; App appFound = user.Apps.Find((app) => app.Id.Value == appId); if (appFound != null) { context.Succeed(requirement); return Task.CompletedTask; } } context.Fail(); return Task.CompletedTask; } private long getAppId(AuthorizationHandlerContext context) { var routeData = httpContextAccessor.HttpContext.Request.RouteValues; if (routeData.TryGetValue("appId", out object? appIdObj)) { long appId = long.Parse(appIdObj.ToString()); return appId; } // no appId specified as request param, so this attribute was incorrectly used throw new Exception("Only use AuthorizeAppApiAttribute on methods with 'appId' parameter."); } }
Program.cs 中间件配置
using Hangfire; using MMS.Core; using MMS.Infrastructure; using MMS.Web.Services; var builder = WebApplication.CreateBuilder(args); // Add and configure services to the container (IOC). #region 3rd party service Configurations builder.Configuration.AddEnvironmentVariables(); builder.Services.ConfigureConnectionStrings(builder.Configuration); builder.Services.ConfigureThirdParties(); builder.Services.ConfigureUI(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.ConfigureValidation(); builder.Services.AddLogging(logging => logging.AddConsole()); #endregion #region Domain Services builder.Services.ConfigureWeb(); builder.Services.ConfigureInfrastructure(builder.Configuration); builder.Services.ConfigureCore(); //Auth is configured last because we need the domain services to be registered first builder.Services.ConfigureAuth(); builder.Services.ConfigureSerializationOptions(); #endregion var app = builder.Build(); // Configure the HTTP request pipeline. #region Middlewares if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); app.UseHttpsRedirection(); } app.UseWebOptimizer(); // WebOptimizer for Bundling the UI app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseSwagger(); app.UseSwaggerUI(); app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new[] { new HangfireAuthorizationFilter() } }); app.UseExceptionHandler(exceptionHandlerApp => { exceptionHandlerApp.ConfigureErrorHandlingMiddleware(); }); app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Apps}/{id=UrlParameter.Optional}"); app.MapFallbackToFile("index.html"); #endregion app.Run();
自定义PolicyProvider代码
public class AppRolesPolicyProvider: IAuthorizationPolicyProvider { const string POLICY_PREFIX = "AppRoles"; public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } public AppRolesPolicyProvider(IOptions<AuthorizationOptions> options) { FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); } public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => Task.FromResult<AuthorizationPolicy>(null); public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName) { if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase)) { var roles = policyName.Substring(POLICY_PREFIX.Length); var policy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(CertificateAuthenticationDefaults.AuthenticationScheme) .AddRequirements(new AppRolesRequirement(roles)) .Build(); return Task.FromResult<AuthorizationPolicy?>(policy); } return Task.FromResult<AuthorizationPolicy?>(null); } }
控制器使用示例
[HttpGet, Route("{appId:long}", Name = "GetApp")] [AppRolesAuthorize("Assembly, Project, Viewer")] public ActionResult<App> Get(long appId) { ... }
问题根源与解决方案
核心问题1:PolicyProvider未注册到DI容器
你实现了AppRolesPolicyProvider但未在ConfigureAuth方法中注册,导致授权系统无法使用该Provider解析自定义策略,只能依赖默认逻辑,进而触发匿名检查失败。
核心问题2:自定义需求与处理器类型不匹配
处理器处理的是AppRolesAuthorizeAttribute,但PolicyProvider中添加的需求是AppRolesRequirement,类型不匹配导致授权逻辑无法正确关联。
核心问题3:用户Principal可能未标记为已认证
如果LoginUserWithCertificate创建的ClaimsPrincipal的Identity.IsAuthenticated为false,授权系统会将用户视为匿名,触发DenyAnonymousAuthorizationRequirement失败。
修复步骤
- 注册自定义PolicyProvider
在ConfigureAuth方法中添加注册代码:
services.AddSingleton<IAuthorizationPolicyProvider, AppRolesPolicyProvider>();
- 修正自定义授权属性与策略的关联
让AppRolesAuthorizeAttribute继承AuthorizeAttribute并设置符合PolicyProvider前缀的策略名称:
public class AppRolesAuthorizeAttribute : AuthorizeAttribute { public AppRolesAuthorizeAttribute(string roles) { Policy = $"AppRoles{roles}"; Roles = roles; } public new string Roles { get; set; } }
- 统一需求与处理器的类型匹配
定义独立的需求类:
public class AppRolesRequirement : IAuthorizationRequirement { public string Roles { get; } public AppRolesRequirement(string roles) { Roles = roles; } }
修改处理器以处理该需求:
public class AppRolesAuthorizationHandler : AuthorizationHandler<AppRolesRequirement> { private readonly IHttpContextAccessor _httpContextAccessor; public AppRolesAuthorizationHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AppRolesRequirement requirement) { string rolesString = requirement.Roles; string[] roles = rolesString.Split(","); foreach (string entry in roles) { string role = entry.Trim(); if (!context.User.IsInRole(role)) { continue; } if (role != Role.Project.ToString()) { context.Succeed(requirement); return Task.CompletedTask; } long appId = GetAppId(context); if (appId == -1) { context.Fail(); return Task.CompletedTask; } User user = context.User as User; App appFound = user.Apps.Find((app) => app.Id.Value == appId); if (appFound != null) { context.Succeed(requirement); return Task.CompletedTask; } } context.Fail(); return Task.CompletedTask; } private long GetAppId(AuthorizationHandlerContext context) { var routeData = _httpContextAccessor.HttpContext.Request.RouteValues; if (routeData.TryGetValue("appId", out object? appIdObj) && long.TryParse(appIdObj.ToString(), out long appId)) { return appId; } throw new Exception("Only use AppRolesAuthorizeAttribute on methods with 'appId' parameter."); } }
- 验证用户Principal的认证状态
在认证事件中添加检查:
context.Principal = userManager.LoginUserWithCertificate(context.ClientCertificate); if (!context.Principal.Identity.IsAuthenticated) { context.Fail("User identity is not marked as authenticated."); return Task.CompletedTask; } context.Success();
排查建议
- 调试时检查
context.User.Identity.IsAuthenticated的值,确认用户是否被正确标记为已认证。 - 在
AppRolesPolicyProvider的GetPolicyAsync方法中添加断点,确认是否被调用。 - 启用授权日志查看详细流程:在
appsettings.json中添加配置
{ "Logging": { "LogLevel": { "Microsoft.AspNetCore.Authorization": "Debug" } } }
内容的提问来源于stack exchange,提问作者Kathi
相关产品推荐
相关产品推荐

