ASP.NET Core 6基于Claims和Policy的角色权限授权访问异常排查
ASP.NET Core 6授权异常排查:已认证用户权限匹配仍被拒绝
在ASP.NET Core 6项目中搭建了角色权限管理系统,支持为角色分配应用权限,通过Claims和Policy实现授权控制。但出现异常:用户完成认证且持有正确角色、权限Claims的情况下,访问受保护资源仍跳转到“访问被拒绝”页面。
相关代码
权限常量定义
using System.Collections.Generic; namespace TheGospelMission.Constants { public static class Permissions { public static List<string> GeneratePermissionsForModule(string module) { return new List<string>() { $"Permission.{module}.All", $"Permission.{module}.Create", $"Permission.{module}.View", $"Permission.{module}.Edit", $"Permission.{module}.Delete", }; } public static class Admin { public const string All = "Permission.Admin.All"; public const string View = "Permission.Admin.View"; public const string Create = "Permission.Admin.Create"; public const string Edit = "Permission.Admin.Edit"; public const string Delete = "Permission.Admin.Delete"; } public static class Roles { public const string All = "Permission.Admin.All"; } public static class Church { public const string View = "Permission.Church.View"; public const string Create = "Permission.Church.Create"; public const string Edit = "Permission.Church.Edit"; public const string Delete = "Permission.Church.Delete"; } public static class Members { public const string View = "Permission.Members.View"; public const string Create = "Permission.Members.Create"; public const string Edit = "Permission.Members.Edit"; public const string Delete = "Permission.Members.Delete"; } public static class Groups { public const string View = "Permission.Groups.View"; public const string Create = "Permission.Groups.Create"; public const string Edit = "Permission.Groups.Edit"; public const string Delete = "Permission.Groups.Delete"; } public static class Attendances { public const string View = "Permission.Attendances.View"; public const string Create = "Permission.Attendances.Create"; public const string Edit = "Permission.Attendances.Edit"; public const string Delete = "Permission.Attendances.Delete"; } } }
ClaimsHelper类
using Microsoft.AspNetCore.Identity; using TheGospelMission.ViewModels; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Security.Claims; using System.Threading.Tasks; namespace TheGospelMission.Helpers { public static class ClaimsHelper { public static void GetPermissions(this List<RoleClaimsViewModel> allPermissions, Type policy, string roleId) { FieldInfo[] fields = policy.GetFields(BindingFlags.Static | BindingFlags.Public); foreach (FieldInfo fi in fields) { allPermissions.Add(new RoleClaimsViewModel { Value = fi.GetValue(null).ToString(), Type = "Permissions" }); } } public static async Task AddPermissionClaim(this RoleManager<IdentityRole> roleManager, IdentityRole role, string permission) { var allClaims = await roleManager.GetClaimsAsync(role); if (!allClaims.Any(a => a.Type == "Permission" && a.Value == permission)) { await roleManager.AddClaimAsync(role, new Claim("Permission", permission)); } } } }
授权处理类
using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; namespace TheGospelMission.Permission { internal class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement> { private readonly ILogger<PermissionAuthorizationHandler> _logger; public PermissionAuthorizationHandler(ILogger<PermissionAuthorizationHandler> logger) { _logger = logger; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { try { if (context.User == null) { return; } _logger.LogInformation("User Claims:"); foreach (var claim in context.User.Claims) { _logger.LogInformation($"{claim.Type}: {claim.Value}"); } var permissions = context.User.Claims.Where(x => x.Type == "Permissions" && x.Value == requirement.Permission && x.Issuer == "LOCAL AUTHORITY"); if (permissions.Any()) { context.Succeed(requirement); return; } } catch (Exception ex) { _logger.LogError(ex, "Error during permission authorization"); } context.Fail(); // Authorization failed } } } using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; namespace TheGospelMission.Permission { internal class PermissionPolicyProvider : IAuthorizationPolicyProvider { private readonly ILogger<PermissionPolicyProvider> _logger; public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } public PermissionPolicyProvider(IOptions<AuthorizationOptions> options, ILogger<PermissionPolicyProvider> logger) { _logger = logger; // ASP.NET Core中只能有一个策略提供程序 // 我们仅处理权限相关策略,其余使用默认提供程序 FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); } public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); // 动态创建包含权限要求的策略,策略名称需匹配所需权限 public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName) { if (policyName.StartsWith("Permission", StringComparison.OrdinalIgnoreCase)) { var policy = new AuthorizationPolicyBuilder(); policy.AddRequirements(new PermissionRequirement(policyName)); var createdPolicy = policy.Build(); _logger.LogInformation($"创建策略 '{policyName}': {string.Join(", ", createdPolicy.Requirements.Select(r => r.GetType().Name))}"); return Task.FromResult<AuthorizationPolicy?>(policy.Build()); } // 非权限类策略,使用默认提供程序处理 return FallbackPolicyProvider.GetPolicyAsync(policyName); } public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() { return Task.FromResult<AuthorizationPolicy?>(null); } } } using Microsoft.AspNetCore.Authorization; namespace TheGospelMission.Permission { internal class PermissionRequirement : IAuthorizationRequirement { public string Permission { get; private set; } public PermissionRequirement(string permission) { Permission = permission; } } }
日志信息
info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] User Claims: TheGospelMission.Permission.PermissionAuthorizationHandler: Information: User Claims: info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: d66790ae-e8ea-4e09-a142-ea76def0199e TheGospelMission.Permission.PermissionAuthorizationHandler: Information: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: d66790ae-e8ea-4e09-a142-ea76def0199e info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: TestUser TheGospelMission.Permission.PermissionAuthorizationHandler: Information: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: TestUser info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress: tt@mail.com TheGospelMission.Permission.PermissionAuthorizationHandler: Information: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress: tt@mail.com TheGospelMission.Permission.PermissionAuthorizationHandler: Information: AspNet.Identity.SecurityStamp: P2KUBBQ6NCGLXTQKBDDQVRUQ3QVQS6WW info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] AspNet.Identity.SecurityStamp: P2KUBBQ6NCGLXTQKBDDQVRUQ3QVQS6WW info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] Permission: Permission.Members.View TheGospelMission.Permission.PermissionAuthorizationHandler: Information: Permission: Permission.Members.View info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] Permission: Permission.Members.Create TheGospelMission.Permission.PermissionAuthorizationHandler: Information: Permission: Permission.Members.Create info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] Permission: Permission.Members.View TheGospelMission.Permission.PermissionAuthorizationHandler: Information: Permission: Permission.Members.View info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] http://schemas.microsoft.com/ws/2008/06/identity/claims/role: Group Leader TheGospelMission.Permission.PermissionAuthorizationHandler: Information: http://schemas.microsoft.com/ws/2008/06/identity/claims/role: Group Leader TheGospelMission.Permission.PermissionAuthorizationHandler: Information: Permission: Permission.Members.Create info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] Permission: Permission.Members.Create info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] Permission: Permission.Members.View TheGospelMission.Permission.PermissionAuthorizationHandler: Information: Permission: Permission.Members.View info: TheGospelMission.Permission.PermissionAuthorizationHandler[0] amr: pwd TheGospelMission.Permission.PermissionAuthorizationHandler: Information: amr: pwd
问题排查与解决方案
对比代码和日志,核心问题有两个:
Claim Type拼写不一致
ClaimsHelper.AddPermissionClaim方法中添加的权限Claim Type是"Permission"(单数)PermissionAuthorizationHandler中判断的Claim Type是"Permissions"(复数)- 日志显示用户持有的权限Claim Type为
"Permission",与Handler中的判断条件不匹配
修复方式:统一Claim Type的拼写,将Handler中的判断改为
x.Type == "Permission",或者将ClaimsHelper中的Type改为"Permissions",确保全项目一致。Issuer判断条件不符合实际情况
- Handler中添加了
x.Issuer == "LOCAL AUTHORITY"的判断,但日志中未显示任何Claim的Issuer为该值(默认ASP.NET Identity的Claim Issuer可能不是这个值,且日志未打印Issuer信息),导致匹配失败 - 修复方式:移除Issuer判断条件,或者确认添加Claim时指定的Issuer并修改判断逻辑。
- Handler中添加了
修改后的PermissionAuthorizationHandler.HandleRequirementAsync核心判断代码:
var permissions = context.User.Claims.Where(x => x.Type == "Permission" && // 统一为单数 x.Value == requirement.Permission); if (permissions.Any()) { context.Succeed(requirement); return; }
- 额外检查点
- 确认在Program.cs中已注册授权服务:
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>(); builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>(); - 清除浏览器缓存或重新登录,确保新的Claim生效。
- 确认在Program.cs中已注册授权服务:
内容的提问来源于stack exchange,提问作者Timmy
相关产品推荐
相关产品推荐

