Skip to main content

JWT 身份认证

什么是 JWT?

JWT (JSON Web Token) 是一种开放标准 (RFC 7519),用于在各方之间以 JSON 对象安全地传输信息。

JWT 结构

JWT 由三部分组成,用点(.)分隔:

Header.Payload.Signature

示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header(头部)

{
"alg": "HS256",
"typ": "JWT"
}

2. Payload(载荷)

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}

3. Signature(签名)

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)

安装依赖

# JWT 认证中间件
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

# JWT 生成和验证
dotnet add package System.IdentityModel.Tokens.Jwt

基本实现

1. 配置 appsettings.json

{
"JwtSettings": {
"SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
"Issuer": "YourAppName",
"Audience": "YourAppUsers",
"ExpirationMinutes": 60,
"RefreshTokenExpirationDays": 7
}
}

2. 创建 JWT 配置类

public class JwtSettings
{
public string SecretKey { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; }
public int RefreshTokenExpirationDays { get; set; }
}

3. 配置服务

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// 绑定 JWT 配置
var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));

// 配置 JWT 认证
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings.SecretKey)
),
ClockSkew = TimeSpan.Zero // 移除默认的 5 分钟时钟偏移
};

// 配置事件处理
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Console.WriteLine($"Token validated for user: {context.Principal?.Identity?.Name}");
return Task.CompletedTask;
}
};
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication(); // 必须在 UseAuthorization 之前
app.UseAuthorization();

app.MapControllers();
app.Run();

4. 创建 JWT 服务

public interface IJwtService
{
string GenerateToken(string userId, string username, IEnumerable<string> roles);
string GenerateRefreshToken();
ClaimsPrincipal? ValidateToken(string token);
}

public class JwtService : IJwtService
{
private readonly JwtSettings _jwtSettings;

public JwtService(IOptions<JwtSettings> jwtSettings)
{
_jwtSettings = jwtSettings.Value;
}

public string GenerateToken(string userId, string username, IEnumerable<string> roles)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
};

// 添加角色声明
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes),
signingCredentials: credentials
);

return new JwtSecurityTokenHandler().WriteToken(token);
}

public string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}

public ClaimsPrincipal? ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_jwtSettings.SecretKey);

try
{
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _jwtSettings.Issuer,
ValidAudience = _jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);

return principal;
}
catch
{
return null;
}
}
}

// 注册服务
builder.Services.AddScoped<IJwtService, JwtService>();

5. 创建认证控制器

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IJwtService _jwtService;
private readonly ILogger<AuthController> _logger;

public AuthController(IJwtService jwtService, ILogger<AuthController> logger)
{
_jwtService = jwtService;
_logger = logger;
}

[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// TODO: 验证用户名和密码(从数据库查询)
if (request.Username != "admin" || request.Password != "password")
{
return Unauthorized(new { message = "用户名或密码错误" });
}

// 生成 Access Token
var accessToken = _jwtService.GenerateToken(
userId: "1",
username: request.Username,
roles: new[] { "Admin", "User" }
);

// 生成 Refresh Token
var refreshToken = _jwtService.GenerateRefreshToken();

// TODO: 保存 Refresh Token 到数据库

return Ok(new LoginResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = 3600 // 秒
});
}

[HttpPost("refresh")]
public IActionResult Refresh([FromBody] RefreshTokenRequest request)
{
// TODO: 验证 Refresh Token(从数据库查询)
var principal = _jwtService.ValidateToken(request.AccessToken);
if (principal == null)
{
return Unauthorized(new { message = "无效的 Token" });
}

var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = principal.FindFirst(ClaimTypes.Name)?.Value;
var roles = principal.FindAll(ClaimTypes.Role).Select(c => c.Value);

// 生成新的 Access Token
var newAccessToken = _jwtService.GenerateToken(userId!, username!, roles);

return Ok(new
{
AccessToken = newAccessToken,
ExpiresIn = 3600
});
}

