今天我们要聊的 JWT(JSON Web Token),就是给你的 API 装上一把智能锁,并且配上一张“网络身份证”。学完这篇,你就能轻松实现登录认证、接口保护这些真实项目必备的功能。
一、🤔 JWT 到底是什么?先看“长相”
在开始之前,我们先来认识一下 JWT 的“长相”。一个典型的 JWT 长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
是不是看着像一串乱码?别慌,它其实是有规律的——由两个点号(.)分成三段 :
- 第一部分:头部(Header)
- 第二部分:载荷(Payload)
- 第三部分:签名(Signature)
你可以把 JWT 想象成一张智能门禁卡:
- 卡上印着你的名字和照片(载荷)
- 有防伪水印,一撕就坏(签名)
- 卡上有有效期,过期自动失效(过期时间)
二、🎯 为什么我们需要 JWT?
在 JWT 出现之前,传统的 Session 认证方式是:
- 用户登录成功,服务器在内存/数据库里存一份 Session 数据
- 服务器返回一个 Session ID 给客户端(存在 Cookie 里)
- 下次请求客户端带上这个 ID,服务器去查 Session 数据
这样做的痛点很明显 :
- 服务器有状态:如果部署多台服务器,Session 数据必须共享(不然用户可能在A服务器登录了,请求打到B服务器就不认识了)
- 扩展性差:用户量一大,Session 存储就成了瓶颈
- 不适合移动端:App 里处理 Cookie 比较麻烦
JWT 的方案完全不同 :
- 服务器不存储任何 session 数据
- 用户信息直接编码在 JWT 里,服务器只要验证签名就知道你是谁
- 服务器可以轻松横向扩展——任何一台服务器只要拿到公钥/密钥,都能验证 JWT
这就是所谓的 “无状态认证”——服务器不需要记住你,你每次来都自己带着身份证。
三、🧩 JWT 的三段式结构(简单版)
3.1 头部(Header)
头部是个 JSON 对象,通常长这样 :
{
"alg": "HS256", // 签名算法,常见的有 HS256、RS256
"typ": "JWT" // 令牌类型
}
3.2 载荷(Payload)
载荷是 JWT 的主体,里面放着你想要传递的信息 :
{
"sub": "1234567890", // 标准声明:用户ID
"name": "张三", // 自定义声明:用户名
"role": "admin", // 自定义声明:角色
"iat": 1516239022, // 签发时间
"exp": 1516242622 // 过期时间
}
⚠️ 重要提醒:载荷只是 Base64Url 编码,不是加密!任何人都可以解码看到里面的内容 。所以绝对不要在载荷里放密码、密钥等敏感信息。
3.3 签名(Signature)
签名是 JWT 的防伪标识,它的生成公式是 :
签名 = 算法( Base64(Header) + "." + Base64(Payload), 密钥 )
验证方收到 JWT 后,会用同样的方式重新计算签名,如果结果一致,说明令牌没有被篡改过 。
四、🚀 JWT 的完整工作流程
咱们用一个真实的场景来理解 JWT 是怎么“旅行”的 :
第1步:用户登录(获取令牌)
sequenceDiagram
participant 客户端
participant 认证服务器
客户端->>认证服务器: POST /login (用户名+密码)
认证服务器->>认证服务器: 验证用户身份
认证服务器->>客户端: 返回 JWT 令牌
第2步:客户端存储令牌
客户端收到 JWT 后,通常存储在:
- Web 端:localStorage 或内存中
- App 端:安全存储区
第3步:访问受保护资源
sequenceDiagram
participant 客户端
participant 资源服务器
客户端->>资源服务器: GET /api/users (Header: Authorization: Bearer <JWT>)
资源服务器->>资源服务器: 验证签名、检查过期时间
资源服务器->>客户端: 返回用户数据
注意这里的 Header 格式是固定的 :
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
五、💻 实战:用 Node.js 实现 JWT 认证
理论说了这么多,咱们直接上手写代码!这里我们用最常见的 jsonwebtoken 库 。
5.1 安装依赖
npm install jsonwebtoken
# TypeScript 用户还可以装类型定义
npm install @types/jsonwebtoken -D
5.2 生成令牌(登录接口)
const jwt = require('jsonwebtoken');
// 登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 1. 验证用户名密码(实际项目会查数据库)
if (username === 'admin' && password === '123456') {
// 2. 定义要放在 JWT 里的用户信息
const payload = {
userId: 1001,
username: 'admin',
role: 'admin'
};
// 3. 生成 JWT(设置1小时过期)
const secretKey = 'your-secret-key'; // 实际应该从环境变量读取
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
// 4. 返回令牌
res.json({
success: true,
token: token,
expiresIn: 3600
});
} else {
res.status(401).json({ success: false, message: '用户名或密码错误' });
}
});
5.3 验证令牌(中间件)
function authenticateToken(req, res, next) {
// 1. 从 Header 里取出 token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: '未提供认证令牌' });
}
// 2. 验证 token
const secretKey = 'your-secret-key';
jwt.verify(token, secretKey, (err, user) => {
if (err) {
// 判断具体错误类型
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ message: '令牌已过期' });
}
return res.status(403).json({ message: '无效令牌' });
}
// 3. 把用户信息挂到请求对象上,后续中间件可以用
req.user = user;
next();
});
}
5.4 保护接口
// 公开接口,不需要认证
app.get('/public', (req, res) => {
res.json({ message: '这是公开数据' });
});
// 受保护接口,需要 JWT
app.get('/api/users', authenticateToken, (req, res) => {
// 这里可以拿到上面挂载的 req.user
console.log('当前用户:', req.user);
res.json({
message: '这是受保护的用户数据',
user: req.user
});
});
5.5 刷新令牌机制
实际项目中,access token 过期时间通常很短(比如15分钟),同时会发放一个 refresh token(刷新令牌),用来在 access token 过期后获取新的 access token 。
// 生成 access token 和 refresh token
function generateTokens(userId) {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// 刷新 token 接口
app.post('/refresh-token', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ message: '请提供 refresh token' });
}
// 验证 refresh token
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: '无效的 refresh token' });
}
// 生成新的 access token
const newAccessToken = jwt.sign(
{ userId: user.userId, type: 'access' },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
});
});
六、🔒 安全使用 JWT 的六大黄金法则
JWT 虽然好用,但用不好就会变成“豆腐渣工程”。下面这些安全建议来自 IETF 的官方最佳实践 :
⚠️ 法则1:严禁在 Payload 里存敏感信息
记住:Payload 是明文编码,不是加密!任何人都可以轻松解码 。
// ❌ 错误示范
const payload = {
userId: 1001,
password: '123456', // 千万别!
creditCard: '4111111111111111' // 更不行!
};
// ✅ 正确做法:只放非敏感信息
const payload = {
userId: 1001,
role: 'user',
sessionId: 'xxx' // 如果需要敏感数据,存个 ID 让服务器去查
};
⚠️ 法则2:强制算法白名单,防“算法混淆攻击”
经典攻击手法:把 RS256(非对称)改成 HS256(对称),然后用公钥作为密钥验证——直接绕过签名验证!
// ✅ 安全做法:明确指定允许的算法
jwt.verify(token, secretOrPublicKey, {
algorithms: ['RS256', 'ES256'] // 只允许这些算法
});
⚠️ 法则3:必须验证所有标准声明
不要只看签名!exp(过期时间)、nbf(生效时间)、aud(受众)、iss(签发者)这些都要验证 。
jwt.verify(token, secretKey, {
algorithms: ['HS256'],
audience: 'my-api', // 这个 token 是给我的吗?
issuer: 'auth-server', // 是可信的签发者发的吗?
maxAge: '1h' // 最多能接受多老的 token?
});
⚠️ 法则4:选择正确的算法(HS256 vs RS256)
- HS256(对称加密):同一个密钥签名和验证
- 优点:性能好,实现简单
- 缺点:验证方也要知道密钥,不适合分布式系统
- RS256(非对称加密):私钥签名,公钥验证
- 优点:验证方只需要公钥,更安全;可以集中管理密钥
- 推荐用于微服务/分布式架构
IETF 推荐算法:RS256 和 ES256 是首选 。
⚠️ 法则5:定期轮转密钥
长期使用同一密钥会增加泄露风险。生产环境应该支持 密钥轮转 :
// 通过 JWKS (JSON Web Key Set) 端点动态获取公钥
// 网关每5分钟拉取一次,新旧公钥并存,无缝轮转
const jwksUri = 'https://auth.example.com/.well-known/jwks.json';
⚠️ 法则6:设置短生命周期的令牌
- Access token:15分钟到2小时
- Refresh token:7天到30天,且应该可以撤销
// 敏感操作还要额外验证
if (req.user.role !== 'admin' && req.method !== 'GET') {
return res.status(403).json({ message: '需要管理员权限' });
}
七、🎯 常见场景速查表
| 场景 | 建议 |
|---|---|
| 单页应用(SPA) | JWT 存内存,刷新 token 存 httpOnly cookie |
| 移动 App | JWT 存安全存储区,用 refresh token 轮换 |
| 微服务 | 用 RS256,所有服务共享公钥,独立验证 |
| 第三方 API | 遵循 RFC 9068 规范,JWT 格式的 Access Token |
| 单点登录(SSO) | 用 JWT 传递用户信息,各子系统独立验证 |
八、📝 总结:一张“智能身份证”的自白
回顾一下 JWT 的“奇妙旅行”:
- 我是谁:一个三段式字符串,包含头部、载荷、签名
- 我干什么:让服务器不再存储 session,实现无状态认证
- 我怎么工作:登录时颁发,后续请求放在 Authorization Header 里
- 怎么用我安全:别放敏感信息,算法白名单,验证所有声明,定期轮转密钥
JWT 不是万能的,但在绝大多数 API 认证场景中,它都是简单又强大的选择。等你真正动手写几个接口,把 JWT 加进去保护起来,就会感受到那种“锁上门、发钥匙”的掌控感。