C# JWT令牌生成与验证全面指南


一、JWT核心概念解析

1.1 JWT令牌结构

组成部分描述示例内容
Header元数据和签名算法{"alg":"HS256","typ":"JWT"}
Payload包含声明的有效载荷{"sub":"123","name":"John","iat":1516239022}
Signature验证令牌完整性的签名使用Header中指定的算法生成

1.2 JWT工作流程

  1. 客户端认证:用户提交凭证(如用户名/密码)
  2. 服务端验证:验证凭证有效性
  3. 令牌生成:创建包含用户信息的JWT
  4. 令牌返回:将JWT发送给客户端
  5. 后续请求:客户端在Authorization头携带JWT
  6. 服务端验证:验证签名和声明

二、基础令牌生成

2.1 依赖包安装

dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.IdentityModel.Tokens

2.2 最小实现示例

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;

public string GenerateSimpleToken()
{
    var securityKey = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes("your-256-bit-secret"));
    var credentials = new SigningCredentials(
        securityKey, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: "your-issuer",
        audience: "your-audience",
        claims: new[] { new Claim(ClaimTypes.Name, "username") },
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: credentials);

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

三、完整令牌服务实现

3.1 配置类设计

public class JwtSettings
{
    public string Secret { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public int ExpireMinutes { get; set; }
    public int RefreshTokenExpireDays { get; set; }
}

// appsettings.json配置
{
  "Jwt": {
    "Secret": "your-256-bit-secret-key",
    "Issuer": "https://yourdomain.com",
    "Audience": "https://yourdomain.com",
    "ExpireMinutes": 30,
    "RefreshTokenExpireDays": 7
  }
}

3.2 令牌服务实现

public class JwtTokenService
{
    private readonly JwtSettings _settings;
    private readonly byte[] _secret;

    public JwtTokenService(IOptions<JwtSettings> settings)
    {
        _settings = settings.Value;
        _secret = Encoding.ASCII.GetBytes(_settings.Secret);
    }

    public string GenerateAccessToken(IEnumerable<Claim> claims)
    {
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddMinutes(_settings.ExpireMinutes),
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(_secret),
                SecurityAlgorithms.HmacSha256Signature),
            Issuer = _settings.Issuer,
            Audience = _settings.Audience
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

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

    public ClaimsPrincipal GetPrincipalFromToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();

        try
        {
            var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(_secret),
                ValidateIssuer = true,
                ValidIssuer = _settings.Issuer,
                ValidateAudience = true,
                ValidAudience = _settings.Audience,
                ValidateLifetime = false // 过期令牌也能解析
            }, out var validatedToken);

            if (!IsJwtWithValidSecurityAlgorithm(validatedToken))
                return null;

            return principal;
        }
        catch
        {
            return null;
        }
    }

    private bool IsJwtWithValidSecurityAlgorithm(SecurityToken validatedToken)
    {
        return validatedToken is JwtSecurityToken jwtSecurityToken &&
               jwtSecurityToken.Header.Alg.Equals(
                   SecurityAlgorithms.HmacSha256,
                   StringComparison.InvariantCultureIgnoreCase);
    }
}

四、声明(Claims)设计策略

4.1 标准声明类型

声明类型描述JWT规范字段
用户标识用户唯一IDsub
用户名用户显示名称unique_name
角色用户角色列表role
颁发时间令牌创建时间iat
过期时间令牌过期时间exp
生效时间令牌生效时间nbf

4.2 自定义声明实现

public IEnumerable<Claim> GenerateUserClaims(User user, IList<string> roles)
{
    var claims = new List<Claim>
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim("department", user.Department),
        new Claim("position", user.Position)
    };

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

    // 添加权限声明
    var permissions = _permissionService.GetUserPermissions(user.Id);
    foreach (var permission in permissions)
    {
        claims.Add(new Claim("permission", permission));
    }

    return claims;
}

五、签名与加密方案

5.1 对称加密(HMAC)

// 使用256位密钥
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
    Configuration["Jwt:Secret"]));

var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

5.2 非对称加密(RSA)

// 从文件读取RSA私钥
var privateKey = File.ReadAllText("private-key.pem");
var rsa = RSA.Create();
rsa.ImportFromPem(privateKey);

