.NET Core Web API中使用JWT实现安全认证的实战指南

📅 2026/7/3 8:48:26
.NET Core Web API中使用JWT实现安全认证的实战指南
1. 项目概述在当今前后端分离的Web应用开发中身份认证是一个绕不开的话题。作为一名长期奋战在一线的.NET开发者我亲历了从传统的Session认证到现代Token认证的转变过程。JWT(JSON Web Token)因其无状态、跨域友好和易于扩展的特性已成为微服务架构和单页应用(SPA)的首选认证方案。最近我在重构一个电商平台的认证模块时就采用了.NET Core Web API JWT的方案。相比传统的Cookie-Session模式JWT让我们的移动端和Web端可以共享同一套认证逻辑后端服务也更容易横向扩展。下面我就把这次实战中的关键点和踩过的坑分享给大家。2. JWT核心原理与结构解析2.1 JWT的三大组成部分JWT本质上是一个经过签名的JSON对象由三部分组成用点号(.)连接Header.Payload.SignatureHeader部分通常包含两个信息typ令牌类型固定为JWTalg签名算法如HS256( HMAC SHA-256)或RS256(RSA SHA-256)示例{ alg: HS256, typ: JWT }Payload部分包含所谓的声明(Claims)即关于实体(通常是用户)的声明语句。声明分三类注册声明(预定义)如iss(签发者)、exp(过期时间)、sub(主题)等公共声明可以自定义但建议使用IANA注册的名称私有声明供双方约定使用的自定义声明Signature部分是对前两部分的签名防止数据篡改。以HS256为例签名算法如下HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret)2.2 JWT的工作流程用户使用凭证(如用户名密码)登录服务端验证凭证生成JWT返回给客户端客户端存储JWT(通常放在localStorage或Cookie中)客户端在后续请求的Authorization头中携带JWT服务端验证JWT有效性并处理请求注意JWT一旦签发在有效期内无法主动废止这是与Session机制的重要区别。因此建议设置合理的过期时间并对敏感操作增加二次验证。3. 环境准备与项目搭建3.1 创建.NET Core Web API项目推荐使用.NET CLI创建项目保证环境一致性dotnet new webapi -n JwtAuthDemo cd JwtAuthDemo3.2 安装必要的NuGet包除了基础的JWT包我建议添加以下扩展包dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer dotnet add package System.IdentityModel.Tokens.Jwt dotnet add package Swashbuckle.AspNetCore # 用于API文档和测试3.3 配置appsettings.json{ Jwt: { SecretKey: this-is-a-very-strong-secret-key-with-32-chars, Issuer: https://api.yourdomain.com, Audience: https://app.yourdomain.com, ExpirationMinutes: 60, RefreshTokenExpirationDays: 7 } }安全提示生产环境务必通过机密管理器或环境变量存储密钥切勿直接硬编码在配置文件中。4. JWT认证服务配置4.1 Program.cs完整配置var builder WebApplication.CreateBuilder(args); // 添加控制器 builder.Services.AddControllers(); // 配置JWT认证 builder.Services.AddAuthentication(options { options.DefaultAuthenticateScheme JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], ValidateLifetime true, ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:SecretKey])), ClockSkew TimeSpan.Zero // 严格校验过期时间 }; // 自定义事件处理 options.Events new JwtBearerEvents { OnAuthenticationFailed context { if (context.Exception.GetType() typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add(Token-Expired, true); } return Task.CompletedTask; } }; }); // 配置授权策略 builder.Services.AddAuthorization(options { options.AddPolicy(AdminOnly, policy policy.RequireRole(Admin)); options.AddPolicy(MemberOnly, policy policy.RequireRole(Member, Admin)); }); // 配置Swagger builder.Services.AddSwaggerGen(c { c.SwaggerDoc(v1, new OpenApiInfo { Title JWT Auth Demo, Version v1 }); // 添加JWT认证支持 var securityScheme new OpenApiSecurityScheme { Name JWT Authentication, Description Enter JWT Bearer token, In ParameterLocation.Header, Type SecuritySchemeType.Http, Scheme bearer, BearerFormat JWT, Reference new OpenApiReference { Id JwtBearerDefaults.AuthenticationScheme, Type ReferenceType.SecurityScheme } }; c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); c.AddSecurityRequirement(new OpenApiSecurityRequirement { {securityScheme, Array.Emptystring()} }); }); var app builder.Build(); // 开发环境使用Swagger if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();4.2 关键配置解析TokenValidationParameters控制JWT验证的核心参数ValidateIssuer/Audience是否验证签发者和受众ClockSkew允许的时钟偏差设为Zero表示严格校验LifetimeValidator可自定义生命周期验证逻辑JwtBearerEvents提供认证过程中的事件钩子OnAuthenticationFailed认证失败时触发OnTokenValidatedToken验证成功后触发授权策略可以定义多种策略组合角色和声明要求5. JWT工具类实现5.1 增强版JwtTokenHelperpublic class JwtTokenHelper { private readonly IConfiguration _configuration; private readonly SymmetricSecurityKey _signingKey; public JwtTokenHelper(IConfiguration configuration) { _configuration configuration; var secretKey _configuration[Jwt:SecretKey]; _signingKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); } public string GenerateAccessToken(IEnumerableClaim claims) { var tokenOptions new JwtSecurityToken( issuer: _configuration[Jwt:Issuer], audience: _configuration[Jwt:Audience], claims: claims, expires: DateTime.UtcNow.AddMinutes( Convert.ToDouble(_configuration[Jwt:ExpirationMinutes])), signingCredentials: new SigningCredentials( _signingKey, SecurityAlgorithms.HmacSha256) ); return new JwtSecurityTokenHandler().WriteToken(tokenOptions); } public string GenerateRefreshToken() { var randomNumber new byte[32]; using var rng RandomNumberGenerator.Create(); rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } public ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var tokenValidationParameters new TokenValidationParameters { ValidateAudience false, ValidateIssuer false, ValidateIssuerSigningKey true, IssuerSigningKey _signingKey, ValidateLifetime false // 这里不验证过期时间 }; var tokenHandler new JwtSecurityTokenHandler(); var principal tokenHandler.ValidateToken( token, tokenValidationParameters, out var securityToken); if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals( SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) throw new SecurityTokenException(Invalid token); return principal; } }5.2 关键功能说明双Token机制AccessToken短期有效用于API访问RefreshToken长期有效用于获取新的AccessToken安全增强使用.NET内置的RandomNumberGenerator生成RefreshToken严格校验签名算法防止算法替换攻击过期Token解析专门方法用于解析过期Token(用于刷新Token场景)仍然验证签名有效性只是忽略过期时间6. 认证接口实现6.1 用户模型与DTOpublic class User { public int Id { get; set; } public string Username { get; set; } public string PasswordHash { get; set; } public string Role { get; set; } public string RefreshToken { get; set; } public DateTime? RefreshTokenExpiryTime { get; set; } } public class LoginDto { [Required] public string Username { get; set; } [Required] public string Password { get; set; } } public class TokenDto { public string AccessToken { get; set; } public string RefreshToken { get; set; } public DateTime Expiration { get; set; } }6.2 AuthController实现[ApiController] [Route(api/[controller])] public class AuthController : ControllerBase { private readonly JwtTokenHelper _tokenHelper; private readonly IUserService _userService; public AuthController(JwtTokenHelper tokenHelper, IUserService userService) { _tokenHelper tokenHelper; _userService userService; } [HttpPost(login)] public async TaskIActionResult Login([FromBody] LoginDto loginDto) { var user await _userService.Authenticate(loginDto.Username, loginDto.Password); if (user null) return Unauthorized(Invalid username or password); var claims new[] { new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Role, user.Role), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var accessToken _tokenHelper.GenerateAccessToken(claims); var refreshToken _tokenHelper.GenerateRefreshToken(); // 保存RefreshToken到数据库 await _userService.UpdateRefreshToken(user.Id, refreshToken); return Ok(new TokenDto { AccessToken accessToken, RefreshToken refreshToken, Expiration DateTime.UtcNow.AddMinutes( Convert.ToDouble(_configuration[Jwt:ExpirationMinutes])) }); } [HttpPost(refresh)] public async TaskIActionResult Refresh([FromBody] TokenDto tokenDto) { var principal _tokenHelper.GetPrincipalFromExpiredToken(tokenDto.AccessToken); var username principal.Identity.Name; var user await _userService.GetByUsername(username); if (user null || user.RefreshToken ! tokenDto.RefreshToken || user.RefreshTokenExpiryTime DateTime.UtcNow) return BadRequest(Invalid refresh token); var newAccessToken _tokenHelper.GenerateAccessToken(principal.Claims); var newRefreshToken _tokenHelper.GenerateRefreshToken(); await _userService.UpdateRefreshToken(user.Id, newRefreshToken); return Ok(new TokenDto { AccessToken newAccessToken, RefreshToken newRefreshToken, Expiration DateTime.UtcNow.AddMinutes( Convert.ToDouble(_configuration[Jwt:ExpirationMinutes])) }); } [HttpPost(revoke)] [Authorize] public async TaskIActionResult Revoke() { var username User.Identity.Name; await _userService.RevokeRefreshToken(username); return NoContent(); } }6.3 关键业务逻辑登录流程验证用户凭证生成包含必要声明的JWT生成并存储RefreshToken返回双Token给客户端Token刷新流程使用过期但未篡改的AccessToken验证对应的RefreshToken是否有效签发新的双Token使旧RefreshToken失效安全增强每次刷新都生成新的RefreshToken提供主动撤销接口记录JTI(JWT ID)防止重放攻击7. 保护API资源7.1 控制器级保护[ApiController] [Route(api/[controller])] [Authorize] // 整个控制器需要认证 public class ProductsController : ControllerBase { [HttpGet] [AllowAnonymous] // 允许匿名访问 public IActionResult GetAll() { ... } [HttpGet({id})] public IActionResult GetById(int id) { ... } [HttpPost] [Authorize(Roles Admin)] // 需要Admin角色 public IActionResult Create([FromBody] ProductDto dto) { ... } }7.2 策略式授权// 在Program.cs中定义策略 builder.Services.AddAuthorization(options { options.AddPolicy(RequireAdminRole, policy policy.RequireRole(Admin)); options.AddPolicy(MinimumAge, policy policy.RequireClaim(DateOfBirth, birthDate DateTime.Parse(birthDate).AddYears(18) DateTime.Today)); }); // 在控制器中使用 [HttpGet(premium)] [Authorize(Policy MinimumAge)] public IActionResult PremiumContent() { ... }7.3 自定义授权属性public class PermissionAttribute : AuthorizeAttribute { public PermissionAttribute(string permission) : base(policy: permission) { } } // 注册策略 builder.Services.AddAuthorization(options { options.AddPolicy(Products.Read, policy policy.RequireAssertion(context context.User.HasClaim(c c.Type permissions c.Value.Contains(Products.Read)))); }); // 使用自定义属性 [Permission(Products.Read)] public IActionResult GetProduct(int id) { ... }8. 测试与调试技巧8.1 使用Swagger测试配置好Swagger后可以点击Authorize按钮输入Bearer Token直接调用受保护的API观察请求头中的Authorization字段8.2 Postman测试集建议创建以下测试用例匿名访问受保护API → 应返回401使用错误凭证登录 → 应返回401使用正确凭证登录 → 应返回Token使用Token访问API → 应成功使用过期Token访问 → 应返回401使用RefreshToken获取新Token → 应成功使用旧RefreshToken → 应失败8.3 日志记录建议在开发环境启用详细日志builder.Services.AddLogging(logging { logging.AddConsole(); logging.AddDebug(); logging.SetMinimumLevel(LogLevel.Debug); });9. 安全最佳实践密钥管理生产环境使至少32字符的强密钥通过Azure Key Vault或AWS KMS管理密钥定期轮换密钥Token安全AccessToken有效期建议15-30分钟RefreshToken有效期建议7天使用HttpOnly Cookie存储RefreshToken前端存储AccessToken要防止XSS攻击防御措施实现速率限制防止暴力破解记录失败登录尝试对敏感操作要求二次验证HTTPS强制生产环境必须启用HTTPS配置HSTS头app.UseHsts(); app.UseHttpsRedirection();10. 常见问题与解决方案10.1 Token过期问题症状客户端收到401响应响应头包含Token-Expiredtrue解决方案客户端检测到Token过期后使用RefreshToken获取新Token如果RefreshToken也过期要求用户重新登录10.2 跨域问题症状前端收到CORS错误解决方案builder.Services.AddCors(options { options.AddPolicy(AllowSpecificOrigin, builder builder.WithOrigins(https://client-domain.com) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()); }); app.UseCors(AllowSpecificOrigin);10.3 性能问题症状认证过程导致API响应变慢优化方案使用高效的签名算法(HS256比RS256更快)减少不必要的Claim考虑使用分布式缓存存储Token黑名单10.4 注销问题挑战JWT无法在服务端主动失效解决方案短期Token有效期 强制刷新维护Token黑名单(适用于关键操作)更改签名密钥使所有Token失效(极端情况)11. 进阶扩展11.1 多因素认证(MFA)[HttpPost(login)] public async TaskIActionResult Login([FromBody] LoginDto dto) { var user await _userService.Authenticate(dto.Username, dto.Password); if (user null) return Unauthorized(); if (user.RequiresMfa) { var mfaToken await _mfaService.GenerateMfaToken(user.Id); return Ok(new { RequiresMfa true, MfaToken mfaToken }); } // 正常生成Token... }11.2 微服务间认证使用JWT在服务间传递身份services.AddHttpClientIServiceClient, ServiceClient(client { client.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue( Bearer, contextAccessor.HttpContext.Request.Headers[Authorization]); });11.3 实时权限变更实现权限实时更新options.Events new JwtBearerEvents { OnTokenValidated async context { var userService context.HttpContext.RequestServices .GetRequiredServiceIUserService(); var user await userService.GetByUsername(context.Principal.Identity.Name); if (user null || !user.IsActive) { context.Fail(User is inactive); } // 添加实时权限声明 var identity context.Principal.Identity as ClaimsIdentity; identity.AddClaim(new Claim(permissions, user.Permissions)); } };12. 生产环境部署建议密钥管理使用环境变量或机密存储实现密钥轮换机制监控与告警监控认证失败率设置异常登录告警灾备方案准备备用签名密钥实现优雅降级策略文档与培训编写详细的API认证文档对客户端开发人员进行培训13. 性能优化技巧减少Token大小使用简洁的Claim名称避免在Token中包含不必要的数据缓存验证结果对有效Token的验证结果进行短期缓存对无效Token的验证结果进行更长时间缓存异步验证对于远程验证的场景使用异步验证考虑使用后台队列处理验证请求算法选择HS256比RS256验证更快对于微服务架构可以考虑使用RS25614. 客户端集成指南14.1 Web前端(React示例)// 登录函数 const login async (username, password) { const response await fetch(/api/auth/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username, password }) }); if (!response.ok) throw new Error(Login failed); const { accessToken, refreshToken, expiration } await response.json(); // 存储Token localStorage.setItem(accessToken, accessToken); document.cookie refreshToken${refreshToken}; HttpOnly; Secure; SameSiteStrict; // 设置定时刷新 scheduleTokenRefresh(expiration); }; // 定时刷新Token const scheduleTokenRefresh (expiration) { const expiresIn new Date(expiration) - Date.now(); setTimeout(refreshToken, expiresIn - 60000); // 提前1分钟刷新 }; // 刷新Token const refreshToken async () { const refreshToken getCookie(refreshToken); const response await fetch(/api/auth/refresh, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${localStorage.getItem(accessToken)} }, body: JSON.stringify({ refreshToken }) }); if (!response.ok) { // 刷新失败要求用户重新登录 return logout(); } const { accessToken, refreshToken: newRefreshToken, expiration } await response.json(); // 更新存储的Token localStorage.setItem(accessToken, accessToken); document.cookie refreshToken${newRefreshToken}; HttpOnly; Secure; SameSiteStrict; // 重新设置定时刷新 scheduleTokenRefresh(expiration); };14.2 移动端(Android示例)// Token管理类 class TokenManager(context: Context) { private val prefs context.getSharedPreferences(auth, Context.MODE_PRIVATE) fun saveTokens(accessToken: String, refreshToken: String) { prefs.edit() .putString(access_token, accessToken) .putString(refresh_token, refreshToken) .apply() } fun getAccessToken(): String? prefs.getString(access_token, null) fun getRefreshToken(): String? prefs.getString(refresh_token, null) fun clearTokens() { prefs.edit().clear().apply() } } // OkHttp拦截器 class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val request chain.request() val accessToken tokenManager.getAccessToken() val authenticatedRequest accessToken?.let { request.newBuilder() .header(Authorization, Bearer $it) .build() } ?: request var response chain.proceed(authenticatedRequest) // Token过期时尝试刷新 if (response.code 401 accessToken ! null) { synchronized(this) { val newAccessToken tokenManager.getAccessToken() if (newAccessToken ! accessToken) { // 已经刷新过了重试请求 return chain.proceed(request.newBuilder() .header(Authorization, Bearer $newAccessToken) .build()) } val refreshToken tokenManager.getRefreshToken() if (refreshToken ! null) { val tokenResponse refreshToken(refreshToken) if (tokenResponse.isSuccessful) { val tokens tokenResponse.body() tokens?.let { tokenManager.saveTokens(it.accessToken, it.refreshToken) return chain.proceed(request.newBuilder() .header(Authorization, Bearer ${it.accessToken}) .build()) } } } // 刷新失败清除Token tokenManager.clearTokens() // 跳转到登录页面 // ... } } return response } private fun refreshToken(refreshToken: String): Response { // 实现刷新Token的API调用 // ... } }15. 项目结构建议对于生产级项目推荐如下结构src/ ├── Controllers/ │ ├── AuthController.cs │ └── ... ├── Services/ │ ├── Interfaces/ │ │ ├── IAuthService.cs │ │ └── ... │ ├── AuthService.cs │ └── ... ├── Models/ │ ├── DTOs/ │ │ ├── Auth/ │ │ │ ├── LoginDto.cs │ │ │ └── ... │ │ └── ... │ ├── Entities/ │ │ ├── User.cs │ │ └── ... │ └── ... ├── Helpers/ │ ├── JwtTokenHelper.cs │ └── ... ├── Middleware/ │ ├── JwtMiddleware.cs │ └── ... └── Program.cs关键设计原则关注点分离控制器只处理HTTP相关逻辑业务逻辑集中在服务层DTO与实体分离通用功能放在Helpers或Extensions中跨切面关注点使用中间件16. 单元测试要点16.1 测试JwtTokenHelperpublic class JwtTokenHelperTests { private readonly IConfiguration _configuration; private readonly JwtTokenHelper _tokenHelper; public JwtTokenHelperTests() { var config new Dictionarystring, string { [Jwt:SecretKey] test-secret-key-with-32-chars, [Jwt:Issuer] test-issuer, [Jwt:Audience] test-audience, [Jwt:ExpirationMinutes] 30 }; _configuration new ConfigurationBuilder() .AddInMemoryCollection(config) .Build(); _tokenHelper new JwtTokenHelper(_configuration); } [Fact] public void GenerateToken_ShouldReturnValidJwt() { // Arrange var claims new[] { new Claim(ClaimTypes.Name, testuser) }; // Act var token _tokenHelper.GenerateAccessToken(claims); // Assert Assert.NotNull(token); Assert.NotEmpty(token); var handler new JwtSecurityTokenHandler(); var jwt handler.ReadJwtToken(token); Assert.Equal(test-issuer, jwt.Issuer); Assert.Contains(testuser, jwt.Claims.First(c c.Type ClaimTypes.Name).Value); } [Fact] public void GetPrincipalFromExpiredToken_ShouldReturnPrincipal() { // Arrange var claims new[] { new Claim(ClaimTypes.Name, testuser) }; var token _tokenHelper.GenerateAccessToken(claims); // Act var principal _tokenHelper.GetPrincipalFromExpiredToken(token); // Assert Assert.NotNull(principal); Assert.Equal(testuser, principal.Identity.Name); } }16.2 测试AuthControllerpublic class AuthControllerTests { private readonly AuthController _controller; private readonly MockIUserService _userServiceMock; private readonly JwtTokenHelper _tokenHelper; public AuthControllerTests() { var config new ConfigurationBuilder() .AddInMemoryCollection(new Dictionarystring, string { [Jwt:SecretKey] test-secret-key-with-32-chars, [Jwt:Issuer] test-issuer, [Jwt:Audience] test-audience, [Jwt:ExpirationMinutes] 30 }) .Build(); _tokenHelper new JwtTokenHelper(config); _userServiceMock new MockIUserService(); _controller new AuthController(_tokenHelper, _userServiceMock.Object); } [Fact] public async Task Login_WithValidCredentials_ShouldReturnToken() { // Arrange var user new User { Id 1, Username testuser, Role User }; _userServiceMock.Setup(x x.Authenticate(testuser, password)) .ReturnsAsync(user); var loginDto new LoginDto { Username testuser, Password password }; // Act var result await _controller.Login(loginDto); // Assert var okResult Assert.IsTypeOkObjectResult(result); var tokenDto Assert.IsTypeTokenDto(okResult.Value); Assert.NotNull(tokenDto.AccessToken); Assert.NotNull(tokenDto.RefreshToken); } [Fact] public async Task Login_WithInvalidCredentials_ShouldReturnUnauthorized() { // Arrange _userServiceMock.Setup(x x.Authenticate(It.IsAnystring(), It.IsAnystring())) .ReturnsAsync((User)null); var loginDto new LoginDto { Username wrong, Password wrong }; // Act var result await _controller.Login(loginDto); // Assert Assert.IsTypeUnauthorizedObjectResult(result); } }17. 集成测试策略17.1 测试认证流程public class AuthIntegrationTests : IClassFixtureWebApplicationFactoryProgram { private readonly WebApplicationFactoryProgram _factory; public AuthIntegrationTests(WebApplicationFactoryProgram factory) { _factory factory.WithWebHostBuilder(builder { builder.ConfigureTestServices(services { // 替换真实服务为测试服务 services.AddScopedIUserService, TestUserService(); }); }); } [Fact] public async Task FullAuthFlow_ShouldWork() { // Arrange var client _factory.CreateClient(); var loginDto new { username testuser, password password }; // Act 1: 登录获取Token var loginResponse await client.PostAsJsonAsync(/api/auth/login, loginDto); loginResponse.EnsureSuccessStatusCode(); var tokenDto await loginResponse.Content.ReadFromJsonAsyncTokenDto(); // Assert 1: 验证Token结构 Assert.NotNull(tokenDto); Assert.NotNull(tokenDto.AccessToken); Assert.NotNull(tokenDto.RefreshToken); // Act 2: 使用Token访问受保护API client.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, tokenDto.AccessToken); var protectedResponse await client.GetAsync(/api/protected); // Assert 2: 验证访问成功 protectedResponse.EnsureSuccessStatusCode(); // Act 3: 模拟Token过期并刷新 await Task.Delay(1000); // 确保Token过期(测试环境Token有效期很短) var refreshResponse await client.PostAsJsonAsync(/api/auth/refresh, new { refreshToken tokenDto.RefreshToken }); // Assert 3: 验证刷新成功 refreshResponse.EnsureSuccessStatusCode(); var newTokenDto await refreshResponse.Content.ReadFromJsonAsyncTokenDto(); Assert.NotNull(newTokenDto); Assert.NotEqual(tokenDto.AccessToken, newTokenDto.AccessToken); } } public class TestUserService : IUserService { public TaskUser Authenticate(string username, string password) { if (username testuser password password) return Task.FromResult(new User { Id 1, Username testuser, Role User }); return Task.FromResultUser(null); } // 实现其他接口方法... }18. 性能测试建议使用BenchmarkDotNet测试关键组件[MemoryDiagnoser] public class JwtBenchmarks { private JwtTokenHelper _tokenHelper; private IEnumerableClaim _claims; [GlobalSetup] public void Setup() { var config new ConfigurationBuilder() .AddInMemoryCollection(new Dictionarystring, string { [Jwt:SecretKey] benchmark-secret-key-with-32-chars, [Jwt:Issuer] benchmark-issuer, [Jwt:Audience] benchmark-audience, [Jwt:ExpirationMinutes] 30 }) .Build(); _tokenHelper new JwtTokenHelper(config); _claims new[] { new Claim(ClaimTypes.Name, benchmarkuser), new Claim(ClaimTypes.Role, User), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; } [Benchmark] public string GenerateToken() _tokenHelper.GenerateAccessToken(_claims); [Benchmark] public ClaimsPrincipal ValidateToken() { var token _tokenHelper.GenerateAccessToken(_claims); return _tokenHelper.GetPrincipalFromExpiredToken(token); } }关键指标Token生成时间Token验证时间内存分配情况19. 安全审计要点密钥强度检查确保密钥长度足够(HS256至少32字符)验证密钥是否安全存储Token验证检查验证所有关键参数(issuer, audience等)是否开启检查ClockSkew设置是否合理传输安全检查确认始终使用HTTPS检查CORS配置是否合理存储安全检查前端存储方式是否安全RefreshToken是否使用HttpOnly Cookie注销机制检查是否有合理的Token失效机制关键操作是否有二次验证20. 升级与迁移策略20.1 从Session迁移到JWT并行运行同时支持Session和JWT认证逐步迁移客户端用户状态处理将必要状态从Session移到Token Claims使用服务端缓存补充客户端适配更新客户端Token处理逻辑实现自动刷新机制20.2 JWT版本升级