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

ASP.NET Core自定义授权异常:GetPolicy未触发,默认策略被调用

问题排查与解决方案:ASP.NET Core自定义双参数授权特性不触发

首先,咱们先梳理下你遇到的核心问题:自定义的CustomAuthorize双参数特性没有触发,SubmodulePolicy.GetPolicyAsync始终没被调用,反而一直走默认策略;同时登录时添加的Claims可能没有正确持久化到后续请求的Principal中。下面一步步拆解问题并给出修正方案。

1. 核心原因:自定义授权特性的Policy未正确初始化

你的CustomAuthorize特性中,Policy的设置依赖于Type属性的setter,但构造函数初始化时并没有触发这个setter,导致Policy属性始终为空,授权系统只能使用默认策略,自然不会调用你的自定义GetPolicyAsync方法。

修正后的CustomAuthorize特性

public class CustomAuthorize : AuthorizeAttribute
{
    public SubmoduleActionType ActionType { get; }
    public SubmoduleType Type { get; }

    public CustomAuthorize(SubmoduleActionType submoduleActionType, SubmoduleType submoduleType)
    {
        ActionType = submoduleActionType;
        Type = submoduleType;
        // 直接在构造函数中生成策略名称,确保特性初始化时就绑定正确的Policy
        Policy = $"{Type.ToString()};{ActionType.ToString()}";
    }
}

这样构造函数执行时就会直接设置Policy,授权系统就能识别到你的自定义策略,进而调用GetPolicyAsync。

2. 优化SubmodulePolicy的策略解析逻辑

你的GetPolicyAsync方法中解析失败时抛出异常的做法不够健壮,应该交给默认策略提供者处理。同时优化参数拆分和枚举解析的逻辑:

public class SubmodulePolicy : IAuthorizationPolicyProvider
{
    public DefaultAuthorizationPolicyProvider DefaultPolicyProvider { get; }

    public SubmodulePolicy(IOptions<AuthorizationOptions> options)
    {
        DefaultPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
    }

    public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        var policyParts = policyName.Split(";");
        // 不符合自定义策略格式(不是分号分隔的双参数),交给默认提供者处理
        if (policyParts.Length != 2)
        {
            return DefaultPolicyProvider.GetPolicyAsync(policyName);
        }

        var submoduleTypeStr = policyParts[0];
        var actionTypeStr = policyParts[1];

        // 尝试解析枚举,成功则创建自定义策略
        if (Enum.TryParse(submoduleTypeStr, out SubmoduleType submoduleType) &&
            Enum.TryParse(actionTypeStr, out SubmoduleActionType actionType))
        {
            var policy = new AuthorizationPolicyBuilder()
                .AddRequirements(new SubmoduleTypeRequirement(actionType, submoduleType))
                .Build();
            return Task.FromResult(policy);
        }

        // 解析失败,交给默认策略提供者
        return DefaultPolicyProvider.GetPolicyAsync(policyName);
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
    {
        return DefaultPolicyProvider.GetDefaultPolicyAsync();
    }
}

3. 修复Claims的持久化问题

你在登录时设置Thread.CurrentPrincipal的方式在ASP.NET Core中是不可靠的——异步场景下线程可能切换,而且这个设置只对当前请求有效,后续请求的Principal会重置。如果不用JWT,推荐使用Cookie认证来持久化用户身份:

第一步:Startup中配置Cookie认证

// 在AddAuthorization之前添加认证服务
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true; // 防止前端脚本访问Cookie,提升安全性
        options.ExpireTimeSpan = TimeSpan.FromHours(8); // Cookie过期时间
        options.LoginPath = "/api/login"; // 未认证时的跳转路径(API场景可返回401)
        options.Events = new CookieAuthenticationEvents
        {
            OnRedirectToLogin = ctx =>
            {
                // API场景下不跳转,直接返回401
                ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return Task.CompletedTask;
            }
        };
    });

services.AddAuthorization();
// 注册你的授权服务
services.AddTransient<IAuthorizationPolicyProvider, SubmodulePolicy>();
services.AddSingleton<IAuthorizationHandler, SubmoduleAuthorizationHandler>();

同时在Configure方法中确保中间件顺序正确:

app.UseAuthentication(); // 认证中间件必须在授权中间件之前
app.UseAuthorization();

第二步:修正Login动作的Claims添加逻辑

