ASP.NET Core自定义授权异常:GetPolicy未触发,默认策略被调用
首先,咱们先梳理下你遇到的核心问题:自定义的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

