JWT 认证机制
文章目录
- JWT
- 一、JWT 的核心结构
- 1. Header (头部)
- 2. Payload (负载)
- 3. Signature (签名)
- 二、JWT 认证流程
- 三、JWT 优缺点
- 1. 优点
- 2. 缺点
- 四、JWT 安全性实践
- 五、JWT 实现示例(Node.js)
- 1. 安装依赖
- 1) `jsonwebtoken`
- 2) `express-jwt`:
- 2. 配置中间件
- 3. 生成 JWT (登录接口)
- 4. 访问受保护的路由
- 5. 错误处理
- 六、高级配置
- 1. 动态获取秘钥
- 2. 自定义令牌位置
- 3. 权限校验
- 七、完整示例代码
- 八、注意事项
JWT
JSON Web Token(JWT)认证机制是一种无状态、基于令牌的身份验证方法,广泛应用于现代Web应用和API服务。
一、JWT 的核心结构
JWT由三部分组成,格式为:Header.Payload.Signature
,以点分割,Base64编码。
1. Header (头部)
- 声明令牌类型(
typ: "JWT"
)和签名算法(如alg:HS256
)
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload (负载)
- 包含声明(Claims),即用户信息和其他数据。
- 标准声明 (建议但不强制) : iss(签发者)、exp(过期时间)、sub(主题)、aud(受众)等。
- 自定义声明 : 存放业务数据(如
userId
)。
{
"sub": "123456789",
"name": "John Doe",
"iat": 1516239022
}
3. Signature (签名)
- 将Header和Payload用Base64编码后拼接,通过Header中指定的算法和密钥生成签名。
- 示例(HMAC SHA256):
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey )
二、JWT 认证流程
- 用户登录
客户端发送用户名密码到服务端,验证成功后生成JWT返回。 - 令牌存储
客户端将JWT存储在localStorage或Cookie中。 - 发送请求
每次请求API时,在Authorization头中携带JWT:Authorization: Bearer <token>
- 服务端验证
- 检查签名是否有效(防篡改)。
- 验证声明(如exp是否过期)。
- 授权访问
验证通过后,服务端返回请求的资源。
三、JWT 优缺点
1. 优点
- 无状态:服务端无需存储会话信息,适合分布式系统。
- 扩展性强:Payload可携带业务数据,减少数据库查询。
- 跨域友好:适用于API网关、微服务架构。
2. 缺点
- 不可撤销:令牌到期前无法强制失效(需额外设计机制)。
- 安全性风险:令牌泄露可能导致长期权限滥用。
- 性能开销:频繁签名验证可能增加CPU负载。
四、JWT 安全性实践
- 使用 HTTPS 传输: 防止令牌在传输中被窃听。
- 设置短有效期: 通过exp声明限制令牌寿命,结合Refresh Token刷新。
- 强密钥管理: HS256算法需使用高强度密钥,或采用RS256非对称加密。
- 避免敏感数据: Payload内容可被Base64解码,勿存密码等敏感信息。
- 防御XSS攻击: 优先使用HttpOnly、Secure、SameSite的Cookie存储。
五、JWT 实现示例(Node.js)
1. 安装依赖
npm install express-jwt jsonwebtoken
1) jsonwebtoken
- 用途: 用于直接生成(签名)和验证 JWT Token 的底层库。
- 核心功能:
- 生成 Token:将用户数据(如用户ID)通过密钥签名,生成 JWT 字符串。
- 验证 Token:验证 JWT 是否有效(如是否被篡改、是否过期),并解码 Token 中的用户数据。
- 典型场景:
- 用户登录成功后,生成 Token 返回给客户端。
- 手动验证客户端发来的 Token(例如在某些自定义逻辑中)。
2) express-jwt
:
- 用途:为 Express 框架设计的中间件,用于自动验证请求中的 JWT Token,并将解码后的数据附加到请求对象(如 req.user)。
- 核心功能:
- 自动从请求头、Cookie 或查询参数中提取 Token。
- 验证 Token 的有效性。
- 将 Token 解码后的数据(如用户信息)挂载到 Express 的 req 对象上,供后续路由使用。
- 典型场景:
- 保护需要身份验证的 API 路由(例如 /api/profile)。
- 自动提取用户身份信息,无需在每个路由中手动处理 Token。
2. 配置中间件
const express = require('express');
const { expressjwt } = require('express-jwt');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = 'your_secret_key_here'; // 建议使用环境变量存储
// 配置 JWT 验证中间件
app.use(
expressjwt({
secret: SECRET_KEY,
algorithms: ['HS256'], // 指定签名算法
}).unless({
path: ['/login', '/public'], // 排除不需要验证的路由
})
);
3. 生成 JWT (登录接口)
// 模拟用户登录
app.post('/login', (req, res) => {
// 假设验证用户密码成功
const user = { id: 1, username: 'admin' };
// 生成 JWT(有效期为 1 小时)
const token = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ code: 200, token });
});
4. 访问受保护的路由
app.get('/protected', (req, res) => {
// 通过 req.auth 获取 JWT 中的 payload 数据
res.json({
code: 200,
message: '访问成功',
user: req.auth.userId,
});
});
5. 错误处理
自定义错误中间件
// 捕获 JWT 验证错误
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ code: 401, message: '无效或过期的令牌' });
}
next(err);
});
六、高级配置
1. 动态获取秘钥
如果密钥需要从数据库或其他服务动态获取
app.use(
expressjwt({
secret: (req, token) => {
const { header } = token;
// 根据 header 中的信息动态获取密钥
return getSecretFromDB(header.keyId);
},
algorithms: ['HS256'],
})
);
2. 自定义令牌位置
默认从 Authorization
头获取令牌,可自定义其他位置
app.use(
expressjwt({
secret: SECRET_KEY,
algorithms: ['HS256'],
getToken: (req) => {
// 从 Cookie 获取
if (req.cookies?.token) return req.cookies.token;
// 从 Query 参数获取
if (req.query.token) return req.query.token;
return null;
},
})
);
3. 权限校验
结合角色验证(需自定义中间件)
// 检查用户角色是否为管理员
function checkAdmin(req, res, next) {
if (req.auth.role !== 'admin') {
return res.status(403).json({ code: 403, message: '权限不足' });
}
next();
}
// 管理员专用接口
app.get('/admin', checkAdmin, (req, res) => {
res.json({ code: 200, data: '管理员数据' });
});
七、完整示例代码
const express = require('express');
const { expressjwt } = require('express-jwt');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const app = express();
const SECRET_KEY = 'your_secret_key';
// 解析 Cookie 和 JSON 请求体
app.use(express.json());
app.use(cookieParser());
// JWT 验证中间件
app.use(
expressjwt({
secret: SECRET_KEY,
algorithms: ['HS256'],
getToken: (req) => req.cookies.token, // 从 Cookie 获取令牌
}).unless({ path: ['/login'] })
);
// 登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 模拟用户验证
if (username === 'admin' && password === '123456') {
const token = jwt.sign(
{ userId: 1, role: 'admin' },
SECRET_KEY,
{ expiresIn: '1h' }
);
res.cookie('token', token, { httpOnly: true }); // 设置 HttpOnly Cookie
res.json({ code: 200, message: '登录成功' });
} else {
res.status(401).json({ code: 401, message: '用户名或密码错误' });
}
});
// 受保护的路由
app.get('/profile', (req, res) => {
res.json({
code: 200,
data: `用户ID: ${req.auth.userId}, 角色: ${req.auth.role}`,
});
});
// 错误处理
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ code: 401, message: '身份验证失败' });
}
next(err);
});
// 启动服务
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
八、注意事项
- 密钥安全:
- 不要将密钥硬编码在代码中,使用环境变量(如 process.env.SECRET_KEY)。
- 生产环境推荐使用 RSA 非对称加密(算法 RS256)。
- 令牌存储:
- 推荐使用 HttpOnly Cookie 防止 XSS 攻击。
- 避免将令牌存储在 localStorage(易受 XSS 攻击)。
- 令牌有效期:
- 设置较短的过期时间(如 15 分钟),配合 Refresh Token 刷新机制。