ASP.NET Core:自定义AuthorizationHandler中Scoped服务注入问题
解决方案:ASP.NET Core多租户授权中CurrentTenantService实例复用问题
问题根源
单例的PermissionAuthorizationPolicyProvider如果直接注入Scoped服务(如ICurrentTenantService),会导致服务实例来自根容器而非当前请求域,因此无法复用已初始化租户数据的实例。同时,若权限策略构建逻辑过度依赖租户信息,也会加剧实例不匹配的问题。
修复步骤
1. 调整PermissionAuthorizationPolicyProvider实现,通过请求域获取Scoped服务
在单例的PolicyProvider中,不要直接注入Scoped服务,而是通过IHttpContextAccessor获取当前请求的RequestServices,以此解析请求域内的服务实例。
public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IOptions<AuthorizationOptions> _options; public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IHttpContextAccessor httpContextAccessor) : base(options) { _options = options; _httpContextAccessor = httpContextAccessor; } public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { // 优先使用默认策略 var policy = await base.GetPolicyAsync(policyName); if (policy != null) return policy; // 从当前请求域获取Scoped服务 var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) return null; var tenantService = httpContext.RequestServices.GetRequiredService<ICurrentTenantService>(); var securityService = httpContext.RequestServices.GetRequiredService<ISecurityService>(); // 根据租户ID和策略名称获取权限要求 var requiredPermissions = await securityService.GetPermissionsByPolicy(policyName, tenantService.TenantId); if (!requiredPermissions.Any()) return null; // 构建自定义授权策略 policy = new AuthorizationPolicyBuilder() .AddRequirements(new PermissionRequirement(requiredPermissions)) .Build(); // 按租户维度缓存策略(避免跨租户共享) _options.Value.AddPolicy($"{tenantService.TenantId}_{policyName}", policy); return policy; } }
2. 确保服务注册顺序与生命周期正确
// 注册HttpContext访问器(必须) services.AddHttpContextAccessor(); // 单例注册自定义PolicyProvider services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>(); // Scoped注册授权Handler(直接注入请求域服务) services.AddScoped<IAuthorizationHandler, PermissionHandler>(); // 确保CurrentTenantService为Scoped生命周期 services.AddScoped<ICurrentTenantService, CurrentTenantService>(); services.AddScoped<ISecurityService, SecurityService>();
3. 优化PermissionHandler逻辑,直接使用请求域服务
由于Handler是Scoped生命周期,可直接注入ICurrentTenantService和ISecurityService,无需额外处理即可获取当前请求的实例:
public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { private readonly ICurrentTenantService _tenantService; private readonly ISecurityService _securityService; public PermissionHandler(ICurrentTenantService tenantService, ISecurityService securityService) { _tenantService = tenantService; _securityService = securityService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) { context.Fail(); return; } // 使用当前请求的租户实例校验权限 var hasPermission = await _securityService.VerifyUserPermissions(userId, _tenantService.TenantId, requirement.RequiredPermissions); if (hasPermission) { context.Succeed(requirement); } else { context.Fail(); } } }
4. 保证租户中间件执行顺序
确保租户初始化中间件在授权中间件之前执行,保证进入授权流程时CurrentTenantService已加载租户数据:
app.UseMiddleware<TenantMiddleware>(); // 先初始化租户 app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); // 再执行授权
额外注意事项
- 不要在策略中缓存跨租户的权限规则,需按
租户ID+策略名称作为缓存键,避免租户数据混淆。 - 若必须将Handler注册为单例,需在
HandleRequirementAsync中通过IServiceProvider创建请求域Scope来获取服务:using var scope = _serviceProvider.CreateScope(); var tenantService = scope.ServiceProvider.GetRequiredService<ICurrentTenantService>();
内容的提问来源于stack exchange,提问作者Lam Trường
相关产品推荐
相关产品推荐

