.NET 7 API用Specflow模拟JWT认证时始终返回Unauthorized的问题
问题描述
我有一个.NET 7 API,尝试使用Specflow/xUnit编写测试用例覆盖接口,但无法模拟API的认证逻辑,访问带[Authorize]特性的端点时始终返回Unauthorized状态码。
API核心配置(Program.cs)
public partial class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Configuration OidcConfigurationSection oidcConfig = new OidcConfigurationSection(); builder.Configuration.GetSection(OidcConfigurationSection.SectionName).Bind(oidcConfig); // Authentication builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { //options.Authority = oidcConfig.Authority; //options.Audience = oidcConfig.Identifier; // Received token should have a user identified, the audience it asked for initially and the issuer should be the one that we asked for. options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = ClaimTypes.NameIdentifier, ValidAudience = oidcConfig.Identifier, ValidIssuer = oidcConfig.Authority }; }); ... var app = builder.Build(); ... app.Run(); } }
受保护端点示例
[ApiController] [Route("user/settings")] [Authorize] public class UserSettingsController : ControllerBase { ... [HttpGet] public async Task<IActionResult> Get() { return Ok(_dataRepo.Get()) } }
测试项目配置
FakeJwtManager类(生成模拟JWT)
public static class FakeJwtManager { public static string Issuer { get; } = Guid.NewGuid().ToString(); public static string Audience { get; } = Guid.NewGuid().ToString(); public static SecurityKey SecurityKey { get; } public static SigningCredentials SigningCredentials { get; } private static readonly JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); private static readonly RandomNumberGenerator generator = RandomNumberGenerator.Create(); private static readonly byte[] key = new byte[32]; static FakeJwtManager() { generator.GetBytes(key); SecurityKey = new SymmetricSecurityKey(key) { KeyId = Guid.NewGuid().ToString() }; SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); } public static string GenerateJwtToken() { return tokenHandler.WriteToken(new JwtSecurityToken(Issuer, Audience, null, null, DateTime.UtcNow.AddMinutes(10), SigningCredentials)); } }
FakeJwtWebApplicationFactory类(重写认证配置)
public class FakeJwtWebApplicationFactory<TStartup> : WebApplicationFactory<Program> { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => { //options.Authority = FakeJwtManager.Issuer; //options.Audience = FakeJwtManager.Audience; options.TokenValidationParameters = new TokenValidationParameters() { IssuerSigningKey = FakeJwtManager.SecurityKey, ValidIssuer = FakeJwtManager.Issuer, ValidAudience = FakeJwtManager.Audience }; }); }); } }
Specflow步骤定义
[Binding] public class UserCreationStepDefinitions { private readonly FakeJwtWebApplicationFactory<Program> _webApplicationFactory; public HttpClient _httpClient { get; set; } = null!; private HttpResponseMessage _response { get; set; } = null!; public UserCreationStepDefinitions(FakeJwtWebApplicationFactory<Program> webApplicationFactory) { _webApplicationFactory = webApplicationFactory; } [Given(@"I am a client")] public void GivenIAmAClient() { _httpClient = _webApplicationFactory.CreateDefaultClient(); } [Given(@"I provide valid authentified oidc user")] public void GivenIProvideValidAuthentifiedOidcUser() { var accessToken = FakeJwtManager.GenerateJwtToken(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } [When(@"I make a GET request to '([^']*)'")] public async Task WhenIMakeAGETRequestTo(string endpoint) { _response = await _httpClient.GetAsync($"{endpoint}"); } [Then(@"The response status code is '([^']*)'")] public void ThenTheResponseStatusCodeIs(int statusCode) { var expected = (HttpStatusCode)statusCode; Assert.Equal(expected, _response.StatusCode); <-- **Always false, expected is 'Unauthorized' instead of 'Ok' } }
问题分析与解决方案
核心问题点
测试用TokenValidationParameters缺少NameClaimType配置
原API的认证逻辑明确指定了NameClaimType = ClaimTypes.NameIdentifier,但测试项目的FakeJwtWebApplicationFactory中未同步该配置,导致JWT签名验证通过后,无法正确识别用户标识,最终触发Unauthorized。生成的JWT令牌缺少用户标识Claim
FakeJwtManager.GenerateJwtToken()方法生成的令牌未包含ClaimTypes.NameIdentifier类型的Claim,而原API要求以此作为用户唯一标识,因此即使签名验证通过,也会因缺少必要的用户信息而认证失败。
修复代码
1. 修复FakeJwtWebApplicationFactory
添加与原API一致的NameClaimType配置:
public class FakeJwtWebApplicationFactory<TStartup> : WebApplicationFactory<Program> { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters = new TokenValidationParameters() { IssuerSigningKey = FakeJwtManager.SecurityKey, ValidIssuer = FakeJwtManager.Issuer, ValidAudience = FakeJwtManager.Audience, // 新增:与原API保持一致的用户标识Claim类型 NameClaimType = ClaimTypes.NameIdentifier }; }); }); } }
2. 修复FakeJwtManager
生成令牌时添加NameIdentifier类型的Claim:
public static class FakeJwtManager { // 现有代码保持不变... public static string GenerateJwtToken() { // 添加用户标识Claim var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()) }; return tokenHandler.WriteToken(new JwtSecurityToken( Issuer, Audience, claims, // 传入包含用户标识的Claim集合 null, DateTime.UtcNow.AddMinutes(10), SigningCredentials)); } }
可选:彻底替换认证配置(若PostConfigure不生效)
如果上述修复后仍存在问题,可以直接移除原认证服务并重新配置:
public class FakeJwtWebApplicationFactory<TStartup> : WebApplicationFactory<Program> { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { // 移除原有的认证服务注册 var authServiceDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IAuthenticationService)); if (authServiceDescriptor != null) { services.Remove(authServiceDescriptor); } // 重新添加JWT认证,使用测试专用配置 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters() { IssuerSigningKey = FakeJwtManager.SecurityKey, ValidIssuer = FakeJwtManager.Issuer, ValidAudience = FakeJwtManager.Audience, NameClaimType = ClaimTypes.NameIdentifier }; }); }); } }
内容的提问来源于stack exchange,提问作者Nicolas G.
相关产品推荐
相关产品推荐

