如何在ASP.NET Core Web API(EF Core)中实现动态数据库授权
解决方案
1. 重构通用权限验证模型
替换单个权限对应单个Requirement的冗余写法,创建通用PermissionRequirement承载权限名称:
public class PermissionRequirement : IAuthorizationRequirement { // 存储需要验证的策略名(即权限码,如"UserListRead") public string PermissionName { get; } public PermissionRequirement(string permissionName) { PermissionName = permissionName ?? throw new ArgumentNullException(nameof(permissionName)); } }
2. 实现通用授权处理程序
编写统一的PermissionAuthorizationHandler,处理所有权限的验证逻辑,无需为每个权限单独写Handler:
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement> { private readonly DataContext _context; private readonly UserManager<UserArkiva> _userManager; private readonly IAuthService _authService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IMemoryCache _cache; private readonly CompanyWithPrivilegesMethod _privilegeService; // 仅注入必要服务,避免冗余依赖 public PermissionAuthorizationHandler(DataContext context, UserManager<UserArkiva> userManager, IAuthService authService, IHttpContextAccessor httpContextAccessor, IMemoryCache cache, CompanyWithPrivilegesMethod privilegeService) { _context = context; _userManager = userManager; _authService = authService; _httpContextAccessor = httpContextAccessor; _cache = cache; _privilegeService = privilegeService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) { context.Fail(); return; } // 获取当前登录用户名(异步调用避免死锁) var userNameResult = await _authService.GetUserNameFromToken(httpContext); if (string.IsNullOrEmpty(userNameResult.UserName)) { context.Fail(); return; } // 从缓存或数据库获取用户权限列表,减少DB查询压力 var cacheKey = $"UserPermissions_{userNameResult.UserName}"; var userPermissions = await _cache.GetOrCreateAsync(cacheKey, async entry => { // 缓存5分钟,可根据实际权限变更频率调整 entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await _privilegeService.GetCompaniesWithPrivilegesAsync(userNameResult.UserName); }); // 验证用户是否拥有当前策略对应的权限 if (userPermissions.Any(p => p.PrivilegeName == requirement.PermissionName)) { context.Succeed(requirement); } else { context.Fail(); } } }
3. 动态批量注册权限策略
在Program.cs中从数据库读取所有权限码,循环注册策略,无需手动添加50+个:
var builder = WebApplication.CreateBuilder(args); // 注册必要服务 builder.Services.AddDbContext<DataContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddIdentity<UserArkiva, Roles>() .AddEntityFrameworkStores<DataContext>(); builder.Services.AddAuthorization(); // 注册通用授权处理程序和权限服务 builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>(); builder.Services.AddScoped<CompanyWithPrivilegesMethod>(); builder.Services.AddMemoryCache(); builder.Services.AddHttpContextAccessor(); var app = builder.Build(); // 动态注册权限策略 using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; var context = services.GetRequiredService<DataContext>(); // 从数据库读取所有权限码(假设权限表为Permissions,字段为Name) var permissions = await context.Permissions.Select(p => p.Name).ToListAsync(); var authorizationOptions = services.GetRequiredService<IOptions<AuthorizationOptions>>().Value; foreach (var permission in permissions) { authorizationOptions.AddPolicy(permission, policy => { policy.Requirements.Add(new PermissionRequirement(permission)); }); } } // 后续中间件配置 app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
4. 关键问题解决说明
- 获取当前策略名:通过
PermissionRequirement.PermissionName直接拿到当前要验证的策略名称(如"UserListRead"),无需额外处理。 - Requirement为null的问题:原代码中
UserListReadRequirement无构造函数且未正确初始化,改用通用Requirement后,每个策略都会创建带权限名的实例,Handler能正常接收参数。 - 避免Token过长:不在Token中存储权限列表,仅保留用户标识,每次授权时从数据库(或缓存)拉取最新权限,既精简Token,又实现权限动态生效(无需重新登录)。
5. 额外优化建议
- 将
GetCompaniesWithPrivileges改为异步方法,避免同步调用导致的性能问题。 - 权限缓存时间可根据业务调整,若权限变更频繁可缩短缓存时长,或提供权限变更时主动清除缓存的逻辑。
- 确保
CompanyWithPrivilegesMethod通过依赖注入获取服务,而非在Handler中手动实例化,符合DI设计原则。
内容的提问来源于stack exchange,提问作者Leo Ramadani
相关产品推荐
相关产品推荐

