JWT(JSON Web Token)详解:Web应用身份认证的基石

引言:为什么需要JWT?
假设你开发了一个Web应用,用户登录后,服务器如何识别”这个请求是谁发的”?
传统做法是Session:服务器存一份用户状态,每次请求带上Session ID。
但Session有个问题:服务器要存状态。用户多了,内存压力大;多节点部署,还要共享Session。
JWT(JSON Web Token)提出了一个不同的思路:把用户信息直接打包进Token里,服务器不需要存状态。
今天,我用最白话的方式,给你讲清楚JWT到底是什么、怎么用、有什么坑。
第一章:JWT长什么样?
一句话解释
JWT就是一个字符串,长得像这样:
1 2 3
| eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
三段字符串,用.连接。每段都有含义。
三段分别是什么?
1 2 3
| Header.Payload.Signature ↑ ↑ ↑ 头部 载荷 签名
|
Header(头部):说明这是什么类型的Token,用什么算法签名。
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
Payload(载荷):存放实际的数据(叫”claims”,声明)。
1 2 3 4 5 6
| { "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622 }
|
Signature(签名):用私钥对Header和Payload做签名,防止篡改。
三段都是Base64Url编码
注意:Header和Payload都是明文! 任何人拿到Token都能解码看到内容。
1 2 3 4 5 6 7
| echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" | base64 -d
|
这就是JWT最大的风险点——不要把密码、敏感信息放进Payload!
第二章:JWT的工作原理
认证流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 用户登录 颁发Token 携带Token │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────────────┐ ┌──────────────────┐ │ 前端 │───▶│ 后端验证账号密码 │───▶│ 生成JWT返回前端 │ └─────────┘ └──────────────────┘ └──────────────────┘ │ ▼ 前端存入localStorage/Cookie │ ┌────────────────────┼────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ API请求 │ │ 页面刷新 │ │ 新标签页 │ │ 带Token │ │ Token还在│ │ Token还在│ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ └───────────────────┼───────────────────┘ ▼ ┌──────────────────┐ │ 后端验证签名 │ │ 不需要查数据库 │ └──────────────────┘
|
关键区别:
- Session:每次请求,后端去数据库查Session
- JWT:每次请求,后端验证Token签名,不查数据库
签发流程(后端代码示例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const jwt = require('jsonwebtoken');
const user = { id: 1, username: 'diebug' };
const token = jwt.sign( { sub: user.id, username: user.username, role: 'user' }, 'your-256-bit-secret', { expiresIn: '1h' } );
res.json({ token });
|
验证流程(后端代码示例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function authenticate(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) { return res.status(401).json({ error: '未授权' }); }
try { const decoded = jwt.verify(token, 'your-256-bit-secret'); req.user = decoded; next(); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token已过期' }); } return res.status(401).json({ error: 'Token无效' }); } }
app.get('/api/profile', authenticate, (req, res) => { res.json({ user: req.user }); });
|
第三章:JWT的签名算法
两种签名方式
1. HMAC(对称签名)
1
| HS256 = HMAC_SHA256(base64Url(header) + "." + base64Url(payload), secret)
|
2. RSASSA(非对称签名)
1
| RS256 = RSA_SHA256(base64Url(header) + "." + base64Url(payload), privateKey)
|
- 签发用私钥,验证用公钥
- 公钥可以公开,私钥必须保密
- 适合分布式系统(多个服务验证Token)
选哪种算法?
| 场景 |
推荐算法 |
| 单体应用 |
HS256 |
| 微服务架构 |
RS256 |
| 需要密钥轮换 |
ES256 (ECDSA) |
| 生产环境 |
至少HS256或RS256 |
⚠️ 致命陷阱:alg: “none”攻击
有些JWT库允许alg: "none"——意思是不签名。
1 2 3 4
| { "alg": "none", "typ": "JWT" }
|
如果后端代码没有校验算法,攻击者可以:
- 把Header改成
alg: none
- 伪造任意Payload
- 留空Signature
结果:攻击者可以伪造任意用户的Token!
1 2 3 4 5 6 7 8 9 10 11 12
| import base64, hmac, hashlib
header = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b'=')
payload = base64.urlsafe_b64encode(b'{"sub":"1","role":"admin"}').rstrip(b'=')
token = header.decode() + "." + payload.decode() + "." print(token)
|
防御:后端必须明确指定允许的算法,拒绝none。
第四章:JWT vs Session — 该选谁?
Session的特点
1 2 3 4 5 6 7 8 9
| 优点: ✅ 服务端可控,可以随时吊销 ✅ Token可以很长,安全性高 ✅ 不需要前端存储Token
缺点: ❌ 服务端要存状态(内存/Redis) ❌ 多节点需要共享Session ❌ 跨域需要处理CSRF
|
JWT的特点
1 2 3 4 5 6 7 8 9
| 优点: ✅ 无状态,服务端不需要存Token ✅ 适合分布式/微服务架构 ✅ 天然支持跨域
缺点: ❌ Token不可撤销(除非加黑名单) ❌ Payload是明文,不能存敏感信息 ❌ Token一旦被截获,有效期内都能用
|
怎么选?
| 场景 |
推荐方案 |
| 单体应用 |
Session(简单可靠) |
| 微服务/分布式 |
JWT(无状态优势) |
| 移动端App |
JWT(跨域友好) |
| 需要强控制(随时吊销) |
Session 或 JWT + 黑名单 |
| 安全要求极高 |
OAuth2 + Refresh Token |
第五章:JWT的安全最佳实践
实践1:密钥要足够长且随机
1 2 3 4 5 6 7
| jwt.sign(payload, 'secret')
const crypto = require('crypto'); const secret = crypto.randomBytes(64).toString('hex'); jwt.sign(payload, secret)
|
密钥长度建议:至少256位(32字节)
实践2:设置合理的过期时间
1 2 3 4 5 6 7 8
| jwt.sign(payload, secret)
jwt.sign(payload, secret, { expiresIn: '15m' })
jwt.sign(refreshPayload, secret, { expiresIn: '7d' })
|
建议:Access Token短期(15分钟),Refresh Token长期(7-30天)
实践3:不要把敏感信息放进Payload
1 2 3 4 5 6 7 8
| jwt.sign({ password: '123456' }, secret)
jwt.sign({ idCard: '110101199001011234' }, secret)
jwt.sign({ sub: userId, role: 'admin' }, secret)
|
记住:Payload是Base64编码的,不是加密的!任何人都能解码看到。
实践4:验证时检查所有字段
1 2 3 4 5 6
| jwt.verify(token, secret, { algorithms: ['HS256'], issuer: 'https://your-domain.com', audience: 'your-app' })
|
实践5:实现Token吊销机制
JWT本身是无状态的,但生产环境需要能吊销Token。
方案1:黑名单(最简单)
1 2 3 4 5 6 7 8 9 10 11 12 13
| const revokedTokens = new Set();
function revokeToken(token) { revokedTokens.add(token); }
function verifyToken(token) { const decoded = jwt.verify(token, secret); if (revokedTokens.has(token)) { throw new Error('Token已吊销'); } return decoded; }
|
方案2:短寿命Token + 数据库状态(推荐)
实践6:使用HTTPS
1 2
| ❌ HTTP + JWT = Token明文传输,中间人可直接截获 ✅ HTTPS + JWT = Token传输加密
|
没有HTTPS,JWT形同虚设。
实践7:Secure + HttpOnly Cookie存储
1 2 3 4 5 6 7
| res.cookie('token', jwtToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 * 60 * 1000 });
|
第六章:JWT常见漏洞
漏洞1:密钥硬编码
1 2 3 4 5
| const secret = 'my-secret-key-123'; jwt.sign(payload, secret);
|
修复:密钥放到环境变量或密钥管理服务
1 2
| const secret = process.env.JWT_SECRET; if (!secret) throw new Error('JWT_SECRET未配置');
|
漏洞2:算法混淆攻击
1 2 3 4 5
| jwt.verify(token, publicKey);
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
|
漏洞3:过长的Token
1 2 3 4 5 6 7 8 9 10
| jwt.sign({ id: 1, name: 'John', email: 'john@example.com', roles: ['admin', 'user', 'editor'], }, secret);
|
修复:Payload只放最小必要信息,其他数据从数据库获取
漏洞4:不检查过期时间
1 2 3 4 5
| const decoded = jwt.decode(token);
const decoded = jwt.verify(token, secret);
|
jwt.decode()只解码不验证,jwt.verify()才会检查签名和过期时间。
第七章:JWT在其他场景的应用
OAuth2中的JWT
OAuth2的Access Token可以是JWT格式:
1
| Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
好处:资源服务器可以直接验证签名,不需要每次回Authorization Server查询。
Refresh Token模式
1 2 3 4 5 6 7 8 9 10 11 12 13
| ┌─────────────────────────────────────────────┐ │ Access Token (短期,15分钟) │ │ - 每次API请求携带 │ │ - 过期后无法使用 │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Refresh Token (长期,7天) │ │ - 存在HttpOnly Cookie中 │ │ - 用于换取新的Access Token │ │ - 不携带在API请求中 │ └─────────────────────────────────────────────┘
|
好处:Access Token泄露后,攻击者只能用15分钟;Refresh Token在Cookie里,XSS偷不到。
总结
JWT的核心要点
- JWT = Header.Payload.Signature,三段Base64Url编码
- Payload是明文,不要放敏感信息
- 签名防止篡改,验证时用
verify而非decode
- 设置合理的过期时间,Access Token短期,Refresh Token长期
- 密钥要足够长且随机,不要硬编码
- 限制算法,防止
none和算法混淆攻击
- 搭配HTTPS和Secure Cookie,形成完整的安全链条
一句话总结
JWT是无状态认证的利器,但它不是银弹。正确使用JWT,需要理解它的结构、知道它的风险、掌握最佳实践。
延伸阅读