Node系列 · Express:JWT 登录认证
上一章讲了 JWT 原理——这一章用
jsonwebtoken+express-jwt在 Express 项目里实现完整鉴权流程。
一、登录流程
二、安装依赖
bash
npm install jsonwebtoken express-jwtjsonwebtoken:签发 / 验证 tokenexpress-jwt:Express 中间件,自动从 Authorization header 提取并验证 token
三、签发 token
javascript
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
router.post('/login', async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ where: { email } });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
// 签发 token
const token = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
user: { id: user.id, name: user.name, email: user.email },
token,
});
} catch (err) {
next(err);
}
});四、验证 token 中间件
4.1 express-jwt 基础用法
javascript
const { expressjwt } = require('express-jwt');
// 验证 Authorization: Bearer xxx
app.use(expressjwt({
secret: JWT_SECRET,
algorithms: ['HS256'],
}));
// 验证失败的请求会抛 UnauthorizedError
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Token 无效或已过期' });
}
next(err);
});4.2 读取当前用户
express-jwt 验证成功后把 payload 挂到 req.auth:
javascript
router.get('/api/profile', (req, res, next) => {
try {
// req.auth 是 jwt.verify 解析出的 payload
const { userId, tokenVersion } = req.auth;
// 验证版本号(防 token 撤销后仍能用)
const user = await User.findByPk(userId);
if (!user || user.tokenVersion !== tokenVersion) {
return res.status(401).json({ error: 'Token 已失效' });
}
res.json({ user });
} catch (err) {
next(err);
}
});4.3 自定义 Authorization header 提取
默认 express-jwt 从 Authorization: Bearer <token> 提取。如果用 Cookie 存 token:
javascript
const { expressjwt } = require('express-jwt');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use(expressjwt({
secret: JWT_SECRET,
algorithms: ['HS256'],
getToken: (req) => req.cookies.token, // 从 cookie 读取
}));五、Token 来源
JWT 可以从两个地方取——前端灵活选:
5.1 Authorization header(推荐 API 场景)
http
GET /api/profile HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...javascript
fetch('/api/profile', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});5.2 Cookie(推荐传统 Web 场景)
http
GET /api/profile HTTP/1.1
Cookie: token=eyJhbGciOiJIUzI1NiIs...javascript
res.cookie('token', token, {
httpOnly: true, // 防 XSS
secure: true, // 仅 HTTPS
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000,
});| 方案 | 优点 | 缺点 |
|---|---|---|
| Authorization header | 跨域友好(不受 Cookie 限制) | 前端需手动加 header |
| Cookie | 浏览器自动带 | 受 CORS + SameSite 限制 |
TIP
API 接口用 Authorization header,Web 应用用 Cookie——前者对应前后端分离,后者对应传统 SSR。
六、谁需要鉴权、谁不需要
express-jwt 是中间件,挂在哪里就保护哪里:
javascript
// 全局:所有路由都要鉴权
app.use(expressjwt({ secret: JWT_SECRET, algorithms: ['HS256'] }));
// 部分:只对 /api/private 生效
app.use('/api/private', expressjwt({ secret: JWT_SECRET, algorithms: ['HS256'] }));
// 公开路由:放在 express-jwt 之前
app.post('/api/auth/login', loginHandler); // 登录公开
app.post('/api/auth/register', registerHandler);
app.get('/api/posts', listPosts); // 文章列表公开WARNING
登录 / 注册接口必须放在 express-jwt 之前——否则未登录用户拿不到 token 就无法登录。
七、退出登录
javascript
router.post('/api/logout', async (req, res) => {
// 客户端删除 token;服务端让当前 token 失效
const userId = req.auth.userId;
// 方法 1:tokenVersion 递增(推荐)
await User.increment('tokenVersion', { where: { id: userId } });
// 方法 2:黑名单(Redis 存黑名单 token 列表)
// await redis.setex(`blacklist:${req.auth.jti}`, 7 * 24 * 60 * 60, '1');
// 方法 3:什么都不做(依赖 token 自然过期)
res.json({ ok: true });
});TIP
JWT 一旦签发就无法主动失效——只能通过:
- 短过期 + Refresh Token(推荐)
- tokenVersion 字段递增(用户级撤销)
- 黑名单(精确撤销单个 token,但需要服务端存储)
大部分项目用短过期 + tokenVersion 足够。
八、刷新 Token
javascript
const jwt = require('jsonwebtoken');
router.post('/api/refresh', (req, res, next) => {
try {
const { refreshToken } = req.body;
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
// 验证 tokenVersion
const user = await User.findByPk(payload.userId);
if (!user || user.tokenVersion !== payload.tokenVersion) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// 签发新 access token
const accessToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (err) {
next(err);
}
});九、whoami 接口
"我是谁"是几乎所有系统的标配接口:
javascript
router.get('/api/whoami', async (req, res, next) => {
try {
const { userId } = req.auth;
const user = await User.findByPk(userId, {
attributes: ['id', 'name', 'email', 'avatar', 'role'], // 不返回密码
});
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json({ user });
} catch (err) {
next(err);
}
});十、最佳实践
| 场景 | 推荐 |
|---|---|
| secret | 长随机字符串,存环境变量 |
| 过期 | Access 15-60 分钟;Refresh 7-30 天 |
| 传输 | API 用 Authorization header;Web 用 HttpOnly Cookie |
| 撤销 | tokenVersion 字段 + 短过期 |
| payload | 只放 ID + 必要业务字段 |
| 登录 | 失败提示要通用("邮箱或密码错误") |
| 中间件顺序 | 登录 / 注册路由在 express-jwt 之前 |
| HTTPS | 永远用 HTTPS |
十一、小结
- 登录:用
bcrypt.compare验证密码 →jwt.sign签发 token - 验证:
express-jwt中间件自动从Authorization: Bearer提取并验证 - payload 挂到
req.auth;读取用户信息查req.auth.userId - 退出:递增
tokenVersion主动失效旧 token - 刷新:双 token 机制(短期 access + 长期 refresh)
- API 用 Authorization header,Web 用 HttpOnly Cookie
- 中间件顺序:登录 / 注册路由在 express-jwt 之前