var signingCredentials = new SigningCredentials(
    new RsaSecurityKey(rsa),
    SecurityAlgorithms.RsaSha256);

5.3 加密令牌(JWE)

var encryptingCredentials = new EncryptingCredentials(
    new SymmetricSecurityKey(Encoding.UTF8.GetBytes("encryption-key")),
    SecurityAlgorithms.Aes128KW,
    SecurityAlgorithms.Aes128CbcHmacSha256);

var tokenDescriptor = new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(claims),
    SigningCredentials = signingCredentials,
    EncryptingCredentials = encryptingCredentials,
    Expires = DateTime.UtcNow.AddHours(1)
};

var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);

六、令牌验证与刷新

6.1 验证中间件配置

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = Configuration["Jwt:Audience"],
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(Configuration["Jwt:Secret"])),
            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;
            }
        };
    });

6.2 刷新令牌机制

[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
    var principal = _tokenService.GetPrincipalFromExpiredToken(request.AccessToken);
    if (principal == null)
        return BadRequest("Invalid token");

    var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
    var user = await _userManager.FindByIdAsync(userId);

    if (user == null || user.RefreshToken != request.RefreshToken || 
        user.RefreshTokenExpiryTime <= DateTime.UtcNow)
        return BadRequest("Invalid refresh token");

    var newClaims = await _claimsService.GetUserClaimsAsync(user);
    var newAccessToken = _tokenService.GenerateAccessToken(newClaims);
    var newRefreshToken = _tokenService.GenerateRefreshToken();

    user.RefreshToken = newRefreshToken;
    user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpireDays);
    await _userManager.UpdateAsync(user);

    return Ok(new TokenResponse
    {
        AccessToken = newAccessToken,
        RefreshToken = newRefreshToken,
        ExpiresIn = _jwtSettings.ExpireMinutes * 60
    });
}

七、性能优化策略

7.1 令牌压缩

// 使用DEFLATE压缩Payload
public string GenerateCompressedToken(IEnumerable<Claim> claims)
{
    var payload = new JwtPayload(
        _settings.Issuer,
        _settings.Audience,
        claims,
        DateTime.UtcNow,
        DateTime.UtcNow.AddMinutes(_settings.ExpireMinutes));

    using var outputStream = new MemoryStream();
    using (var compressionStream = new DeflateStream(outputStream, CompressionLevel.Optimal))
    {
        using var writer = new StreamWriter(compressionStream);
        writer.Write(payload.SerializeToJson());
    }

    var compressedPayload = Base64UrlEncoder.Encode(outputStream.ToArray());
    var header = new JwtHeader(new SigningCredentials(
        new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256));

    return $"{header.Base64UrlEncode()}.{compressedPayload}";
}

7.2 缓存验证结果

services.AddMemoryCache();

public class CachingJwtValidator : ISecurityTokenValidator
{
    private readonly IJwtValidator _innerValidator;
    private readonly IMemoryCache _cache;

    public bool ValidateToken(
        string token, 
        TokenValidationParameters validationParameters,
        out SecurityToken validatedToken)
    {
        var cacheKey = $"jwt_validation_{token.GetHashCode()}";

        if (_cache.TryGetValue(cacheKey, out validatedToken))
            return true;

        var result = _innerValidator.ValidateToken(
            token, validationParameters, out validatedToken);

        if (result)
        {
            var expiry = (validatedToken as JwtSecurityToken)?.ValidTo;
            var cacheTime = expiry - DateTime.UtcNow - TimeSpan.FromMinutes(1);
            _cache.Set(cacheKey, validatedToken, cacheTime.Value);
        }

        return result;
    }
}

八、安全增强措施

8.1 令牌黑名单

public class TokenBlacklistService
{
    private readonly IDistributedCache _cache;

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

    public async Task BlacklistToken(string token, TimeSpan expiry)
    {
        var tokenId = GetTokenId(token);
        await _cache.SetStringAsync(
            $"blacklist_{tokenId}", 
            "revoked", 
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = expiry
            });
    }

    public async Task<bool> IsTokenRevoked(string token)
    {
        var tokenId = GetTokenId(token);
        var result = await _cache.GetStringAsync($"blacklist_{tokenId}");
        return result != null;
    }

    private string GetTokenId(string token)
    {
        var handler = new JwtSecurityTokenHandler();
        var jwtToken = handler.ReadJwtToken(token);
        return jwtToken.Id;
    }
}