[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value;
var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);

return Ok(new
{
UserId = userId,
Username = username,
Roles = roles
});
}

[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult AdminOnly()
{
return Ok(new { message = "只有管理员可以访问" });
}
}

public record LoginRequest(string Username, string Password);
public record RefreshTokenRequest(string AccessToken, string RefreshToken);
public record LoginResponse
{
public string AccessToken { get; init; } = string.Empty;
public string RefreshToken { get; init; } = string.Empty;
public int ExpiresIn { get; init; }
}

高级特性

1. Refresh Token 管理

public class RefreshToken
{
public int Id { get; set; }
public string UserId { get; set; } = string.Empty;
public string Token { get; set; } = string.Empty;
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public string? ReplacedByToken { get; set; }
public DateTime? RevokedAt { get; set; }
public string? RevokedByIp { get; set; }

public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
public bool IsRevoked => RevokedAt != null;
public bool IsActive => !IsRevoked && !IsExpired;
}

public interface IRefreshTokenService
{
Task<RefreshToken> CreateRefreshTokenAsync(string userId);
Task<RefreshToken?> GetRefreshTokenAsync(string token);
Task RevokeRefreshTokenAsync(string token, string? replacedByToken = null);
Task RemoveExpiredRefreshTokensAsync();
}

public class RefreshTokenService : IRefreshTokenService
{
private readonly ApplicationDbContext _context;
private readonly JwtSettings _jwtSettings;

public RefreshTokenService(ApplicationDbContext context, IOptions<JwtSettings> jwtSettings)
{
_context = context;
_jwtSettings = jwtSettings.Value;
}

public async Task<RefreshToken> CreateRefreshTokenAsync(string userId)
{
var refreshToken = new RefreshToken
{
UserId = userId,
Token = GenerateRefreshToken(),
ExpiresAt = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays),
CreatedAt = DateTime.UtcNow
};

_context.RefreshTokens.Add(refreshToken);
await _context.SaveChangesAsync();

return refreshToken;
}

public async Task<RefreshToken?> GetRefreshTokenAsync(string token)
{
return await _context.RefreshTokens
.FirstOrDefaultAsync(rt => rt.Token == token);
}

public async Task RevokeRefreshTokenAsync(string token, string? replacedByToken = null)
{
var refreshToken = await GetRefreshTokenAsync(token);
if (refreshToken == null) return;

refreshToken.RevokedAt = DateTime.UtcNow;
refreshToken.ReplacedByToken = replacedByToken;

await _context.SaveChangesAsync();
}

public async Task RemoveExpiredRefreshTokensAsync()
{
var expiredTokens = await _context.RefreshTokens
.Where(rt => rt.ExpiresAt < DateTime.UtcNow)
.ToListAsync();

_context.RefreshTokens.RemoveRange(expiredTokens);
await _context.SaveChangesAsync();
}

private string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}

2. 自定义授权策略

// 基于 Claim 的策略
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));

options.AddPolicy("RequireEmailVerified", policy =>
policy.RequireClaim("EmailVerified", "true"));

options.AddPolicy("MinimumAge18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

// 自定义授权要求
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }

public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == "DateOfBirth");
if (dateOfBirthClaim == null)
{
return Task.CompletedTask;
}

if (DateTime.TryParse(dateOfBirthClaim.Value, out var dateOfBirth))
{
var age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
{
age--;
}

if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}
}

// 注册处理器
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

// 使用策略
[Authorize(Policy = "MinimumAge18")]
[HttpGet("adult-content")]
public IActionResult GetAdultContent()
{
return Ok(new { message = "成人内容" });
}

3. 多租户支持

public class JwtService : IJwtService
{
public string GenerateToken(string userId, string username, string tenantId, IEnumerable<string> roles)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, username),
new Claim("TenantId", tenantId), // 添加租户 ID
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};

foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}

// ... 生成 Token
}
}

