.NET 9集成测试中结合TimeProvider配置JWT认证——令牌验证因过期错误失败的解决方案
.NET 9集成测试中结合TimeProvider配置JWT认证——令牌验证因过期错误失败的解决方案
兄弟,看你贴的这段JWT认证配置代码,估计是在集成测试里踩了TimeProvider配合JWT验证的坑——明明模拟了令牌的有效期,结果还是触发了过期验证失败的错误对吧?我之前做.NET 9的集成测试时也碰到过一模一样的问题,给你整理下可行的解决步骤:
首先先看看你现有的配置(我帮你补全了部分缺失的代码结构):
public static void SetupAuthentication(this IServiceCollection services) { var authSettings = services.BuildServiceProvider().GetService<IOptionsSnapshot<AuthSettings>>()?.Value; // JWT Configuration services .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { // 你的其他验证参数... ValidateLifetime = true }; }); }
问题出在:默认情况下,JWT的TokenValidationParameters会用系统的默认时钟,不会自动使用你注册到DI容器里的TimeProvider。如果你在集成测试里用了FakeTimeProvider来模拟时间(比如测试令牌过期/有效场景),验证逻辑和你生成令牌时用的时间就对不上,自然就会判定令牌过期。
具体解决步骤:
- 第一步:把TimeProvider注入到JWT验证参数中
修改你的SetupAuthentication方法,确保在配置TokenValidationParameters时指定TimeProvider。这里推荐用AddJwtBearer的重载方法,直接从服务容器里获取依赖,避免提前调用BuildServiceProvider的潜在问题:
public static void SetupAuthentication(this IServiceCollection services) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) // 用带serviceProvider参数的重载 .AddJwtBearer((serviceProvider, options) => { var authSettings = serviceProvider.GetRequiredService<IOptionsSnapshot<AuthSettings>>().Value; // 从DI容器获取注册的TimeProvider var timeProvider = serviceProvider.GetRequiredService<TimeProvider>(); options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = authSettings.Issuer, ValidAudience = authSettings.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey)), // 关键:把TimeProvider绑定到验证参数 TimeProvider = timeProvider, // 测试场景下可以把ClockSkew设为0,减少时钟偏差的干扰 ClockSkew = TimeSpan.Zero }; }); }
- 第二步:在集成测试中注册FakeTimeProvider并控制时间
在你的测试类里,用.NET 8+自带的FakeTimeProvider来模拟时间,并且替换掉DI容器里默认的TimeProvider实现:
public class ProtectedEndpointTests : IClassFixture<WebApplicationFactory<Program>> { private readonly WebApplicationFactory<Program> _factory; private readonly FakeTimeProvider _fakeTimeProvider; public ProtectedEndpointTests(WebApplicationFactory<Program> factory) { _fakeTimeProvider = new FakeTimeProvider(); // 设置模拟的当前UTC时间,比如2024年1月1日 _fakeTimeProvider.SetUtcNow(new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)); _factory = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // 替换默认的TimeProvider为我们的模拟实例 services.AddSingleton<TimeProvider>(_fakeTimeProvider); }); }); } [Fact] public async Task When_ValidTokenIsUsed_Should_ReturnOk() { // 用同一个FakeTimeProvider生成令牌的有效期 var tokenHandler = new JwtSecurityTokenHandler(); var secretKey = Encoding.UTF8.GetBytes("your-test-secret-key"); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "test-user") }), // 令牌1小时后过期,用模拟的当前时间计算 Expires = _fakeTimeProvider.GetUtcNow().AddHours(1), NotBefore = _fakeTimeProvider.GetUtcNow(), Issuer = "test-issuer", Audience = "test-audience", SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); // 创建测试客户端并添加令牌 var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenString); // 调用受保护的接口 var response = await client.GetAsync("/api/protected"); response.EnsureSuccessStatusCode(); } }
额外的优化建议:
- 避免在配置方法中过早调用
BuildServiceProvider,像上面那样用AddJwtBearer的重载直接从服务容器获取依赖,更符合DI的最佳实践,也能避免多次构建服务容器带来的性能问题。 - 测试场景下建议把
ClockSkew设为TimeSpan.Zero,默认的时钟偏差会给测试结果带来不确定性,关闭后验证逻辑会严格按照令牌的有效期来判断。
备注:内容来源于stack exchange,提问作者hiddenhenry
相关产品推荐
相关产品推荐