8.2 动态密钥轮换

public class KeyRotationService : IKeyRotationService
{
    private readonly List<SymmetricSecurityKey> _activeKeys;
    private readonly IConfiguration _config;

    public KeyRotationService(IConfiguration config)
    {
        _config = config;
        _activeKeys = LoadActiveKeys();
    }

    public IEnumerable<SymmetricSecurityKey> GetValidKeys()
    {
        return _activeKeys;
    }

    public void RotateKeys()
    {
        var newKey = GenerateNewKey();
        _activeKeys.Insert(0, newKey);

        // 保留最近3个有效密钥
        if (_activeKeys.Count > 3)
            _activeKeys.RemoveRange(3, _activeKeys.Count - 3);
    }

    private SymmetricSecurityKey GenerateNewKey()
    {
        var key = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(key);
        return new SymmetricSecurityKey(key);
    }
}

// 在验证时接受多个密钥
services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters.IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>
        {
            return _keyRotationService.GetValidKeys();
        };
    });

九、测试与调试

9.1 单元测试示例

public class JwtTokenServiceTests
{
    private readonly JwtTokenService _tokenService;

    public JwtTokenServiceTests()
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string>
            {
                ["Jwt:Secret"] = "test-secret-12345678901234567890123456789012",
                ["Jwt:Issuer"] = "test-issuer",
                ["Jwt:Audience"] = "test-audience",
                ["Jwt:ExpireMinutes"] = "30"
            })
            .Build();

        _tokenService = new JwtTokenService(Options.Create(config.GetSection("Jwt").Get<JwtSettings>()));
    }

    [Fact]
    public void GenerateToken_ShouldReturnValidJwt()
    {
        // Arrange
        var claims = new[] { new Claim(ClaimTypes.Name, "testuser") };

        // Act
        var token = _tokenService.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(jwt.Claims, c => c.Type == ClaimTypes.Name && c.Value == "testuser");
    }
}

9.2 调试令牌内容

// 解码JWT令牌
public JwtSecurityToken DecodeToken(string token)
{
    var handler = new JwtSecurityTokenHandler();
    return handler.ReadJwtToken(token);
}

// 打印令牌信息
public void PrintTokenInfo(string token)
{
    var jwt = DecodeToken(token);

    Console.WriteLine($"Issuer: {jwt.Issuer}");
    Console.WriteLine($"Audience: {string.Join(",", jwt.Audiences)}");
    Console.WriteLine($"Valid From: {jwt.ValidFrom}");
    Console.WriteLine($"Valid To: {jwt.ValidTo}");

    Console.WriteLine("\nClaims:");
    foreach (var claim in jwt.Claims)
    {
        Console.WriteLine($"{claim.Type}: {claim.Value}");
    }
}

十、生产环境实践

10.1 Kubernetes密钥管理

# 通过Secret存储JWT密钥
apiVersion: v1
kind: Secret
metadata:
  name: jwt-secret
type: Opaque
data:
  jwt-secret: YmFzZTY0LWVuY29kZWQtc2VjcmV0Cg==  # base64编码的密钥

10.2 密钥注入方案

// 从环境变量获取密钥
var secret = Environment.GetEnvironmentVariable("JWT_SECRET") 
    ?? throw new InvalidOperationException("JWT_SECRET not configured");

// 或使用Kubernetes Secrets
var secret = File.ReadAllText("/etc/secrets/jwt-secret");

// 配置JWT服务
services.AddSingleton<IJwtTokenService>(_ => 
    new JwtTokenService(new JwtSettings
    {
        Secret = secret,
        Issuer = Configuration["Jwt:Issuer"],
        Audience = Configuration["Jwt:Audience"],
        ExpireMinutes = Configuration.GetValue<int>("Jwt:ExpireMinutes")
    }));

通过本指南,开发者可以全面掌握在C#中生成、验证和管理JWT令牌的最佳实践。从基础实现到高级安全方案,这些技术将帮助构建安全可靠的现代认证系统。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注