共计 3467 个字符,预计需要花费 9 分钟才能阅读完成。
问题背景:为什么 Token 重复生成很危险
在 API 开发中,Token 相当于用户的数字身份证。如果每次生成的 Token 都一样,就像给所有用户发了相同的门禁卡——只要有一张卡泄露,整个系统的安全性就会崩塌。我们项目曾遇到过因 Token 重复导致的黑产批量爬取数据事件,攻击者拿到一个 Token 就能永久有效访问所有接口。

常见风险场景包括:
- 用户 A 的 Token 被用户 B 盗用
- 爬虫程序重复利用同一个 Token 暴力请求
- Token 无法主动失效,泄露后长期有效
技术方案选型:Guid vs 自定义 Token vs JWT
1. Guid 方案(不推荐)
// 典型错误示例:用 Guid.NewGuid() 生成 Token
var token = Guid.NewGuid().ToString("N");
- 优点:实现简单
- 缺点:
- 需要额外存储校验
- 无内置过期机制
- 安全性完全依赖存储层
2. 自定义 Token 方案
// 组合时间戳 + 随机数 + 签名
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var randomPart = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
var signature = ComputeHMACSHA256($"{timestamp}:{randomPart}", secretKey);
- 优点:可控性强
- 缺点:
- 需要自行实现验证逻辑
- 容易遗漏安全环节
3. JWT 方案(推荐)
// 使用 Microsoft.IdentityModel.Tokens
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: new[] { new Claim(ClaimTypes.Name, username) },
expires: DateTime.Now.AddMinutes(30),
signingCredentials: credentials);
- 优点:
- 标准化规范(RFC 7519)
- 内置过期机制
- 自包含用户信息(Claims)
- 签名防篡改
完整 JWT 实现方案
1. 安装必要 NuGet 包
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
2. 配置 Startup.cs
// ConfigureServices 方法
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
3. Token 生成服务
public class JwtTokenService
{
private readonly IConfiguration _config;
public JwtTokenService(IConfiguration config)
{_config = config;}
public string GenerateToken(string userId, IEnumerable<Claim> customClaims = null)
{
var claims = new List<Claim>
{new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
};
if (customClaims != null)
claims.AddRange(customClaims);
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var token = new JwtSecurityToken(issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_config["Jwt:ExpireMinutes"])),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
性能优化实测数据
我们在 AWS t3.medium 实例上测试不同配置:
| 算法 | Token 长度 | QPS(每秒请求数) | CPU 占用 |
|---|---|---|---|
| HS256 | 128 字节 | 2350 | 12% |
| HS512 | 256 字节 | 1890 | 18% |
| RS256 | 512 字节 | 920 | 27% |
结论:
– 内部系统推荐 HS256
– 对公网 API 考虑 RS256(非对称加密)
– Token 长度控制在 200 字节内
必须遵守的安全纪律
- 密钥管理
- 永远不要硬编码密钥
- 使用 Azure Key Vault 或 AWS KMS 管理密钥
-
开发 / 测试 / 生产环境使用不同密钥
-
防御措施
- 启用 HTTPS 防止 Token 截获
- 设置合理的过期时间(建议 30 分钟 - 2 小时)
-
实现 Token 刷新机制
// 刷新 Token 示例 if (token.ValidTo - DateTime.UtcNow < TimeSpan.FromMinutes(5)) {var newToken = _tokenService.GenerateToken(userId); Response.Headers.Add("X-New-Token", newToken); } -
监控审计
- 记录异常的 Token 验证失败
- 监控同一 Token 的高频使用
常见坑点排查指南
-
时钟偏移问题
// 解决服务器间时间不同步 options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1); -
Claims 缺失
- 确保 Claims 命名符合规范
-
复杂对象需要序列化存储
-
跨域问题
services.AddCors(options => { options.AddPolicy("ApiPolicy", builder => {builder.WithOrigins("https://yourdomain.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // 重要!}); });
留给读者的思考题
- 如何实现动态 Token 过期时间?例如:
- 高风险操作要求短过期时间
-
内网设备可以延长有效期
-
在微服务架构下,如何设计中心化的 Token 吊销机制?
-
当用户修改密码时,如何优雅地使所有已发放 Token 失效?
希望本文能帮你构建更安全的 API 认证体系。实际开发中,安全往往需要平衡用户体验和系统性能,这需要我们持续学习和优化。
正文完
