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

如何为带[Authorize]标签的ASP.NET WebAPI集成测试获取access_token?

问题:ASP.NET WebAPI集成测试无法访问带[Authorize]的接口(401 Unauthorized)

我有一个基于ASP.NET的WebAPI,配套IdentityServer4做授权、注册等功能。现在写集成测试时,访问带[Authorize]标签的API方法会返回Unauthorized:401异常。

示例代码

WebAPI接口方法

[ProducesResponseType(typeof(ChangePasswordResponse), 200)]
[ProducesResponseType(typeof(ChangePasswordResponse), 400)]
[Authorize]
[HttpPost("change-password")]
public async Task<ActionResult<ChangePasswordResponse>> ChangePassword([FromBody] ChangePasswordRequest request)
{
    _logger.LogInformation("***");
    var model = _mapper.Map<ChangePasswordModel>(request);
    var response = await _userAccountService.ChangePassword(model);
    if (response == null)
        return BadRequest(response);
    return Ok(response);
}

集成测试代码

[Fact]
public async Task ChangePassword_Returns200Response()
{
    // Arrange;
    var model = new ChangePasswordRequest
    {
        Email = StudentConsts.Email,
        CurrentPassword = StudentConsts.Password,
        NewPassword = StudentConsts.NewPassword
    };

    var request = _sutDataHelper.GenerateRequestFromModel(model);
    
    // Act
    var response = await _client.PostAsync("accounts/change-password", request);
    var responseContent = response.Content.ReadAsStringAsync().Result;
    var content = JsonSerializer.Deserialize<ChangePasswordResponse>(responseContent);

    // Asserts
    response.EnsureSuccessStatusCode();
    content.Email.Should().Be(model.Email);
    content.UserName.Should().Be(StudentConsts.UserName);
}

WebAPI授权配置代码

services
    .AddIdentity<User, IdentityRole<Guid>>(options =>
    {
        options.Password.RequiredLength = 8;
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = false;
        options.Password.RequireNonAlphanumeric = false;
    })
    .AddEntityFrameworkStores<AppDbContext>()
    .AddUserManager<UserManager<User>>()
    .AddDefaultTokenProviders();

services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
})
    .AddJwtBearer(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
    {
        options.RequireHttpsMetadata = "HERE IS IDENTITYSERVER4 SERVICE URL";
        options.Authority = "HERE IS IDENTITYSERVER4 SERVICE URL";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = false,
            ValidateIssuer = false,
            ValidateAudience = false,
            RequireExpirationTime = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
        options.Audience = "api";
    });

services.AddAuthorization(options =>
{
    options.AddPolicy(AppScopes.OpenId, policy => policy.RequireClaim("scope", AppScopes.OpenId));
    options.AddPolicy(AppScopes.Profile, policy => policy.RequireClaim("scope", AppScopes.Profile));
    options.AddPolicy(AppScopes.Email, policy => policy.RequireClaim("scope", AppScopes.Email));
    // 其他策略...
});

遇到的困境

  1. 集成测试基于IClassFixture<CustomWebApplicationFactory>创建,未添加WebAPI认证代码时返回401;添加后出现SchemeAlreadyExists: Identity.Application或SchemeAlreadyExists: Bearer错误。
  2. 尝试创建另一个AnotherWebApplicationFactory启动IdentityServer4以获取令牌,但IDE提示Program类在WebAPI和IS4项目中重复,无法同时启动两个项目。
  3. 手动硬编码假access_token不被API认可,仍返回401。

解决方案

1. 解决认证Scheme重复问题

在集成测试的CustomWebApplicationFactory中,覆盖ConfigureWebHost方法,移除重复的认证服务后重新配置测试专用的认证逻辑:

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // 移除已注册的认证Scheme提供者
            var authSchemeProvider = services.SingleOrDefault(d => d.ServiceType == typeof(IAuthenticationSchemeProvider));
            if (authSchemeProvider != null) services.Remove(authSchemeProvider);

            // 移除已注册的Identity核心服务
            var identityBuilder = services.SingleOrDefault(d => d.ServiceType == typeof(IIdentityBuilder));
            if (identityBuilder != null) services.Remove(identityBuilder);

            // 配置测试专用的JWT认证,跳过严格验证
            services.AddAuthentication("TestBearer")
                .AddJwtBearer("TestBearer", options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = false,
                        ValidateAudience = false,
                        ValidateLifetime = false,
                        ValidateIssuerSigningKey = false,
                        SignatureValidator = (token, parameters) => new JwtSecurityToken(token)
                    };
                });
        });
    }
}

如果需要完全模拟认证(无需令牌),可以自定义认证处理器直接通过验证:

// 自定义测试认证处理器
public class TestAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthenticationHandler(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.NameIdentifier, Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.Email, StudentConsts.Email),
            new Claim("scope", AppScopes.OpenId),
            new Claim("scope", AppScopes.Email)
        };

        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));
    }
}

// 在CustomWebApplicationFactory中配置该处理器
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Test";
    options.DefaultChallengeScheme = "Test";
})
.AddScheme<AuthenticationSchemeOptions, TestAuthenticationHandler>("Test", options => { });

2. 真实IS4令牌获取方案

如果必须用真实的IdentityServer4验证,不要在测试中同时启动两个项目,而是:

  • 提前启动IS4测试实例(比如用Docker容器或独立测试脚本)
  • 在CustomWebApplicationFactory中配置WebAPI指向该IS4实例
  • 在测试代码中请求IS4的/connect/token端点获取令牌,添加到请求头:
[Fact]
public async Task ChangePassword_Returns200Response()
{
    // 1. 从IS4获取令牌
    var tokenClient = new HttpClient();
    var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "http://your-is4-test-url/connect/token")
    {
        Content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            {"grant_type", "password"},
            {"client_id", "your-client-id"},
            {"client_secret", "your-client-secret"},
            {"username", StudentConsts.Email},
            {"password", StudentConsts.Password},
            {"scope", "api openid email"}
        })
    };

    var tokenResponse = await tokenClient.SendAsync(tokenRequest);
    var tokenContent = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();

    // 2. 构造API请求并添加令牌
    var model = new ChangePasswordRequest
    {
        Email = StudentConsts.Email,
        CurrentPassword = StudentConsts.Password,
        NewPassword = StudentConsts.NewPassword
    };

    var request = _sutDataHelper.GenerateRequestFromModel(model);
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenContent.AccessToken);
    
    // 3. 发送请求并断言
    var response = await _client.PostAsync("accounts/change-password", request);
    response.EnsureSuccessStatusCode();
    
    var content = await response.Content.ReadFromJsonAsync<ChangePasswordResponse>();
    content.Email.Should().Be(model.Email);
    content.UserName.Should().Be(StudentConsts.UserName);
}

// 令牌响应模型
public class TokenResponse
{
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; }
}

3. 快速跳过认证(仅用于业务逻辑测试)

如果只需验证业务逻辑,可在测试环境中动态跳过授权检查:
在CustomWebApplicationFactory的ConfigureWebHost中添加中间件:

builder.Configure(app =>
{
    app.Use(async (context, next) =>
    {
        // 清空授权要求,直接通过
        context.Features.Set<IAuthorizationFeature>(new AuthorizationFeature());
        await next();
    });
});

或者在WebAPI启动类中根据环境判断是否启用认证:

if (!app.Environment.IsEnvironment("Testing"))
{
    app.UseAuthentication();
    app.UseAuthorization();
}

内容的提问来源于stack exchange,提问作者JoZzzward

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 03:07:17