如何将HttpContext派生身份值注入多层架构业务类
当前系统采用多层架构设计,Web API接口/Action调用BLL业务逻辑层Provider时,需要同时叠加业务规则、安全权限两类过滤逻辑:比如超级用户可以查看全部数据条目,普通用户只能查看自己所属分组下的条目。
常规实现会把用户标识作为方法参数传入,类似public IEnumerable<Item> GetItems(int? userId, string color, string location),不传userId时返回全量数据,但这种实现很容易因为开发人员漏做权限校验出现越权漏洞,因此希望在BLL层内置一层安全逻辑,基于自定义MyIdentityProvider自动生成带权限过滤的查询,初始的业务Provider代码如下:
public class BllItemDataProvider : IBllItemDataProvider { private IMyIdentityProvider _myIdentity; // 注意贴出的原始代码重复写了两次相同构造函数,实际保留一个即可 public BllItemDataProvider(IMyIdentityProvider myIdentity) { _myIdentity = myIdentity; } }
MyIdentityProvider内部需要包含userId、isSuperUser以及其他业务需要的权限标识字段。
核心需求是通过依赖注入配置,自动从当前请求的HttpContext.User.Claims中读取声明填充MyIdentityProvider实例,最终让控制器不需要手动传递任何用户参数,直接调用Provider方法即可拿到经过权限过滤的数据,目标控制器代码如下:
public class ItemController : ControllerBase { private readonly IBllItemDataProvider _prov; public ItemController (IBllItemDataProvider prov) { _prov = prov; } [HttpGet("[action]/{color}/{location?}")] public IActionResult SomeItems (string color, string location) { var result = _prov.GetItems(color, location); return Ok(result); } }
之前考虑过实现一个ClaimControllerBase基类控制器,在构造函数里解析声明写入线程主体,供下游BLL组件访问,但不确定这个方案是否合理。补充说明:所有权限标识都来自JWT令牌解析出的Claims,由ASP.NET Core Identity框架自动填充到HttpContext.User中,基类写线程上下文的方案虽然能跑,但不够优雅,需要更符合框架规范的实现。
基类控制器写入线程上下文的方案不要用:一是权限逻辑耦合在控制器层,BLL层如果被后台任务、单元测试等非Web场景调用就完全失效;二是异步调用场景下线程上下文很容易出现值丢失、串请求的问题,排查问题成本极高。
按照ASP.NET Core的DI规范按以下步骤实现即可,不需要额外做基类封装:
- 第一步:注册
IHttpContextAccessor
ASP.NET Core默认不会把HttpContext访问器注入到容器中,首先在Program.cs的服务配置段添加注册:builder.Services.AddHttpContextAccessor(); - 第二步:实现
IMyIdentityProvider,直接依赖IHttpContextAccessor读取当前请求的Claims
这个类不需要做任何静态/线程全局存储,注册为Scoped生命周期,每个请求会独立生成实例,完全不会出现串用户的问题:public interface IMyIdentityProvider { int? UserId { get; } bool IsSuperUser { get; } // 其他需要的权限字段按需补充 } public class MyIdentityProvider : IMyIdentityProvider { private readonly IHttpContextAccessor _contextAccessor; private ClaimsPrincipal? CurrentUser => _contextAccessor.HttpContext?.User; public MyIdentityProvider(IHttpContextAccessor contextAccessor) { _contextAccessor = contextAccessor; } public int? UserId { get { var uidClaim = CurrentUser?.FindFirst(ClaimTypes.NameIdentifier)?.Value; return int.TryParse(uidClaim, out var uid) ? uid : null; } } public bool IsSuperUser { get { var superClaim = CurrentUser?.FindFirst("isSuperUser")?.Value; return bool.TryParse(superClaim, out var isSuper) && isSuper; } } } - 第三步:按Scoped生命周期注册所有相关服务
服务生命周期和请求生命周期对齐即可,不要注册为单例,否则会出现捕获的HttpContext是旧请求的问题:builder.Services.AddScoped<IMyIdentityProvider, MyIdentityProvider>(); builder.Services.AddScoped<IBllItemDataProvider, BllItemDataProvider>(); - 第四步:在BLL层的查询方法里直接叠加权限过滤逻辑,不需要控制器传任何用户参数
示例过滤逻辑如下,权限判断完全收敛在BLL层,控制器无感知:public IEnumerable<Item> GetItems(string color, string location) { var query = _dbContext.Items.AsQueryable(); // 非超管自动追加所属分组过滤,从根源避免越权 if (!_myIdentity.IsSuperUser) { var uid = _myIdentity.UserId ?? throw new UnauthorizedAccessException("用户身份校验失败"); var userGroupIds = _dbContext.UserGroups .Where(ug => ug.UserId == uid) .Select(ug => ug.GroupId); query = query.Where(item => userGroupIds.Contains(item.GroupId)); } // 叠加业务参数过滤 if (!string.IsNullOrWhiteSpace(color)) query = query.Where(item => item.Color == color); if (!string.IsNullOrWhiteSpace(location)) query = query.Where(item => item.Location == location); return query.ToList(); }
这个实现的优势很明显:
- 权限逻辑完全下沉到BLL层,不会因为控制器漏传参数、漏做校验出现越权漏洞
- 所有组件生命周期和请求绑定,没有异步上下文错乱、串用户的风险
- 单元测试时可以直接Mock
IMyIdentityProvider模拟不同权限的用户场景,不需要构造HttpContext测试环境 - 后续如果身份来源变更(比如从JWT换成ApiKey认证、内部服务调用传递用户标识),只需要修改
MyIdentityProvider的实现,上层控制器和业务逻辑完全不需要改动
内容的提问来源于stack exchange,提问作者T.S.

