.NET Core无Http Context下,用装饰器统一处理处理器认证可行吗?
首先必须给这个思路点个赞——用装饰器模式把认证逻辑封装到命令/查询处理器上,完美契合关注点分离原则:业务处理器只需要聚焦核心业务,认证逻辑集中维护,不管调用方是Web API、Kafka消费者还是批处理任务,都能共享同一套安全规则,完全避免了重复造轮子的问题。这个方案不仅合理,还具备很强的扩展性和可维护性。
接下来咱们聊聊怎么实现不依赖HttpContext的通用认证逻辑:
核心思路:抽象认证上下文,隔离协议依赖
关键是把“认证所需的信息”和“从特定协议获取信息的逻辑”拆分开来。我们不需要让装饰器直接依赖HttpContext或者Kafka的消息对象,而是定义一个抽象的认证上下文和认证服务,让装饰器只依赖这个抽象层。
步骤1:定义通用的认证上下文
先创建一个AuthContext类,用来封装所有认证相关的核心信息,不管什么场景都能用:
public class AuthContext { public string UserId { get; set; } public IEnumerable<string> Roles { get; set; } = Enumerable.Empty<string>(); public bool IsAuthenticated { get; set; } // 可以按需添加其他字段,比如租户ID、令牌有效期等 }
步骤2:抽象认证服务接口
然后定义一个IAuthService接口,负责从当前执行环境中获取并生成AuthContext:
public interface IAuthService { AuthContext GetCurrentAuthContext(); }
这个接口的核心是不绑定任何特定协议——具体的实现交给不同场景的服务来做。
步骤3:为不同场景实现认证服务
1. HTTP场景的实现(依赖HttpContext,但只在这里依赖)
在Web API项目里,我们从HttpContext中提取用户信息:
public class HttpAuthService : IAuthService { private readonly IHttpContextAccessor _httpContextAccessor; public HttpAuthService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public AuthContext GetCurrentAuthContext() { var httpContext = _httpContextAccessor.HttpContext; if (httpContext?.User == null || !httpContext.User.Identity.IsAuthenticated) { return new AuthContext { IsAuthenticated = false }; } return new AuthContext { UserId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value, Roles = httpContext.User.Claims .Where(c => c.Type == ClaimTypes.Role) .Select(c => c.Value), IsAuthenticated = true }; } }
2. Kafka场景的实现(从消息头获取认证信息)
在Kafka消费者项目里,我们可以从消息的Headers中提取令牌,然后验证生成AuthContext:
// 先定义一个用来获取当前Kafka消息的上下文接口 public interface IKafkaMessageContext { ConsumeResult<string, string> CurrentMessage { get; } } // 实现这个上下文(具体取决于你用的Kafka客户端) public class KafkaMessageContext : IKafkaMessageContext { public ConsumeResult<string, string> CurrentMessage { get; set; } } // Kafka认证服务 public class KafkaAuthService : IAuthService { private readonly ITokenValidator _tokenValidator; // 抽象的令牌验证服务 private readonly IKafkaMessageContext _messageContext; public KafkaAuthService(ITokenValidator tokenValidator, IKafkaMessageContext messageContext) { _tokenValidator = tokenValidator; _messageContext = messageContext; } public AuthContext GetCurrentAuthContext() { var message = _messageContext.CurrentMessage; if (message?.Headers == null) { return new AuthContext { IsAuthenticated = false }; } // 从消息头读取Authorization令牌 if (message.Headers.TryGetValue("Authorization", out var tokenBytes)) { var token = Encoding.UTF8.GetString(tokenBytes).Replace("Bearer ", ""); var validationResult = _tokenValidator.ValidateToken(token); if (validationResult.IsValid) { return new AuthContext { UserId = validationResult.UserId, Roles = validationResult.Roles, IsAuthenticated = true }; } } return new AuthContext { IsAuthenticated = false }; } }
3. 批处理场景的实现
如果是批处理任务,你可以从配置文件、环境变量或者启动参数中获取认证信息:
public class BatchAuthService : IAuthService { private readonly IConfiguration _configuration; public BatchAuthService(IConfiguration configuration) { _configuration = configuration; } public AuthContext GetCurrentAuthContext() { // 从配置读取批处理任务的认证信息 var batchUserId = _configuration["Batch:UserId"]; var batchRoles = _configuration["Batch:Roles"]?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty<string>(); return new AuthContext { UserId = batchUserId, Roles = batchRoles, IsAuthenticated = !string.IsNullOrEmpty(batchUserId) }; } }
步骤4:改造认证装饰器,依赖抽象的IAuthService
现在把你的装饰器改成依赖IAuthService,完全脱离对HttpContext的依赖:
public class AuthDecorator<TCommand> : ICommandHandler<TCommand> where TCommand : ICommand { private readonly ICommandHandler<TCommand> _decoratee; private readonly IAuthService _authService; private readonly IEnumerable<string> _requiredRoles; public AuthDecorator(ICommandHandler<TCommand> decoratee, IAuthService authService) { _decoratee = decoratee; _authService = authService; // 从命令的特性中读取所需权限(可选,更灵活) var authorizeAttr = typeof(TCommand).GetCustomAttribute<AuthorizeAttribute>(); _requiredRoles = authorizeAttr?.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty<string>(); } public void Handle(TCommand command) { var authContext = _authService.GetCurrentAuthContext(); // 1. 验证是否已认证 if (!authContext.IsAuthenticated) { throw new UnauthorizedAccessException("操作未授权:用户未认证"); } // 2. 验证是否拥有所需角色(如果有要求) if (_requiredRoles.Any() && !_requiredRoles.Intersect(authContext.Roles).Any()) { throw new UnauthorizedAccessException($"操作未授权:用户缺少必需角色({string.Join(", ", _requiredRoles)})"); } // 认证通过,执行业务逻辑 _decoratee.Handle(command); } }
步骤5:用特性标记命令的权限要求(可选但推荐)
给命令添加自定义的权限特性,让装饰器能自动识别需要的权限:
[AttributeUsage(AttributeTargets.Class)] public class AuthorizeAttribute : Attribute { public string Roles { get; set; } } // 在命令上标记需要的角色 [Authorize(Roles = "Admin,UserManager")] public class CreateUserCommand : ICommand { public string Email { get; set; } public string Password { get; set; } }
步骤6:依赖注入配置
最后在不同项目中注册对应的认证服务和装饰器(推荐用Scrutor库来自动装饰所有命令处理器):
Web API项目:
services.AddHttpContextAccessor(); services.AddScoped<IAuthService, HttpAuthService>(); // 自动装饰所有ICommandHandler<T> services.Decorate(typeof(ICommandHandler<>), typeof(AuthDecorator<>));
Kafka消费者项目:
services.AddScoped<IKafkaMessageContext, KafkaMessageContext>(); services.AddScoped<ITokenValidator, JwtTokenValidator>(); // 实现你的令牌验证服务 services.AddScoped<IAuthService, KafkaAuthService>(); services.Decorate(typeof(ICommandHandler<>), typeof(AuthDecorator<>));
额外优化建议
- 添加匿名访问支持:可以定义一个
AllowAnonymousAttribute,在装饰器中检测到这个特性就跳过认证,适合公开接口(比如用户注册)。 - 统一异常处理:在不同场景中捕获
UnauthorizedAccessException,返回对应的结果(Web API返回401/403,Kafka消费者记录日志并标记消息失败)。 - 令牌验证抽象:把JWT或其他令牌的验证逻辑封装到
ITokenValidator接口中,让不同的认证服务都能复用。
这个方案完全能实现你想要的跨场景统一认证管控,而且业务代码完全不用感知底层的传输协议,完美符合开闭原则。
内容的提问来源于stack exchange,提问作者ozman

