JWT 认证
一、JWT 是什么?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以 JSON 格式安全地传输声明(Claims)。
典型用途: 用户登录后,服务端签发一个 JWT,客户端后续请求携带该 Token,服务端验证其有效性,实现无状态认证。
二、JWT 的结构(三段式 Base64 编码)
一个 JWT 通常形如:xxxxx.yyyyy.zzzzz
1. Header(头部)
包含令牌类型和签名算法,经 Base64Url 编码。
json
{
"alg": "HS256",
"typ": "JWT"
}2. Payload(载荷)
包含"声明"(Claims),即用户信息和元数据。
标准声明(Registered Claims):
sub(Subject): 主体(如用户ID)exp(Expiration Time): 过期时间(Unix 时间戳)iat(Issued At): 签发时间iss(Issuer): 签发者aud(Audience): 接收方
自定义声明: 如 role, name, email 等
json
{
"sub": "1234567890",
"name": "John Doe",
"role": "Admin",
"exp": 1300819380
}3. Signature(签名)
用于验证 Token 未被篡改:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)三、JWT 的优缺点
✅ 优点
- 无状态 - 服务端不需要存储会话信息
- 跨域友好 - 可以在不同域名之间传递
- 性能好 - 减少数据库查询
- 扩展性强 - 便于分布式系统
❌ 缺点
- 无法撤销 - Token 签发后在过期前一直有效
- 安全性 - 如果 secret 泄露,所有 Token 都可被伪造
- 体积较大 - 每次请求都要携带完整 Token
- 续签问题 - 需要额外机制处理 Token 刷新
四、.NET Core 中的 JWT 实现
1. 安装 NuGet 包
bash
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer2. 配置服务
csharp
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "your-issuer",
ValidAudience = "your-audience",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-secret-key"))
};
});3. 生成 Token
csharp
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role)
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddHours(2),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}4. 验证 Token
csharp
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value;
return Ok(new { userId, username });
}5. 在 .NET Core 中注入服务
csharp
// 在 Program.cs 中注册 JWT 服务
builder.Services.AddSingleton<IJwtService, JwtService>();
// 使用配置
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
// JWT 服务实现
public class JwtService : IJwtService
{
private readonly IConfiguration _configuration;
public JwtService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Token ID
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(
int.Parse(_configuration["Jwt:ExpireMinutes"] ?? "60")),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}6. Refresh Token 实现
csharp
// Refresh Token 实体
public class RefreshToken
{
public string Token { get; set; }
public DateTime Expires { get; set; }
public int UserId { get; set; }
}
// 生成 Refresh Token
public string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
// 登录接口返回 Access Token 和 Refresh Token
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var user = await _userService.AuthenticateAsync(request.Username, request.Password);
if (user == null)
{
return Unauthorized();
}
var accessToken = _jwtService.GenerateToken(user);
var refreshToken = _jwtService.GenerateRefreshToken();
// 保存 Refresh Token 到数据库或缓存
await _tokenService.SaveRefreshTokenAsync(user.Id, refreshToken);
return Ok(new
{
accessToken,
refreshToken,
expiresIn = 3600 // 秒
});
}
// 刷新 Token 接口
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken(RefreshTokenRequest request)
{
var refreshToken = await _tokenService.GetRefreshTokenAsync(request.RefreshToken);
if (refreshToken == null || refreshToken.Expires < DateTime.UtcNow)
{
return Unauthorized();
}
var user = await _userService.GetByIdAsync(refreshToken.UserId);
var newAccessToken = _jwtService.GenerateToken(user);
return Ok(new { accessToken = newAccessToken });
}五、常见面试题
Q1: JWT 和 Session 的区别?
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端 |
| 扩展性 | 好(无状态) | 差(需要共享session) |
| 安全性 | Token 可能被窃取 | 相对安全(服务端控制) |
| 注销 | 困难(需要黑名单) | 简单(删除session) |
Q2: 如何解决 JWT 无法撤销的问题?
- 设置较短过期时间 + Refresh Token
- 维护黑名单 - 将需要撤销的 Token 加入 Redis 黑名单
- 版本号机制 - 在 Payload 中添加版本号,用户修改密码时递增版本号
Q3: JWT 的 secret 应该如何管理?
- 使用环境变量或配置中心存储
- 定期轮换 secret
- 使用非对称加密(RSA)代替对称加密
- 避免硬编码在代码中
使用非对称加密(RSA):
csharp
// 生成 RSA 密钥对
var rsa = RSA.Create();
var privateKey = rsa.ExportRSAPrivateKey();
var publicKey = rsa.ExportRSAPublicKey();
// 签发 Token(使用私钥)
var key = new RsaSecurityKey(rsa);
var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
// 验证 Token(使用公钥)
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(RSA.Create().ImportRSAPublicKey(publicKey, out _)),
// ... 其他参数
};Q4: 如何处理 Token 过期?
方案一:Refresh Token 机制
- Access Token 设置较短过期时间(如 15 分钟)
- Refresh Token 设置较长过期时间(如 7 天)
- 使用 Refresh Token 刷新 Access Token
方案二:Token 续签
- 在 Token 即将过期前自动续签
- 在每次请求时检查 Token 剩余时间,如果小于阈值则续签
csharp
[Authorize]
public class ApiController : ControllerBase
{
[HttpPost("refresh-if-needed")]
public async Task<IActionResult> RefreshIfNeeded()
{
var expiresClaim = User.FindFirst(JwtRegisteredClaimNames.Exp)?.Value;
if (expiresClaim != null)
{
var expires = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiresClaim));
var timeUntilExpiry = expires - DateTime.UtcNow;
// 如果剩余时间小于 5 分钟,自动续签
if (timeUntilExpiry.TotalMinutes < 5)
{
var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value);
var user = await _userService.GetByIdAsync(userId);
var newToken = _jwtService.GenerateToken(user);
return Ok(new { accessToken = newToken });
}
}
return Ok();
}
}六、实际应用场景
1. 多角色权限控制
csharp
[Authorize(Roles = "Admin")]
[HttpDelete("users/{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
// 只有 Admin 角色可以删除用户
await _userService.DeleteAsync(id);
return NoContent();
}
[Authorize(Roles = "Admin,Manager")]
[HttpPut("users/{id}")]
public async Task<IActionResult> UpdateUser(int id, UpdateUserRequest request)
{
// Admin 和 Manager 都可以更新用户
await _userService.UpdateAsync(id, request);
return Ok();
}2. 自定义策略
csharp
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("Over18", policy =>
policy.RequireClaim("Age", "18", "19", "20")); // 大于等于 18
options.AddPolicy("OwnResourceOrAdmin", policy =>
policy.Requirements.Add(new OwnResourceOrAdminRequirement()));
});
// 使用
[Authorize(Policy = "Over18")]
public IActionResult RestrictedContent() { }
[Authorize(Policy = "OwnResourceOrAdmin")]
public IActionResult UpdateResource(int id) { }3. Token 黑名单(Redis 实现)
csharp
public class TokenBlacklistService
{
private readonly IDatabase _redis;
public TokenBlacklistService(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}
public async Task BlacklistTokenAsync(string token, DateTime expires)
{
var jti = GetTokenId(token);
var ttl = expires - DateTime.UtcNow;
if (ttl > TimeSpan.Zero)
{
await _redis.StringSetAsync($"blacklist:{jti}", "1", ttl);
}
}
public async Task<bool> IsTokenBlacklistedAsync(string token)
{
var jti = GetTokenId(token);
return await _redis.KeyExistsAsync($"blacklist:{jti}");
}
}
// 自定义中间件验证黑名单
public class BlacklistTokenMiddleware
{
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context, TokenBlacklistService blacklistService)
{
var token = context.Request.Headers["Authorization"]
.ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(token) &&
await blacklistService.IsTokenBlacklistedAsync(token))
{
context.Response.StatusCode = 401;
return;
}
await _next(context);
}
}七、最佳实践
- ✅ 使用 HTTPS 传输 Token - 防止 Token 被窃取
- ✅ 设置合理的过期时间 - Access Token 15-60 分钟,Refresh Token 7-30 天
- ✅ 敏感信息不要放在 Payload 中 - Payload 可以被解码(只编码不加密)
- ✅ 实现 Refresh Token 机制 - 解决 Token 撤销问题
- ✅ 对 Token 进行签名验证 - 确保 Token 未被篡改
- ✅ 使用 Token 黑名单 - 实现 Token 撤销功能
- ✅ 定期轮换 Secret - 增强安全性
- ✅ 使用非对称加密 - 分布式系统推荐使用 RSA
- ❌ 不要在 URL 中传递 Token - 会记录在日志中
- ❌ 不要在客户端存储敏感的 secret - Secret 只存在于服务端
- ❌ 不要设置过长的过期时间 - 增加安全风险
- ❌ 不要在 Payload 中存储过多数据 - 增加 Token 大小