// 租户中间件
public class TenantMiddleware
{
private readonly RequestDelegate _next;

public TenantMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
var tenantId = context.User.FindFirst("TenantId")?.Value;
if (!string.IsNullOrEmpty(tenantId))
{
context.Items["TenantId"] = tenantId;
}

await _next(context);
}
}

// 使用中间件
app.UseMiddleware<TenantMiddleware>();

4. Token 黑名单

public interface ITokenBlacklistService
{
Task AddToBlacklistAsync(string token, DateTime expiresAt);
Task<bool> IsBlacklistedAsync(string token);
Task RemoveExpiredTokensAsync();
}

public class TokenBlacklistService : ITokenBlacklistService
{
private readonly IDistributedCache _cache;

public TokenBlacklistService(IDistributedCache cache)
{
_cache = cache;
}

public async Task AddToBlacklistAsync(string token, DateTime expiresAt)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpiration = expiresAt
};

await _cache.SetStringAsync($"blacklist:{token}", "1", options);
}

public async Task<bool> IsBlacklistedAsync(string token)
{
var result = await _cache.GetStringAsync($"blacklist:{token}");
return result != null;
}

public async Task RemoveExpiredTokensAsync()
{
// Redis 会自动删除过期的 key
await Task.CompletedTask;
}
}

// JWT Bearer 事件中检查黑名单
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var tokenBlacklist = context.HttpContext.RequestServices
.GetRequiredService<ITokenBlacklistService>();

var token = context.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");

if (await tokenBlacklist.IsBlacklistedAsync(token))
{
context.Fail("Token has been revoked");
}
}
};

客户端使用

1. HTTP 请求头

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

2. JavaScript 示例

// 登录
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'admin',
password: 'password'
})
});

const data = await response.json();
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);

// 使用 Token 访问受保护的 API
const protectedResponse = await fetch('/api/auth/profile', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
});

// 刷新 Token
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken')
})
});

const refreshData = await refreshResponse.json();
localStorage.setItem('accessToken', refreshData.accessToken);

3. C# HttpClient 示例

public class ApiClient
{
private readonly HttpClient _httpClient;
private string? _accessToken;

public ApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}

public async Task<bool> LoginAsync(string username, string password)
{
var request = new { Username = username, Password = password };
var response = await _httpClient.PostAsJsonAsync("/api/auth/login", request);

if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<LoginResponse>();
_accessToken = result?.AccessToken;
return true;
}

return false;
}

public async Task<UserProfile?> GetProfileAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "/api/auth/profile");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);

var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<UserProfile>();
}

return null;
}
}

最佳实践

最佳实践

安全性

  • ✅ 使用强密钥(至少 256 位)
  • ✅ 密钥存储在环境变量或密钥管理服务中
  • ✅ 设置合理的过期时间(Access Token: 15-60 分钟)
  • ✅ 使用 HTTPS 传输
  • ✅ 不在 Payload 中存储敏感信息
  • ✅ 实现 Refresh Token 轮换机制
  • ✅ 实现 Token 撤销机制

性能

  • ✅ 使用对称加密(HS256)而非非对称加密(RS256)以提高性能
  • ✅ 缓存公钥(如果使用 RS256)
  • ✅ 移除 ClockSkew 或设置较小值
  • ✅ 使用分布式缓存存储黑名单

可维护性

  • ✅ 使用配置文件管理 JWT 设置
  • ✅ 创建专门的 JWT 服务类
  • ✅ 记录认证失败日志
  • ✅ 使用自定义授权策略
  • ✅ 统一错误处理

常见问题

1. Token 过期处理

[HttpGet("protected")]
public IActionResult Protected()
{
try
{
// 业务逻辑
return Ok();
}
catch (SecurityTokenExpiredException)
{
return Unauthorized(new { message = "Token 已过期,请刷新" });
}
}

2. 跨域问题

builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();

3. Swagger 集成

builder.Services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});

c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});

相关资源