ASP.NET集成测试重写认证仍调用其他认证处理器的解决方案
问题场景
开发Web API时配置了两套独立认证方案,分别服务不同控制器、使用独立认证校验逻辑,Startup.cs中认证方案注册代码如下:
//startup.cs services.AddAuthentication(options => { options.DefaultScheme = MarsAuthConstants.Scheme; }) .AddScheme<MarsAuthSchemeOptions, MarsAuthHandler>(MarsAuthConstants.Scheme, options => { }) .AddScheme<MarsProjectAuthSchemeOptions, MarsProjectAuthHandler>(MarsAuthConstants.ProjectScheme, options => { });
两个认证方案名称分别为 "Mars" 和 "MarsProject"。
参照集成测试模拟认证方案的常规写法编写测试用例,代码如下:
[Fact] public async Task GetProject_Http_Test() { var client = Factory.WithWebHostBuilder(builder => { builder.ConfigureTestServices(services => { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Test"; options.DefaultChallengeScheme = "Test"; }).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>( "Test", options => { }); }); }) .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test"); var res = await client.GetAsync("/api/projects/" + ProjectId); res.StatusCode.Should().NotBe(HttpStatusCode.Unauthorized); res.StatusCode.Should().Be(HttpStatusCode.OK); }
测试运行失败,请求接口返回401 Unauthorized状态码。调试发现,即便测试环境已经配置了"Test"认证方案,程序依然会调用业务代码中定义的MarsAuthHandler执行认证逻辑。
由于两个控制器需要使用不同认证规则,必须在对应接口方法上标记[Authorize]特性并明确指定AuthenticationSchemes属性,示例代码如下:
[HttpGet] [Route("{id}")] [Authorize(AuthenticationSchemes = MarsAuthConstants.ProjectScheme)] public async Task<ProjectView> GetProjectAsync(long id) { return await _presentationService.GetProjectAsync(id); }
移除接口上的[Authorize]特性,测试即可正常通过,但全局无法配置唯一默认认证方案,该特性配置必须保留。核心诉求:使用Test认证方案开展集成测试时,绕过[Authorize]特性指定的原有认证逻辑,让测试用例正常通过。
解决方案
- 根本原因:
[Authorize]特性上硬编码指定AuthenticationSchemes参数时,认证中间件只会调用参数声明的方案对应的Handler,不会读取测试环境配置的默认Test方案,因此会触发原有业务认证逻辑返回401。 - 方案1(推荐):在测试的
ConfigureTestServices配置阶段,直接替换原有业务认证方案的Handler注册,将Mars和MarsProject两个业务方案都指向测试用的TestAuthHandler,无需新增名为Test的独立方案。
实现代码:
builder.ConfigureTestServices(services => { // 清理原有认证配置的后置初始化,避免旧配置干扰 services.RemoveAll<IPostConfigureOptions<AuthenticationOptions>>(); services.AddAuthentication(options => { // 默认方案和业务代码保持一致,避免特性校验时找不到匹配方案 options.DefaultScheme = MarsAuthConstants.Scheme; }) // 直接将两个业务认证方案的Handler替换为测试模拟Handler .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(MarsAuthConstants.Scheme, _ => {}) .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(MarsAuthConstants.ProjectScheme, _ => {}); });
该方案无需修改任何业务代码,请求到达时,[Authorize]特性指定的方案会直接走测试认证Handler,返回认证通过结果,不会触发原有业务认证逻辑。
- 方案2:如果不想替换原有方案注册,可以在测试环境添加全局授权过滤器,覆盖原有
[Authorize]特性的方案要求,强制所有接口接受Test方案的认证结果。
实现代码:
builder.ConfigureTestServices(services => { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Test"; options.DefaultChallengeScheme = "Test"; }).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { }); // 配置全局MVC选项,添加高优先级授权过滤器 services.Configure<MvcOptions>(options => { var testAuthPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes("Test") .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(testAuthPolicy)); }); });
该方案下全局授权过滤器优先级高于控制器/Action上标记的[Authorize]配置,Test方案认证通过后即可正常访问接口。
- 注意事项:测试用的
TestAuthHandler需要在HandleAuthenticateAsync方法中构造合法认证票据返回,不能返回NoResult或Fail,参考实现:
public 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() { // 可根据测试场景按需添加业务校验需要的Claims var claims = new[] { new Claim(ClaimTypes.Name, "TestUser"), new Claim(ClaimTypes.Role, "Admin") }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } }
内容的提问来源于stack exchange,提问作者DLeh
相关产品推荐
相关产品推荐

