.NET
JWT 身份验证:完整实现指南
为现代 Web API 和应用程序实现安全身份验证是一项关键挑战。JSON Web 令牌(JWT)已成为无状态身份验证的行业标准解决方案,使服务器无需维护服务器端会话状态即可验证用户身份。通过本指南,你将全面了解基于 JWT 的身份验证架构,能够在自己的应用程序中安全地实现令牌签发和验证,并掌握一套可用于生产环境的令牌生命周期管理策略。你将获得在任何技术栈中安全实现 JWT 身份验证的实用知识。
你将学到什么
你将理解 JWT 的架构工作原理,明白正确实现为何对安全性至关重要,并掌握配置身份验证中间件、签发令牌以及保护 API 端点的实践知识。最终,你将能够自信地在自己的项目中实现 JWT 身份验证,并确保所做的安全决策正确无误。
理解 JWT 身份验证架构
在深入代码之前,理解 JWT 身份验证的工作原理至关重要。JSON Web 令牌是一种紧凑、URL 安全的表示双方之间声明的方式。令牌由三个 Base64URL 编码的部分组成,各部分之间用点分隔:头部、载荷和签名(header.payload.signature)。
JWT 身份验证流程遵循清晰的模式:客户端进行身份验证(通常通过登录端点),接收签名的令牌,然后在后续请求的 Authorization 头部中包含该令牌。服务器在每个请求上验证令牌的签名和声明,使系统成为无状态系统——无需服务器端会话存储。
JWT 的关键组成部分
| 组成部分 | 用途 | 示例 |
|---|---|---|
| 头部 | 指定签名算法和令牌类型 | {"alg":"RS256","typ":"JWT"} |
| 载荷 | 包含声明(用户数据、过期时间、签发者) | {"sub":"user123","exp":1700000000} |
| 签名 | 确保令牌完整性和真实性 | 使用密钥对头部+载荷进行加密哈希 |
这种设计使 JWT 成为分布式系统和微服务的理想选择,因为在这些场景中维护共享会话状态是不切实际的。
如何实现 JWT 身份验证:分步指南
以下步骤概述了一种可用于生产环境的 JWT 身份验证实现方法。虽然示例使用 .NET 和 Node.js,但这些原则适用于所有编程语言。
步骤 1:安装所需的身份验证包
现代框架提供了官方的 JWT Bearer 身份验证处理程序。对于 .NET,安装 Microsoft.AspNetCore.Authentication.JwtBearer 包。对于 Node.js,使用 jsonwebtoken。
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
# Node.js
npm install jsonwebtoken bcryptjs dotenv
依赖官方且经过实战检验的库至关重要——切勿自行实现加密签名逻辑。
步骤 2:配置身份验证服务
身份验证配置定义了 API 如何验证传入的令牌。关键的验证参数包括:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 验证令牌来自已知的颁发机构
ValidateAudience = true, // 验证令牌是为该 API 签发的
ValidateLifetime = true, // 自动拒绝过期令牌
ValidateIssuerSigningKey = true, // 验证加密签名
ValidIssuer = "https://your-identity-provider.com",
ValidAudience = "https://your-api.com",
IssuerSigningKey = publicKey // 用于签名验证的密钥
};
});
在生产环境中,所有四个验证标志都应设置为 true。禁用其中任何一个都会造成安全漏洞。IssuerSigningKey 可以从 PEM 文件加载,或者在生产环境中从身份提供者的元数据端点自动获取。
步骤 3:添加身份验证和授权中间件
中间件组件的顺序至关重要——身份验证必须在授权之前运行:
app.UseAuthentication(); // 验证 JWT 并填充 HttpContext.User
app.UseAuthorization(); // 对受保护端点强制执行 [Authorize] 策略
当请求到达 UseAuthentication 时,处理程序会检查 Authorization 头部,验证 JWT,并创建一个代表已认证用户的 ClaimsPrincipal 对象。该对象随后在整个请求管道中可用于授权决策。
步骤 4:使用 [Authorize] 属性保护端点
保护 API 端点需要使用 [Authorize] 属性:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
[HttpGet]
public IActionResult GetSecureData()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Ok($"已认证用户:{userId}");
}
}
没有有效令牌的请求将收到 401 Unauthorized 响应。当身份验证成功但缺乏所需权限时,API 返回 403 Forbidden。
步骤 5:实现令牌签发(仅限开发环境)
在生产环境中,令牌签发应归属于专门的 Identity Provider(IdP),实现 OAuth 2.0 或 OpenID Connect。API 的角色仅限于验证。
然而,对于开发和测试,你可能需要一个本地令牌端点:
// Node.js 示例,使用 jsonwebtoken
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: '无效的凭据' });
}
const token = jwt.sign(
{ id: user._id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // 短生命周期的访问令牌
);
res.json({ token });
});
访问令牌应具有短生命周期——通常为 15 分钟——以限制令牌被盗带来的损害。
生产环境安全最佳实践
切勿将令牌存储在 LocalStorage 中
对于 Web 应用程序,将 JWT 存储在 HttpOnly、Secure cookie 中,而不是 localStorage 或 sessionStorage。这可以防止跨站脚本攻击(XSS)访问令牌。Cookie 配置应包括:
HttpOnly: true // 阻止 JavaScript 访问
Secure: true // 仅通过 HTTPS 发送
SameSite: Lax 或 Strict // CSRF 保护
使用刷新令牌轮换
由于访问令牌过期较快,请实现刷新令牌机制,以便在不要求重新认证的情况下维持用户会话:
- 刷新令牌:生命周期较长(例如 7 天),安全存储在服务器上
- 访问令牌:生命周期较短(例如 15 分钟),随每个请求发送
- 轮换:每次刷新请求都会签发新的访问+刷新令牌对,并使旧的刷新令牌失效
这种方法可以减轻令牌被盗的影响,并实现干净的注销功能。
在生产环境中使用非对称签名(RS256/ES256)
在生产环境中使用非对称算法(RS256 或 ES256),而不是 HMAC(HS256):
| 算法 | 密钥类型 | 使用场景 |
|---|---|---|
| HS256 | 对称(共享密钥) | 单一服务应用程序 |
| RS256/ES256 | 非对称(公钥/私钥) | 多服务系统、微服务 |
非对称签名允许多个服务使用公钥验证令牌,而只有签发者持有私钥——遵循最小权限原则。
验证所有标准声明
OAuth 2.0 访问令牌需要以下声明,并且必须进行验证:
iss(签发者)—— 与预期的颁发机构匹配aud(受众)—— 与你的 API 标识符匹配exp(过期时间)—— 令牌未过期sub(主题)—— 用户标识符iat(签发时间)—— 令牌是在过去签发的jti(JWT ID)—— 用于令牌跟踪的唯一标识符
使用 Redis 实现无状态身份验证
虽然 JWT 本质上是无状态的,但实现令牌撤销需要一个存储层。Redis 提供了理想的解决方案:
// 存储令牌元数据以用于撤销
func (r *Redis) SetJTI(ctx context.Context, key, userID string, exp time.Time) error {
return r.Client.Set(ctx, key, userID, time.Until(exp)).Err()
}
// 检查令牌是否被撤销
func (r *Redis) GetUserByJTI(ctx context.Context, key string) (string, error) {
return r.Client.Get(ctx, key).Result()
}
将令牌的唯一标识符(jti)存储在 Redis 中,并设置与令牌过期时间匹配的 TTL。这可以实现即时注销,并防止被泄露的令牌被重复使用。
应避免的常见 JWT 实现错误
在生产环境中自行签发令牌:API 应验证令牌,而不是签发令牌。使用专门的 IdP 进行签发。
弱签名密钥:使用通过
openssl rand -base64 32生成的加密强度高的密钥(至少 32 字节)。禁用声明验证:切勿在生产环境中将
ValidateIssuer或ValidateAudience设置为false。令牌生命周期过长:访问令牌应在 15 分钟内过期。生命周期越长,攻击面越大。
缺少撤销机制:使用 Redis 或类似的存储实现注销和令牌撤销。
常见问题解答
JWT 身份验证和 OAuth 2.0 有什么区别? JWT 是一种令牌格式,而 OAuth 2.0 是一个授权框架。OAuth 2.0 定义了如何获取令牌(包括 JWT),而 JWT 定义了令牌本身的结构。它们是互补的——OAuth 2.0 可以使用 JWT 作为访问令牌,但在较小的系统中,你也可以独立使用 JWT 进行身份验证。
如何在客户端安全地存储 JWT? 将 JWT 存储在 HttpOnly、Secure cookie 中,而不是 localStorage 或 sessionStorage。HttpOnly cookie 可阻止 JavaScript 访问,从而防止 XSS 攻击。对于移动应用程序,请使用安全的平台特定存储机制。
我可以在 JWT 过期之前撤销它吗? 可以,通过维护一个令牌标识符(jti 声明)的撤销列表或存储。Redis 是理想的选择——将 jti 与令牌过期时间匹配的 TTL 一起存储。对于每个请求,检查 jti 是否存在于撤销存储中。这可以实现即时注销和令牌失效。
我应该使用 HS256 还是 RS256 来签名 JWT? 在生产环境的多服务场景中,使用 RS256(非对称),因为令牌签发和验证发生在不同的系统上。仅在可以安全共享相同密钥的单一服务应用程序中使用 HS256。非对称签名允许验证服务仅使用公钥,遵循最小权限原则。
如何安全地实现刷新令牌?
在短生命周期的访问令牌(15 分钟)旁边签发一个长生命周期的刷新令牌(7 天)。提供一个 /refresh 端点,该端点接受刷新令牌,验证它,并返回一个新的访问+刷新令牌对。每次使用时轮换刷新令牌,以防止令牌重放攻击。安全地存储刷新令牌(例如在 Redis 中),并在注销时使其失效。
来源
- Duende Software. "A step by step JWT authentication example." Duende Software. 2025.
- Microsoft Learn. "Configure JWT bearer authentication in ASP.NET Core." Microsoft. 2025.
- Duende Software. "Implementing Token Authentication in Controller-Based ASP.NET Core Web APIs." Duende Software. 2026.
- QuickNode. "How to Implement JSON Web Tokens (JWT) Authorization." QuickNode. 2025.
- freeCodeCamp. "How to Build a Secure Authentication System with JWT and Refresh Tokens." freeCodeCamp. 2025.
- Vonage. "JWT Authentication in Go with Gin." Vonage Developer. 2025.
— Editorial Team
暂无评论。