You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:42:14