.NET Web API集成测试中JWT获取与使用异常排查
在基于JWT认证授权的.NET Web API集成测试中,调用/start-registration接口获取token后,携带该token请求/complete-registration时返回401未授权;但在测试项目中直接复制API的token生成代码生成的token,能正常通过授权。Postman单独测试各端点时认证授权机制符合预期,测试通过WebApplicationFactory在内存中运行API。
相关代码
生成token的控制器方法
// POST /start-registration [HttpPost("start-registration")] [AllowAnonymous] public async Task<ActionResult<string>> StartRegistrationAsync([FromBody] RegistrationDto dto) { try { if (validator.IsValid(dto)) { var acc = await GetUnregisteredAccountAsync(dto); if(acc is not null) { if (acc.IsAuthenticated<RegistrationDto>(dto)) { if (acc.IsAuthorized()) { await acc.RecordAuthnActivityAsync("Success"); return CreateRegistrationToken(config); } else { await acc.RecordAuthnActivityAsync("Failure"); return Unauthorized(UnauthorizedMessage()); } } else { await acc.RecordAuthnActivityAsync("Failure"); return NotFound(NotFoundMessage()); } } else { return NotFound(NotFoundMessage()); } } else { return BadRequest(BadRequestMessage()); } } catch (MySqlException ex) { exHandler.HandleException(ex, logger); return StatusCode(500); } }
需要token验证的控制器方法
// POST /complete-registration [HttpPost("complete-registration")] [Authorize(Roles = REGISTRATION_ONLY)] public async Task<ActionResult> CompleteRegistrationAsync([FromBody] RegistrationDto dto) { try { if (validator.IsValid(dto)) { await RegisterAccountAsync(dto); return Ok(OkMessage()); } else { return BadRequest(BadRequestMessage()); } } catch (MySqlException ex) { exHandler.HandleException(ex, logger); // Error code 1062 is thrown when a duplicate entry is attempted in a column // with a UNIQUE constraint. // Will occur if the desired username already exists. if(ex.Number == 1062) { return Conflict("Username already exists."); } else { return StatusCode(500); } } }
API的token生成方法
public static string CreateRegistrationToken(IConfiguration config) { var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.Role, "Registration Only") }; var token = new JwtSecurityToken(config["Jwt:Issuer"], config["Jwt:Audience"], claims, expires: DateTime.Now.AddHours(1), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); }
测试方法(后续将重构)
[Fact] public async Task RegisterAccount_WithRegistrationSuccess_ReturnsOk() { // Arrange NewAccountDto newAccdto = new() { FirstName = "Jane", LastName = "Doe", PhoneNumber = "1234567890", Location = "Some City", Position = "Crew", Shift = "Day", Status = "Active" }; var response = await client.PostAsJsonAsync("/account/add-new", newAccdto); var registrationCode = await response.Content.ReadFromJsonAsync<int>(); RegistrationDto regDto = new() { RegistrationStage = RegStage.START, FirstName = "Jane", LastName = "Doe", RegistrationCode = registrationCode }; response = await client.PostAsJsonAsync("/account/start-registration", regDto); var token = await response.Content.ReadAsStringAsync(); regDto = new() { RegistrationStage = RegStage.COMPLETE, FirstName = "Jane", LastName = "Doe", NewUsername = "NewUsername", NewPassword = "NewPassword" }; // Act client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); response = await client.PostAsJsonAsync("/account/complete-registration", regDto); // Assert response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK); }
Program.cs中的认证授权配置
// Add JWT Authentication. builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) }; }); // Add default authorization policy builder.Services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireClaim(ClaimTypes.Role, new string[] { "Crew", "Manager", "General Manager", "Administrator", "Recovery Only", "Registration Only"}) .Build());
已尝试的方案
- 修改
StartRegistrationAsync返回类型为封装token的DTO,测试中通过ReadFromJsonAsync<DtoType>()获取token; - 在Program.cs中显式配置
DefaultAuthenticationScheme和DefaultChallengeScheme; - 删除原有额外授权策略,将角色添加到默认授权策略;
- 在测试项目中复制
CreateRegistrationToken()方法直接生成token,此方法下测试通过。
排查方向与解决建议
检查token获取的格式问题
用ReadAsStringAsync()获取token时,可能返回的内容带有额外引号(比如API返回JSON字符串而非纯token)。可以在测试中打印token值,若包含"或多余字符,用token = token.Trim('"')处理。验证测试环境配置一致性
确认集成测试项目的配置文件(如appsettings.Test.json)中Jwt:Issuer、Jwt:Audience、Jwt:Key与API运行时的配置完全一致,避免因配置差异导致token验证失败。解析token对比声明内容
在测试中获取token后,解析查看其声明是否符合预期,对比直接生成的token:var handler = new JwtSecurityTokenHandler(); var jwtToken = handler.ReadJwtToken(token); // 打印Role声明、Issuer、Audience等信息,确认与直接生成的token一致检查请求头格式
确保Authorization头格式为Bearer+ token,无多余空格。打印client.DefaultRequestHeaders.Authorization的值确认格式正确。确认API响应的Content-Type
若/start-registration响应的Content-Type为application/json,直接用ReadAsStringAsync()会拿到带引号的字符串,此时应改用ReadFromJsonAsync<string>()获取纯token值。
内容的提问来源于stack exchange,提问作者user21058375

