如何为ASP.NET Core Web API测试添加模拟ClaimsIdentity
我正在为API编写测试用例,需要验证角色不合法的用户请求时是否会返回403 Forbidden状态码。
最初我通过替换IPolicyEvaluator的默认实现来模拟ClaimsIdentity,代码如下:
public class FakePolicyEvaluator : IPolicyEvaluator { public ClaimsIdentity? ClaimsIdentity { get; set; } public Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) { var claimsPrincipal = new ClaimsPrincipal(); if (ClaimsIdentity != null) { claimsPrincipal.AddIdentity(ClaimsIdentity); } var ticket = new AuthenticationTicket(claimsPrincipal, "test"); var result = AuthenticateResult.Success(ticket); return Task.FromResult(result); } public Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource) { var result = PolicyAuthorizationResult.Success(); return Task.FromResult(result); } }
我的控制器方法标记了[Authorize(Roles="SomeAllowedRole")]特性,但这个方案不生效,推测是因为AuthorizeAsync直接返回了PolicyAuthorizationResult.Success(),跳过了角色校验。
后来我考虑在FakePolicyEvaluator中注入原始的IPolicyEvaluator实现,让真实的授权逻辑生效,代码如下:
public class FakePolicyEvaluator : IPolicyEvaluator { private readonly IPolicyEvaluator _realPolicyEvaluator; public FakePolicyEvaluator(IPolicyEvaluator realPolicyEvaluator) { _realPolicyEvaluator = realPolicyEvaluator; } public ClaimsIdentity? ClaimsIdentity { get; set; } public Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) { var claimsPrincipal = new ClaimsPrincipal(); if (ClaimsIdentity != null) { claimsPrincipal.AddIdentity(ClaimsIdentity); } context.User = claimsPrincipal; return _realPolicyEvaluator.AuthenticateAsync(policy, context); } public Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource) { return _realPolicyEvaluator.AuthorizeAsync(policy, authenticationResult, context, resource); } }
但我不知道如何在服务集合中注册这个类,或者说如何获取原始的PolicyEvaluator实例并注入到Fake类中,求解决办法。
1. 修正FakePolicyEvaluator的依赖逻辑
保持当前代码中依赖IPolicyEvaluator接口的写法,不要直接依赖具体的PolicyEvaluator类,这样更符合依赖注入原则,也方便后续注册。
2. 用装饰器模式注册FakePolicyEvaluator
要实现"替换原始实现但保留其逻辑",可以通过装饰器模式完成注册:先保留原始的IPolicyEvaluator服务,再将FakePolicyEvaluator注册为接口的实现,并注入原始实例。
在WebApplicationFactory中注册(集成测试场景)
如果使用WebApplicationFactory做集成测试,重写ConfigureWebHost方法完成注册:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class { public FakePolicyEvaluator? FakePolicyEvaluator { get; private set; } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // 找到并移除原始的IPolicyEvaluator注册 var originalRegistration = services.SingleOrDefault(d => d.ServiceType == typeof(IPolicyEvaluator)); if (originalRegistration != null) { services.Remove(originalRegistration); // 创建原始IPolicyEvaluator实例 var realEvaluator = (IPolicyEvaluator)ActivatorUtilities.CreateInstance(ServiceProvider, originalRegistration.ImplementationType!); // 实例化Fake类并注入原始实现 FakePolicyEvaluator = new FakePolicyEvaluator(realEvaluator); // 注册Fake类作为IPolicyEvaluator的实现 services.AddSingleton<IPolicyEvaluator>(FakePolicyEvaluator); } }); } }
用Autofac等容器直接注册(项目使用第三方DI容器场景)
如果项目使用Autofac,可直接通过装饰器语法注册:
builder.RegisterType<PolicyEvaluator>().As<IPolicyEvaluator>(); builder.RegisterDecorator<FakePolicyEvaluator, IPolicyEvaluator>();
3. 测试时设置模拟角色
在测试方法中,获取FakePolicyEvaluator实例并设置无权限的角色:
using var factory = new CustomWebApplicationFactory<Startup>(); var client = factory.CreateClient(); // 设置无权限的用户角色 factory.FakePolicyEvaluator!.ClaimsIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "UnauthorizedRole") }); // 发起请求 var response = await client.GetAsync("/api/your-protected-endpoint"); // 验证返回状态码 Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
4. 更简单的替代方案:直接设置HttpContext.User
其实在集成测试中,不需要替换IPolicyEvaluator,可以直接通过自定义中间件设置HttpContext.User:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class { public ClaimsPrincipal? TestUser { get; set; } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // 添加测试用的认证方案 services.AddAuthentication("TestAuth") .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestAuth", _ => { }); }); // 添加中间件设置测试用户 builder.UseMiddleware<TestUserMiddleware>(); } // 中间件:将TestUser赋值给HttpContext.User private class TestUserMiddleware { private readonly RequestDelegate _next; private readonly CustomWebApplicationFactory<TStartup> _factory; public TestUserMiddleware(RequestDelegate next, CustomWebApplicationFactory<TStartup> factory) { _next = next; _factory = factory; } public async Task Invoke(HttpContext context) { if (_factory.TestUser != null) { context.User = _factory.TestUser; context.Items["IsTestAuth"] = true; } await _next(context); } } // 测试用的认证处理器 private class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { if (Context.Items.TryGetValue("IsTestAuth", out var value) && value is true) { var ticket = new AuthenticationTicket(Context.User, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } return Task.FromResult(AuthenticateResult.Fail("未设置测试用户")); } } }
测试时直接设置TestUser即可:
using var factory = new CustomWebApplicationFactory<Startup>(); // 设置无权限用户 factory.TestUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "UnauthorizedRole") }, "TestAuth")); var client = factory.CreateClient(); var response = await client.GetAsync("/api/your-protected-endpoint"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
内容的提问来源于stack exchange,提问作者pawelfelcyn

