如何从中间件将UserId传递至当前控制器?最佳实践探讨
问题描述
我想了解将UserId从中间件传递至当前执行控制器的最佳实践是什么?
例如,我通过Bearer令牌向如下控制器发送请求:[Route("")] [Authorize] [HttpPost] public async Task<IActionResult> ExampleTask(AnyObject anyObject) { await _anyModule.ExecuteCommand(anyObject); return Ok(); }请求首先会到达中间件,在那里我可以通过IHttpContextAccessor读取UserId。但我不想在控制器类中初始化IHttpContextAccessor,因为这并非良好且安全的实践。我想知道如何将UserId从中间件传递至当前执行的控制器?我是否应该创建一个实现IHttpContextAccessor的扩展类,将其添加到中间件管道中,再在控制器类中初始化它?这是个好主意吗?
编辑:
以下是我的实现方式:
在Startup类中,我向中间件添加了两项服务:services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IExecutionContextAccessor, ExecutionContextAccessor>();ExecutionContextAccessor会从IHttpContextAccessor中读取所需数据(如UserId):
public class ExecutionContextAccessor : IExecutionContextAccessor { private readonly IHttpContextAccessor _httpContextAccessor; public ExecutionContextAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Guid UserId { get { if (_httpContextAccessor .HttpContext? .User? .Claims? .SingleOrDefault(x => x.Type == "sub")? .Value != null) { return Guid.Parse(_httpContextAccessor.HttpContext.User.Claims.Single( x => x.Type == "sub").Value); } throw new ApplicationException("User context is not available"); } } }基于此,我创建了一个抽象控制器基类,用于初始化IExecutionContextAccessor并分配UserId。
最佳实践分析与优化建议
你的实现思路方向是对的,核心通过封装ExecutionContextAccessor隔离了对IHttpContextAccessor的直接依赖,符合依赖倒置原则,下面是具体的分析和优化点:
1. 现有实现的优势
- 避免控制器直接操作
IHttpContextAccessor,降低了耦合度 - 通过抽象基类统一处理用户上下文,减少重复代码
2. 可优化的细节
- 避免属性getter抛出异常:直接抛出异常会中断请求流程,建议返回
Guid?类型,由调用方决定如何处理空值;或者提前在中间件中校验用户身份,确保进入控制器前UserId一定存在 - 简化Claims读取逻辑:原代码中
SingleOrDefault后又调用Single,可以合并为一次查找,减少集合遍历次数 - 调整服务生命周期:将
IExecutionContextAccessor的注册改为Scoped更合理,虽然HttpContextAccessor内部用AsyncLocal保证线程安全,但Scoped语义更贴合请求生命周期,避免并发场景下的潜在风险
优化后的ExecutionContextAccessor示例:
public class ExecutionContextAccessor : IExecutionContextAccessor { private readonly IHttpContextAccessor _httpContextAccessor; public ExecutionContextAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Guid? UserId { get { var userIdClaim = _httpContextAccessor.HttpContext? .User? .Claims? .FirstOrDefault(x => x.Type == "sub"); if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId)) { return userId; } return null; } } }
注册时改为Scoped:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<IExecutionContextAccessor, ExecutionContextAccessor>();
3. 其他可行方案
自定义Action过滤器
在过滤器中读取UserId,存入HttpContext.Items或直接赋值给控制器属性,实现更细粒度的控制:
public class UserIdFilter : IActionFilter { private readonly IExecutionContextAccessor _executionContextAccessor; public UserIdFilter(IExecutionContextAccessor executionContextAccessor) { _executionContextAccessor = executionContextAccessor; } public void OnActionExecuting(ActionExecutingContext context) { if (_executionContextAccessor.UserId.HasValue) { // 存入HttpContext.Items,控制器可直接读取 context.HttpContext.Items["UserId"] = _executionContextAccessor.UserId.Value; // 若控制器继承自自定义基类,直接赋值 if (context.Controller is BaseController baseController) { baseController.UserId = _executionContextAccessor.UserId.Value; } } } public void OnActionExecuted(ActionExecutedContext context) { // 后续清理或日志逻辑 } }
在Startup中注册全局过滤器:
services.AddControllers(options => { options.Filters.Add<UserIdFilter>(); });
直接在控制器注入IExecutionContextAccessor
无需抽象基类,直接在需要的控制器构造函数中注入,灵活性更高:
public class ExampleController : ControllerBase { private readonly IAnyModule _anyModule; private readonly IExecutionContextAccessor _executionContextAccessor; public ExampleController(IAnyModule anyModule, IExecutionContextAccessor executionContextAccessor) { _anyModule = anyModule; _executionContextAccessor = executionContextAccessor; } [Route("")] [Authorize] [HttpPost] public async Task<IActionResult> ExampleTask(AnyObject anyObject) { var userId = _executionContextAccessor.UserId ?? throw new ApplicationException("用户上下文不存在"); await _anyModule.ExecuteCommand(anyObject, userId); return Ok(); } }
总结
你的现有实现已经是合理的方案,核心是通过封装的上下文访问器隔离底层依赖。建议优先优化异常处理和服务生命周期,再根据项目需求选择过滤器或直接注入的方式,让代码更健壮、贴合实际场景。
内容的提问来源于stack exchange,提问作者Guinea Pig Taco

