Skip to content

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 的优缺点

✅ 优点

  1. 无状态 - 服务端不需要存储会话信息
  2. 跨域友好 - 可以在不同域名之间传递
  3. 性能好 - 减少数据库查询
  4. 扩展性强 - 便于分布式系统

❌ 缺点

  1. 无法撤销 - Token 签发后在过期前一直有效
  2. 安全性 - 如果 secret 泄露,所有 Token 都可被伪造
  3. 体积较大 - 每次请求都要携带完整 Token
  4. 续签问题 - 需要额外机制处理 Token 刷新

四、.NET Core 中的 JWT 实现

1. 安装 NuGet 包

bash
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

2. 配置服务

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 的区别?

特性JWTSession
存储位置客户端服务端
扩展性好(无状态)差(需要共享session)
安全性Token 可能被窃取相对安全(服务端控制)
注销困难(需要黑名单)简单(删除session)

Q2: 如何解决 JWT 无法撤销的问题?

  1. 设置较短过期时间 + Refresh Token
  2. 维护黑名单 - 将需要撤销的 Token 加入 Redis 黑名单
  3. 版本号机制 - 在 Payload 中添加版本号,用户修改密码时递增版本号

Q3: JWT 的 secret 应该如何管理?

  1. 使用环境变量配置中心存储
  2. 定期轮换 secret
  3. 使用非对称加密(RSA)代替对称加密
  4. 避免硬编码在代码中

使用非对称加密(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);
    }
}

七、最佳实践

  1. 使用 HTTPS 传输 Token - 防止 Token 被窃取
  2. 设置合理的过期时间 - Access Token 15-60 分钟,Refresh Token 7-30 天
  3. 敏感信息不要放在 Payload 中 - Payload 可以被解码(只编码不加密)
  4. 实现 Refresh Token 机制 - 解决 Token 撤销问题
  5. 对 Token 进行签名验证 - 确保 Token 未被篡改
  6. 使用 Token 黑名单 - 实现 Token 撤销功能
  7. 定期轮换 Secret - 增强安全性
  8. 使用非对称加密 - 分布式系统推荐使用 RSA
  9. 不要在 URL 中传递 Token - 会记录在日志中
  10. 不要在客户端存储敏感的 secret - Secret 只存在于服务端
  11. 不要设置过长的过期时间 - 增加安全风险
  12. 不要在 Payload 中存储过多数据 - 增加 Token 大小

基于 VitePress 构建 | Copyright © 2026-present