🎫 JWT 令牌完全理解
📖 定义
JWT(JSON Web Token)是用于在客户端和服务器之间安全传输信息的基于JSON的令牌。由三部分(Header、Payload、Signature)组成,由服务器颁发,客户端在请求时一起发送进行认证处理。与会话不同,不在服务器存储状态(stateless),因此扩展性优秀。
🎯 用类比理解
游乐园门票
将JWT比作游乐园门票:
普通门票(会话方式)
├─ 入场时获得手环
├─ 每次乘坐时工作人员检查手环
└─ 工作人员需要记住所有访客(服务器负担)
VIP门票(JWT方式)
├─ 入场时获得防伪印章的门票
├─ 门票上记录姓名、有效期、等级
├─ 每次乘坐只需出示门票
└─ 工作人员只检查印章(服务器负担小)
JWT = 带有防伪印章的信息卡
⚙️ 工作原理
1. JWT结构
JWT = Header.Payload.Signature
例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJraW0ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
Header (头部) Payload (有效载荷) Signature (签名)
Header (头部)
{
"alg": "HS256", // 签名算法
"typ": "JWT" // 令牌类型
}
// Base64编码
Payload (有效载荷) - 实际数据
{
"userId": 123,
"username": "kim",
"email": "kim@example.com",
"role": "admin",
"iat": 1640000000, // Issued At (颁发时间)
"exp": 1640086400 // Expiration (过期时间)
}
// Base64编码
Signature (签名) - 防止伪造
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret // 密钥(只有服务器知道)
)
2. JWT认证流程
1. 登录
客户端 → 服务器: username, password
服务器: 认证成功 → 生成JWT并返回
2. 请求时包含JWT
客户端 → 服务器: Authorization: Bearer <JWT>
3. 服务器验证JWT
- 验证签名(是否伪造)
- 验证过期时间
- 从Payload提取用户信息
4. 响应
服务器 → 客户端: 请求的数据
💡 实际示例
JWT生成和验证 (Node.js)
const jwt = require('jsonwebtoken');
// 密钥(建议用环境变量管理)
const SECRET_KEY = 'your-secret-key-keep-it-safe';
// ========== 生成JWT ==========
function generateToken(user) {
// 定义Payload
const payload = {
userId: user.id,
username: user.username,
email: user.email,
role: user.role
};
// 选项
const options = {
expiresIn: '1h', // 1小时后过期
// expiresIn: '7d', // 7天
// expiresIn: '30m', // 30分钟
issuer: 'my-app', // 颁发者
subject: 'user-auth' // 用途
};
// 生成JWT
const token = jwt.sign(payload, SECRET_KEY, options);
return token;
}
// ========== 验证JWT ==========
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
console.log('验证成功:', decoded);
/*
{
userId: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin',
iat: 1640000000,
exp: 1640003600
}
*/
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.error('令牌已过期');
} else if (error.name === 'JsonWebTokenError') {
console.error('无效的令牌');
}
return null;
}
}
// ========== 使用示例 ==========
const user = {
id: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin'
};
const token = generateToken(user);
console.log('生成的JWT:', token);
const decoded = verifyToken(token);
console.log('解码:', decoded);
登录API实现
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const users = []; // 实际应使用数据库
// ========== 注册 ==========
app.post('/api/register', async (req, res) => {
const { username, password, email } = req.body;
// 密码哈希化
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: users.length + 1,
username,
email,
password: hashedPassword
};
users.push(user);
res.json({ message: '注册成功', userId: user.id });
});
// ========== 登录 ==========
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 查找用户
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: '用户不存在' });
}
// 验证密码
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '密码错误' });
}
// 生成JWT
const token = jwt.sign(
{
userId: user.id,
username: user.username,
email: user.email
},
SECRET_KEY,
{ expiresIn: '1h' }
);
// 同时颁发Refresh Token(可选)
const refreshToken = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '7d' }
);
res.json({
message: '登录成功',
accessToken: token,
refreshToken: refreshToken
});
});
// ========== 认证中间件 ==========
function authenticateToken(req, res, next) {
// 从Authorization头提取令牌
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: '无令牌' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期' });
}
return res.status(403).json({ error: '无效的令牌' });
}
// 验证成功 - 将用户信息保存到req
req.user = user;
next();
});
}
// ========== 受保护的路由 ==========
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user是从JWT提取的信息
res.json({
message: '个人资料信息',
user: req.user
});
});
app.get('/api/admin', authenticateToken, (req, res) => {
// 权限检查
if (req.user.role !== 'admin') {
return res.status(403).json({ error: '需要管理员权限' });
}
res.json({ message: '管理员页面' });
});
app.listen(3000, () => {
console.log('服务器运行中: http://localhost:3000');
});
前端使用JWT
// ========== 登录 ==========
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();
if (response.ok) {
// 将JWT保存到LocalStorage
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
console.log('登录成功!');
}
}
// ========== API请求时包含JWT ==========
async function fetchProfile() {
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// 令牌过期 → 跳转到登录页
window.location.href = '/login';
return;
}
const data = await response.json();
console.log('个人资料:', data);
}
// ========== 使用Axios拦截器自动化 ==========
import axios from 'axios';
// 请求拦截器: 自动为所有请求添加JWT
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截器: 401错误时自动登出
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// 现在所有请求都会自动包含JWT!
axios.get('/api/profile').then(response => {
console.log(response.data);
});
Refresh Token实现
// ========== Access Token + Refresh Token模式 ==========
// Access Token: 短期(15分钟)
// Refresh Token: 长期(7天)
app.post('/api/login', async (req, res) => {
// ... 登录验证 ...
const accessToken = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '15m' } // 15分钟
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET_KEY,
{ expiresIn: '7d' } // 7天
);
// Refresh Token保存到DB(可选)
// await saveRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken });
});
// ========== 刷新Access Token ==========
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: '无Refresh Token' });
}
jwt.verify(refreshToken, REFRESH_SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: '无效的Refresh Token' });
}
// 颁发新的Access Token
const accessToken = jwt.sign(
{ userId: user.userId },
SECRET_KEY,
{ expiresIn: '15m' }
);
res.json({ accessToken });
});
});
// ========== 前端: 自动刷新令牌 ==========
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 用Refresh Token获取新的Access Token
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/refresh', { refreshToken });
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token也过期 → 登出
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
🤔 常见问题
Q1. JWT vs 会话有什么区别?
A:
// ========== 会话方式 ==========
// 服务器保存用户信息
登录 → 服务器保存会话
{
sessionId: 'abc123',
userId: 123,
username: 'kim'
}
客户端只保存sessionId
Cookie: sessionId=abc123
请求时用sessionId查询服务器会话
优点: 服务器可立即无效化
缺点: 需要服务器内存/存储,扩展性低
// ========== JWT方式 ==========
// 客户端保存令牌
登录 → 生成JWT并返回
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
客户端保存整个JWT
localStorage.setItem('token', jwt)
请求时包含JWT,服务器只验证签名
优点: 服务器无负担,扩展性好
缺点: 难以无效化令牌,体积大
Q2. JWT安全吗?
A: 正确使用时安全:
// ✅ 安全的使用方法
1. 必须使用HTTPS
- HTTP会被拦截JWT
2. 密钥安全
- 用环境变量管理
- 绝对禁止硬编码到代码中
const SECRET = process.env.JWT_SECRET;
3. 短过期时间
- Access Token: 15分钟~1小时
- 用Refresh Token刷新
4. 禁止存储敏感信息
❌ { password: '1234', ssn: '123-45-6789' }
✅ { userId: 123, role: 'user' }
5. 防止XSS
- 推荐使用HttpOnly Cookie而非LocalStorage
res.cookie('token', jwt, {
httpOnly: true, // JavaScript无法访问
secure: true, // 仅HTTPS
sameSite: 'strict'
});
// ❌ 危险的使用方法
1. 无过期时间
2. 用HTTP传输
3. 保存在LocalStorage(XSS漏洞)
4. 包含密码等敏感信息
Q3. 如何无效化JWT?
A: 有几种方法:
// 1. 黑名单(Blacklist)
// - 登出时将令牌保存到DB/Redis
// - 请求时检查黑名单
const blacklist = new Set();
app.post('/api/logout', authenticateToken, (req, res) => {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: '登出成功' });
});
function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (blacklist.has(token)) {
return res.status(401).json({ error: '已无效化的令牌' });
}
// ... JWT验证 ...
}
// 2. 保存到Redis(设置过期时间)
const redis = require('redis');
const client = redis.createClient();
app.post('/api/logout', authenticateToken, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
// 黑名单保持到令牌过期时间
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await client.setEx(`blacklist:${token}`, ttl, 'true');
res.json({ message: '登出成功' });
});
// 3. 短期 + Refresh Token
// - Access Token: 15分钟(无需无效化)
// - 只在DB中保存和无效化Refresh Token
// 4. 版本管理
// - 在DB中保存用户令牌版本
// - 登出时增加版本号
const payload = {
userId: user.id,
tokenVersion: user.tokenVersion // 从DB获取
};
// 登出
await db.users.update(
{ id: userId },
{ $inc: { tokenVersion: 1 } } // 增加版本号
);
Q4. JWT应该存储在哪里?
A:
// 选项1: LocalStorage(简单但有XSS漏洞)
localStorage.setItem('token', jwt);
// ❌ XSS攻 击可窃取令牌
// 选项2: SessionStorage(关闭标签页即删除)
sessionStorage.setItem('token', jwt);
// ❌ XSS漏洞
// 选项3: HttpOnly Cookie(最安全,推荐)
// 服务器设置
res.cookie('token', jwt, {
httpOnly: true, // JavaScript无法访问(防XSS)
secure: true, // 仅HTTPS
sameSite: 'strict',// 防CSRF
maxAge: 3600000 // 1小时
});
// 客户端自动发送
fetch('/api/profile', {
credentials: 'include' // 包含Cookie
});
// 选项4: 内存(最安全但刷新后登出)
let token = null;
function login() {
// ... 登录 ...
token = response.accessToken; // 仅保存在变量
}
// 优缺点比较
LocalStorage: 简单但有XSS漏洞
Cookie: 安全但设置复杂
Memory: 最安全但UX差
Q5. Access Token vs Refresh Token?
A:
// Access Token
// - 短期(15分钟~1小时)
// - 每次API请求都发送
// - 被窃取也会快速过期
// Refresh Token
// - 长期(7天~30天)
// - 仅在刷新Access Token时使用
// - 保存在DB中管理
// 流程
1. 登录
→ 颁发Access Token(15分钟) + Refresh Token(7天)
2. API请求
→ 使用Access Token
3. Access Token过期(15分钟后)
→ 用Refresh Token获取新的Access Token
4. Refresh Token也过期(7天后)
→ 需要重新登录
// 代码示例
// 颁发令牌
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
// 自动刷新
if (accessTokenExpired) {
const newAccessToken = await refreshAccessToken(refreshToken);
}
🎓 下一步
理解JWT后, 继续学习:
- 什么是HTTPS? (文档准备中) - 安全的令牌传输
- 什么是CORS? (文档准备中) - API安全
- 什么是Cookie? - 会话 vs JWT
有用的工具
// JWT调试器
// https://jwt.io
// - JWT解码和验证
// 库
// Node.js: jsonwebtoken
// Python: PyJWT
// Java: jjwt
// Go: jwt-go
// 测试
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const decoded = jwt.decode(token, { complete: true });
console.log(decoded);
/*
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { userId: 123, ... },
signature: '...'
}
*/
🎬 总结
JWT是现代Web的标准认证方式:
- 结构: Header.Payload.Signature
- 优点: 无状态、可扩展、支持多平台
- 安全: HTTPS、HttpOnly Cookie、短期有效
- 模式: Access Token + Refresh Token
正确使用可构建安全高效的认证系统! 🎫✨