Skip to content

Node系列 · Express:JWT 登录认证

上一章讲了 JWT 原理——这一章用 jsonwebtoken + express-jwt 在 Express 项目里实现完整鉴权流程。

一、登录流程

二、安装依赖

bash
npm install jsonwebtoken express-jwt
  • jsonwebtoken:签发 / 验证 token
  • express-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-jwtAuthorization: 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')}`,
  },
});
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 一旦签发就无法主动失效——只能通过:

  1. 短过期 + Refresh Token(推荐)
  2. tokenVersion 字段递增(用户级撤销)
  3. 黑名单(精确撤销单个 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 之前