JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象安全地传输信息。由于信息是经过数字签名的,因此可以被验证和信任。在 Web 应用中,JWT 常用于身份验证和授权,特别适用于分布式微服务、单页应用和移动端。
JWT 通常由三部分组成:Header(头部)、Payload(负载) 和 Signature(签名)。它们用点 . 连接成一个字符串:
xxxxx.yyyyy.zzzzz
其中各部分都是 Base64Url 编码的 JSON 字符串。
头部通常包含两部分:令牌的类型(即 JWT)和使用的签名算法,例如 HMAC SHA256 或 RSA。
{
"alg": "HS256",
"typ": "JWT"
}
负载包含要传递的声明(claims)。声明可以是用户 ID、角色、过期时间等。有三类声明:
iss(签发者)、exp(过期时间)、sub(主题)等,非强制但推荐。{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1735689600
}
签名用于验证消息在传输过程中未被篡改。它通过对头部和负载的编码字符串用指定算法和密钥进行签名生成。例如使用 HMAC SHA256:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Authorization: Bearer <token> 中携带 JWT。这种方式是无状态的,服务器不需要存储会话信息,非常适合扩展。
在 Node.js 中使用 JWT,最常用的库是 jsonwebtoken。安装:
npm install jsonwebtoken
同时,我们可能还需要 dotenv 管理密钥。
使用 jwt.sign() 方法生成 token。它接受 payload、密钥和可选选项(如过期时间)。
const jwt = require('jsonwebtoken');
const payload = { userId: 123, role: 'admin' };
const secret = process.env.JWT_SECRET || 'mysecret';
const token = jwt.sign(payload, secret, { expiresIn: '1h' });
console.log(token);
expiresIn 可以使用数值(秒)或描述字符串,如 '2 days'、'10h'、'7d'。
使用 jwt.verify() 验证 token 的签名和有效性。如果验证通过,返回解码后的 payload;否则抛出错误。
try {
const decoded = jwt.verify(token, secret);
console.log(decoded); // { userId: 123, role: 'admin', iat: ..., exp: ... }
} catch (err) {
console.error('Token 无效:', err.message);
}
注意:verify 会自动检查过期时间,如果 token 已过期,会抛出 TokenExpiredError。
创建一个简单的登录路由,验证用户凭据后返回 JWT。
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const secret = process.env.JWT_SECRET || 'mysecret';
// 模拟用户数据库
const users = [
{ id: 1, username: 'alice', password: 'password123' },
{ id: 2, username: 'bob', password: 'password456' }
];
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 查找用户
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 生成 token
const payload = { userId: user.id, username: user.username };
const token = jwt.sign(payload, secret, { expiresIn: '1h' });
res.json({ token });
});
app.listen(3000, () => console.log('Server running on port 3000'));
中间件从请求头中提取 token,验证并挂载用户信息到 req.user,然后继续处理;如果验证失败,返回 401 或 403。
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: '未提供 token' });
}
jwt.verify(token, secret, (err, user) => {
if (err) {
return res.status(403).json({ error: 'token 无效或已过期' });
}
req.user = user;
next();
});
};
在需要保护的路由中使用该中间件:
app.get('/profile', authenticateToken, (req, res) => {
res.json({ message: '这是个人资料', user: req.user });
});
由于 access token 有效期较短(例如 15 分钟),为了良好的用户体验,通常引入 refresh token(刷新令牌)。refresh token 有效期较长(例如 7 天),存储在服务器端(如数据库),用于在 access token 过期后获取新的 access token。
简单流程:
/refresh-token 接口,携带 refresh token。示例 refresh token 接口:
// 假设已将 refresh token 存储到数据库
const refreshTokens = new Set(); // 简单示例,生产用数据库
app.post('/refresh-token', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.has(refreshToken)) {
return res.status(403).json({ error: '无效的 refresh token' });
}
jwt.verify(refreshToken, secret, (err, user) => {
if (err) return res.status(403).json({ error: 'refresh token 无效' });
// 生成新的 access token
const newAccessToken = jwt.sign({ userId: user.userId }, secret, { expiresIn: '15m' });
res.json({ accessToken: newAccessToken });
});
});
注意:实际生产应考虑 refresh token 的存储安全性、轮换机制(每次刷新返回新 refresh token 并使旧失效)等。
下面是一个包含登录、注册、受保护路由的完整 Express 应用,使用 JWT 进行认证。
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const secret = process.env.JWT_SECRET || 'mysecret';
const users = []; // 内存数据库
// 注册
app.post('/register', (req, res) => {
const { username, password } = req.body;
if (users.find(u => u.username === username)) {
return res.status(400).json({ error: '用户已存在' });
}
const newUser = { id: users.length + 1, username, password };
users.push(newUser);
res.status(201).json({ message: '注册成功' });
});
// 登录
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' });
}
const token = jwt.sign({ userId: user.id, username: user.username }, secret, { expiresIn: '1h' });
res.json({ token });
});
// 验证中间件
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: '未提供 token' });
const token = authHeader.split(' ')[1];
jwt.verify(token, secret, (err, user) => {
if (err) return res.status(403).json({ error: 'token 无效' });
req.user = user;
next();
});
};
// 受保护路由
app.get('/profile', authenticate, (req, res) => {
res.json({ message: '这是个人资料', user: req.user });
});
app.listen(3000, () => console.log('Server running on port 3000'));