[HttpGet("login")]
public async Task<IActionResult> Login()
{
    var userName = _httpContextAccessor.HttpContext.User.Identity.Name.GetUserNameFromHttpContext();
    var user = _userClient.GetUserByUserName(userName);
    
    if (user == null)
    {
        return Unauthorized($"User {userName} does not exist in DB");
    }

    var userModulesWithSubmodules = _loginClient.GetUserModulesWithSubmodules(userName);
    if (userModulesWithSubmodules.Count == 0)
    {
        return Conflict($"User {userName} has no modules");
    }

    var claims = new List<Claim>
    {
        // 用户名只需要添加一次,不要在循环中重复添加
        new Claim(ClaimTypes.Name, user.UserName)
    };

    foreach (var module in userModulesWithSubmodules)
    {
        foreach (var submodule in module.Submodules)
        {
            var submoduleActions = new List<string>();
            if (submodule.CanAdd) submoduleActions.Add("CanAdd");
            if (submodule.CanEdit) submoduleActions.Add("CanEdit");
            if (submodule.CanRead) submoduleActions.Add("CanRead");
            if (submodule.CanDelete) submoduleActions.Add("CanDelete");

            if (submoduleActions.Any())
            {
                claims.Add(new Claim(submodule.SubmoduleName.ToString(), string.Join(',', submoduleActions)));
            }
        }
    }

    // 创建ClaimsIdentity时指定认证类型(要和Cookie认证的Scheme对应)
    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

    // 使用SignInAsync将Principal持久化到Cookie中
    await _httpContextAccessor.HttpContext.SignInAsync(claimsPrincipal);

    var loggedInUserDto = new LoggedInUserDto 
    { 
        UserName = userName,
        UserModulesWithSubmodules = userModulesWithSubmodules
    };

    return Ok(loggedInUserDto);
}

4. 修复授权处理器的Claims读取逻辑

你的SubmoduleAuthorizationHandler中使用Convert.ToString(context.User.FindFirst(...))会获取整个Claim的字符串表示(比如"Type: xxx, Value: yyy"),而不是Claim的Value。改用FindFirstValue方法:

public class SubmoduleAuthorizationHandler : AuthorizationHandler<SubmoduleTypeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SubmoduleTypeRequirement submoduleRequirement)
    {
        // 校验参数是否有效
        if (!submoduleRequirement.ActionType.HasValue || !submoduleRequirement.Type.HasValue)
        {
            context.Fail();
            return Task.CompletedTask;
        }

        var submoduleTypeStr = submoduleRequirement.Type.ToString();
        var actionTypeStr = submoduleRequirement.ActionType.ToString();

        // 获取用户的该子模块权限
        var grantedRights = context.User.FindFirstValue(submoduleTypeStr);
        if (string.IsNullOrEmpty(grantedRights))
        {
            context.Fail();
            return Task.CompletedTask;
        }

        // 检查是否包含所需权限
        if (grantedRights.Split(',').Contains(actionTypeStr))
        {
            context.Succeed(submoduleRequirement);
        }
        else
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

关于双参数特性的疑问

ASP.NET Core对自定义AuthorizeAttribute的构造函数参数数量没有限制,你之前的问题完全是因为Policy未正确设置导致的,和参数数量无关。只要构造函数中正确设置了Policy属性,授权系统就能识别并调用对应的策略解析逻辑。

替代方案:使用过滤器

如果觉得授权策略的方式太复杂,也可以用Action过滤器实现类似逻辑。比如写一个CustomAuthorizationFilter:

public class CustomAuthorizationFilter : IActionFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CustomAuthorizationFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var endpoint = context.HttpContext.GetEndpoint();
        var customAuthorizeAttr = endpoint.Metadata.GetMetadata<CustomAuthorize>();
        if (customAuthorizeAttr == null) return;

        var user = _httpContextAccessor.HttpContext.User;
        var submoduleTypeStr = customAuthorizeAttr.Type.ToString();
        var actionTypeStr = customAuthorizeAttr.ActionType.ToString();

        var grantedRights = user.FindFirstValue(submoduleTypeStr);
        if (string.IsNullOrEmpty(grantedRights) || !grantedRights.Split(',').Contains(actionTypeStr))
        {
            context.Result = new ForbidResult();
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // 无需处理
    }
}

然后在Startup中注册过滤器,并在Action上添加[TypeFilter(typeof(CustomAuthorizationFilter))](或者直接在特性上实现IFilterMetadata)。不过这种方式的扩展性不如授权策略,推荐优先使用授权策略方案。

内容的提问来源于stack exchange,提问作者Rafal_Koscinski

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:21:29