1. 引言
HTTP 协议本质上是无状态(Stateless)的。在早期的单体应用时代,为了识别用户身份,我们通常依赖 Session-Cookie 机制:服务端在内存或数据库中存储 Session 数据,客户端浏览器通过 Cookie 携带 Session ID。
然而,随着微服务架构和分布式系统的兴起,这种有状态(Stateful)的机制暴露出了明显的弊端:Session 数据需要在集群节点间同步(Session Sticky 或 Session Replication),这极大地限制了系统的水平扩展能力(Horizontal Scaling)。
为了解决这一痛点,JSON Web Token(JWT)应运而生。作为一种轻量级、自包含的身份验证标准,JWT 已成为现代 Web 应用------特别是前后端分离架构与微服务架构中------主流的身份认证解决方案。本文将从原理剖析、NestJS 实战、架构权衡及高频面试考点四个维度,带你全面深入理解 JWT。
2. 什么是 JWT
JWT(JSON Web Token)是基于开放标准 RFC 7519 定义的一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。
核心特性:
- 紧凑(Compact) :体积小,可以通过 URL 参数、POST 参数或 HTTP Header 发送。
- 自包含(Self-contained) :Payload 中包含了用户认证所需的所有信息,避免了多次查询数据库。
主要应用场景:
- 身份认证(Authorization) :这是最常见的使用场景。一旦用户登录,后续请求将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。
- 信息交换(Information Exchange) :利用签名机制,确保发送者的身份是合法的,且传输的内容未被篡改。
3. JWT 的解剖学:原理详解
一个标准的 JWT 字符串由三部分组成,通过点(.)分隔:Header(请求头).Payload(载荷).Signature(签名信息)。
3.1 Header(头部)
Header 通常包含两部分信息:令牌的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256 或 RSA),一般会有多种算法,如果开发者无选择,那么默认是HMAC SHA256算法。
JSON
json
{
"alg": "HS256",
"typ": "JWT"
}
该 JSON 被 Base64Url 编码后,构成 JWT 的第一部分。
3.2 Payload(负载)
Payload 包含声明(Claims),即关于实体(通常是用户)和其他数据的声明。声明分为三类:
-
Registered Claims(注册声明) :一组预定义的、建议使用的权利声明,如:
- iss (Issuer): 签发者
- exp (Expiration Time): 过期时间
- sub (Subject): 主题(通常是用户ID)
- aud (Audience): 受众
-
Public Claims(公共声明) :可以由使用 JWT 的人随意定义。
-
Private Claims(私有声明) :用于在同意使用这些定义的各方之间共享信息,如 userId、role 等。
架构师警示:
Payload 仅仅是进行了 Base64Url 编码(Encoding) ,而非 加密(Encryption) 。
这意味着,任何截获 Token 的人都可以通过 Base64 解码看到 Payload 中的明文内容 。因此,严禁在 Payload 中存储密码、手机号等敏感信息。
3.3 Signature(签名)
签名是 JWT 安全性的核心。它是对前两部分(编码后的 Header 和 Payload)进行签名,以防止数据被篡改。
生成签名的公式如下(以 HMAC SHA256 为例):
Code
scss
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
原理解析:
服务端持有一个密钥(Secret),该密钥绝不能泄露给客户端。当服务端收到 Token 时,会使用同样的算法和密钥重新计算签名。如果计算出的签名与 Token 中的 Signature 一致,说明 Token 是由合法的服务端签发,且 Payload 中的内容未被篡改(完整性校验)。
4. 实战:基于 NestJS 实现 JWT 认证
NestJS 是 Node.js 生态中优秀的企业级框架。下面演示如何使用 @nestjs/jwt 和 @nestjs/passport 实现标准的 JWT 认证流程。
4.1 依赖安装
Bash
scss
npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
4.2 Module 配置
在 AuthModule 中注册 JwtModule,配置密钥和过期时间。
TypeScript
typescript
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'YOUR_SECRET_KEY', // 生产环境请使用环境变量
signOptions: { expiresIn: '60m' }, // Token 有效期
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
4.3 Service 层:签发 Token
实现登录逻辑,验证用户信息通过后,生成 JWT。
TypeScript
typescript
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
4.4 Strategy 实现:解析 Token
编写策略类,用于解析请求头中的 Bearer Token 并进行验证。
TypeScript
typescript
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, // 拒绝过期 Token
secretOrKey: 'YOUR_SECRET_KEY', // 需与 Module 中配置一致
});
}
async validate(payload: any) {
// passport 会自动把返回值注入到 request.user 中
return { userId: payload.sub, username: payload.username };
}
}
4.5 Controller 使用:路由保护
TypeScript
less
// app.controller.ts
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('profile')
export class ProfileController {
@UseGuards(AuthGuard('jwt'))
@Get()
getProfile(@Request() req) {
return req.user; // 这里是通过 JwtStrategy.validate 返回的数据
}
}
5. 深度分析:JWT 的优缺点与架构权衡
优点
- 无状态与水平扩展(Stateless & Scalability) :服务端不需要存储 Session 信息,完全消除了 Session 同步问题,非常适合微服务和分布式架构。
- 跨域友好:不依赖 Cookie(尽管可以结合 Cookie 使用),在 CORS 场景下处理更为简单,且天然适配移动端(iOS/Android)开发。
- 性能:在不涉及黑名单机制的前提下,验证 Token 只需要 CPU 计算签名,无需查询数据库,减少了 I/O 开销。
缺点与挑战
- 令牌体积:JWT 包含了 Payload 信息,相比仅存储 ID 的 Cookie,其体积更大,这会增加每次 HTTP 请求的 Header 大小,影响流量。
- 撤销难题(Revocation) :这是 JWT 最大 的痛点。JWT 一旦签发,在有效期内始终有效。服务端无法像 Session 那样直接删除服务器端数据来强制用户下线。
6. 面试高频考点与解决方案(进阶)
在面试中,仅仅展示如何生成 JWT 是远远不够的,面试官更关注安全性与工程化挑战。
问题 1:JWT 安全吗?如何防范攻击?
-
XSS(跨站脚本攻击) :如果将 JWT 存储在 localStorage 或 sessionStorage,恶意 JS 脚本可以轻松读取 Token。
- 解决方案:建议将 Token 存储在标记为 HttpOnly 的 Cookie 中,这样 JS 无法读取。
-
CSRF(跨站请求伪造) :如果使用 Cookie 存储 Token,则会面临 CSRF 风险。
- 解决方案:使用 SameSite=Strict 属性,或配合 CSRF Token 防御。如果坚持存储在 localStorage 并通过 Authorization Header 发送,则天然免疫 CSRF,但需重点防范 XSS。
-
中间人攻击:由于 Header 和 Payload 是明文编码。
- 解决方案:必须强制全站使用 HTTPS。
问题 2:如何实现注销(Logout)或强制下线?
既然 JWT 是无状态的,如何实现"踢人下线"?这实际上是无状态 与管控性之间的权衡。
-
方案 A:黑名单机制(Blacklist)
- 将用户注销或被封禁的 Token ID (jti) 存入 Redis,设置过期时间等于 Token 的剩余有效期。
- 每次请求验证时,先校验签名,再查询 Redis 是否在黑名单中。
- 权衡:牺牲了部分"无状态"优势(引入了 Redis 查询),但获得了即时的安全管控。
-
方案 B:版本号/时间戳控制
- 在 JWT Payload 中加入 token_version。
- 在数据库用户表中也存储一个 token_version。
- 当用户修改密码或注销时,增加数据库中的版本号。
- 权衡:每次验证都需要查询数据库比对版本号,退化回了 Session 的模式,性能开销大。
问题 3:Token 续签(Refresh Token)机制是如何设计的?
为了解决 JWT 有效期过长不安全、过短体验差的问题,业界标准做法是 双 Token 机制:
- Access Token:有效期短(如 15 分钟),用于访问业务接口。
- Refresh Token :有效期长(如 7 天),仅用于换取新的 Access Token。
流程设计:
- 客户端请求接口,若 Access Token 过期,服务端返回 401。
- 客户端捕获 401,携带 Refresh Token 请求 /refresh 接口。
- 服务端验证 Refresh Token 合法(且未在黑名单/数据库中被禁用),签发新的 Access Token。
- 关键点:Refresh Token 通常需要在服务端(数据库)持久化存储,以便管理员可以随时禁用某个 Refresh Token,从而间接实现"撤销"用户的登录状态。
7. 结语
JWT 并不是银弹。它通过牺牲一定的"可控性"换取了"无状态"和"扩展性"。
在架构选型时:
- 如果你的应用是小型单体,且对即时注销要求极高,传统的 Session 模式可能更简单有效。
- 如果你的应用是微服务架构 ,或者需要支持多端登录,JWT 是不二之选。
- 在构建企业级应用 时,切勿盲目追求纯粹的无状态。推荐使用 JWT + Access/Refresh Token 双令牌 + Redis 黑名单 的组合拳,以在安全性、性能和扩展性之间取得最佳平衡。