如何为带[Authorize]标签的ASP.NET WebAPI集成测试获取access_token?
我有一个基于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)); // 其他策略... });
遇到的困境
- 集成测试基于
IClassFixture<CustomWebApplicationFactory>创建,未添加WebAPI认证代码时返回401;添加后出现SchemeAlreadyExists: Identity.Application或SchemeAlreadyExists: Bearer错误。 - 尝试创建另一个
AnotherWebApplicationFactory启动IdentityServer4以获取令牌,但IDE提示Program类在WebAPI和IS4项目中重复,无法同时启动两个项目。 - 手动硬编码假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
相关产品推荐
相关产品推荐

