JWT 原理:从结构到实战
JWT 这东西,说白了就是一段自包含的令牌。我刚开始接触它的时候,觉得不就是个加密字符串嘛,有啥好研究的?后来在项目中踩了几次坑,才真正理解它的设计精妙之处。
咱们先看它的全称——JSON Web Token。注意这个"Web",它天生就是为了在 HTTP 这种无状态协议里传递身份信息而生的。你想想看,每次请求都要查 Session,服务器压力多大?JWT 的思路就是:把用户信息直接塞进令牌里,服务器拿到后自己验证就行,不用查数据库。
JWT 结构:三段式
一个 JWT 长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
看到那两个点了吗?它把令牌分成了三部分:Header、Payload、Signature。每部分都是 Base64URL 编码的 JSON。
核心要点:JWT 的三段结构不是随便设计的。Header 告诉你怎么签的,Payload 存实际数据,Signature 保证没人篡改。缺一不可。
Header(头部)
Header 通常包含两个字段:令牌类型(typ)和签名算法(alg)。
{
"alg": "HS256",
"typ": "JWT"
}
alg 字段很关键。我见过有人把 alg 设成 "none",结果服务端没校验,直接放行了所有伪造令牌。嗯,这属于低级错误,但确实发生过。
Payload(载荷)
Payload 才是真正存数据的地方。它分三种声明:
- 注册声明(Registered Claims):预定义的标准化字段,比如 iss(签发者)、sub(主题)、exp(过期时间)、iat(签发时间)。
- 公共声明(Public Claims):自定义字段,但建议在 IANA 注册或使用命名空间避免冲突。
- 私有声明(Private Claims):双方约定的自定义字段,比如 userId、role。
举个例子:
{
"sub": "user_12345",
"name": "张三",
"role": "admin",
"iat": 1700000000,
"exp": 1700003600
}
注意:Payload 只是 Base64 编码,不是加密的!任何人都能解码看到里面的内容。千万别把密码、信用卡号这类敏感信息放进去。我曾经在一个项目里接手过遗留代码,发现他们把用户手机号直接塞进 Payload……赶紧改掉。
Signature(签名)
签名是 JWT 防篡改的核心。它的生成过程:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
说白了,就是把前两段拼起来,用密钥算个哈希。只要密钥不泄露,别人改了一丁点数据,签名就对不上了。
JWT 签发与验证
咱们用 Node.js 的 jsonwebtoken 库演示一下,这样更直观。
签发令牌
const jwt = require('jsonwebtoken');
const payload = {
userId: 'u_1001',
role: 'editor'
};
const secret = 'my-secret-key-2024';
const token = jwt.sign(payload, secret, {
expiresIn: '2h',
issuer: 'my-app'
});
console.log(token);
// 输出:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1XzEwMDEiLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDcyMDB9.xxxxx
我个人习惯把过期时间设短一点,比如 1-2 小时。太长的话,令牌泄露风险大;太短的话,用户老得重新登录,体验不好。
验证令牌
try {
const decoded = jwt.verify(token, secret, {
issuer: 'my-app'
});
console.log(decoded.userId); // u_1001
} catch (err) {
// 过期、签名错误、发行者不匹配都会抛异常
console.error('Token invalid:', err.message);
}
验证的时候,库会自动检查 exp 是否过期、iss 是否匹配。你只需要保证密钥一致就行。
避坑指南:我曾经在生产环境遇到过一个问题——签发和验证用的密钥不一致。原因是配置中心更新了密钥,但服务没重启。后来我改用密钥轮换机制,旧密钥保留一个缓冲期,新密钥逐步切换,才彻底解决。
JWT vs Session:怎么选?
很多新手会问:到底用 JWT 还是 Session?我的答案是:看场景。没有银弹。
| 对比维度 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端(浏览器/App) | 服务端(内存/Redis/数据库) |
| 扩展性 | 天然支持分布式,无共享存储 | 需要共享 Session 存储(如 Redis) |
| 安全性 | Payload 可解码,敏感信息需加密 | 数据在服务端,相对安全 |
| 吊销能力 | 难!签发后无法主动失效(除非维护黑名单) | 容易,直接删 Session 即可 |
| 性能 | 服务端无状态,验证快(只需计算签名) | 每次请求查存储,有 I/O 开销 |
| 适用场景 | 微服务、移动端、单点登录(SSO) | 传统 Web 应用、需要即时吊销的场景 |
我个人的经验是:内部系统、微服务架构、移动端 API 优先用 JWT。因为它无状态,服务随便扩缩容,不用操心 Session 同步。但如果是金融、政务这类对安全要求极高、需要随时踢人下线的场景,Session 更合适——毕竟 JWT 一旦签发,在过期前你拿它没办法。
我的建议:别把 JWT 和 Session 对立起来。很多项目是混合用的——JWT 做 API 鉴权,Session 做 Web 页面登录态。各取所长,才是正道。
JWT 完整流程
下面这张图展示了 JWT 从签发到验证的完整链路:
流程其实就三步:
- 用户登录,认证服务验证身份后签发 JWT
- 客户端存下 JWT(通常放 localStorage 或 Cookie)
- 后续请求带上 JWT,业务服务验证签名和过期时间
你看,业务服务根本不需要查数据库。这就是 JWT 最大的优势——无状态。服务随便重启、随便扩容,令牌照样能用。
小技巧:JWT 的 exp 字段一定要设。我见过有人不设过期时间,结果令牌泄露后永久有效……那场面,简直灾难。另外,iat 字段也建议加上,方便排查问题。