You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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失败。

修复步骤

  1. 注册自定义PolicyProvider
    在ConfigureAuth方法中添加注册代码:
services.AddSingleton<IAuthorizationPolicyProvider, AppRolesPolicyProvider>();
  1. 修正自定义授权属性与策略的关联
    让AppRolesAuthorizeAttribute继承AuthorizeAttribute并设置符合PolicyProvider前缀的策略名称:
public class AppRolesAuthorizeAttribute : AuthorizeAttribute
{
    public AppRolesAuthorizeAttribute(string roles)
    {
        Policy = $"AppRoles{roles}";
        Roles = roles;
    }

    public new string Roles { get; set; }
}
  1. 统一需求与处理器的类型匹配
    定义独立的需求类:
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.");
    }
}
  1. 验证用户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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 02:33:10