人人都会AI编程

14.1 认证与鉴权

更新时间:2026-07-10

用户身份认证是 Web 应用中最基本也是最容易出错的安全环节。Node.js 生态中,最常见的两种认证方案是 Session-Cookie 机制JWT(JSON Web Token)。无论你选择 Express、Koa 还是 NestJS,它们都是构建用户系统的核心。本节会用真实的代码与场景,把两者的原理、用法和权衡说清楚。


14.1.1 Session-Cookie 机制

原理简述

Session-Cookie 是“服务端持有状态,客户端持有凭证”的模式。一次完整的登录流程如下:

  1. 用户提交登录表单,服务端验证账号密码。
  2. 验证通过后,服务端在内存或 Redis 中创建一个 Session 对象,存储用户 ID 等关键信息,并生成一个唯一 Session ID(通常是随机的 UUID 或哈希串)。
  3. 服务端通过 Set-Cookie 响应头将 Session ID 写入浏览器 Cookie。
  4. 后续请求中,浏览器会自动携带该 Cookie,服务端根据其中的 Session ID 查找对应的 Session 数据,从而识别用户。
  5. 用户登出时,服务端删除对应 Session 数据,并清除客户端 Cookie。

这种模式的核心特征是 服务端有状态:用户登录状态完全由服务端维护,客户端只是一个“通行证”。

Express 中的实现

使用 express-session 中间件,几行代码就能搭建 Session 管理。

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis').createClient();

const app = express();

app.use(session({
  store: new RedisStore({ client: redis }), // 生产环境推荐用 Redis 存储
  secret: 'your-secret-key',               // 用于签名 Session ID cookie
  resave: false,                           // 每次请求都重新保存 session 没有意义
  saveUninitialized: false,                // 只保存已修改的 session
  cookie: {
    httpOnly: true,                         // 禁止 JS 读取,防御 XSS
    secure: process.env.NODE_ENV === 'production', // 仅 HTTPS 传输
    maxAge: 1000 * 60 * 60 * 24             // 有效期 24 小时
  }
}));

// 登录接口
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  // 模拟验证用户
  if (username === 'admin' && password === '123') {
    req.session.user = { id: 1, username: 'admin' };
    res.json({ message: '登录成功' });
  } else {
    res.status(401).json({ error: '账号或密码错误' });
  }
});

// 受保护路由中间件
function requireAuth(req, res, next) {
  if (req.session.user) {
    next();
  } else {
    res.status(401).json({ error: '请先登录' });
  }
}

app.get('/profile', requireAuth, (req, res) => {
  res.json({ user: req.session.user });
});

// 登出
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: '登出失败' });
    res.clearCookie('connect.sid'); // 清除客户端 Cookie
    res.json({ message: '已登出' });
  });
});

存储方式与生产优化

  • 默认内存存储:开发时直接使用,但进程重启后所有 Session 都会丢失,且无法在多进程间共享。
  • Redis 存储:将 Session 数据存入 Redis,配合 connect-redisioredis。多实例共享同一 Redis 即可实现集群级别的 Session 统一管理。
  • 数据库存储:某些场景会将 Session 存入 MySQL/PostgreSQL,但性能不如 Redis,只适合低频认证。

安全风险与防护

  • Session 固定攻击:用户登录前后如果 Session ID 不变,攻击者可能诱骗用户使用已知的 Session ID。解决方法是登录成功后调用 req.session.regenerate() 重新生成 ID。
  • XSS 窃取 Cookie:设置 httpOnly: true 阻止 JS 读取 document.cookie,但无法完全防御 XSS,仍需对用户输入严格转义。
  • CSRF 攻击:由于浏览器自动携带 Cookie,恶意网站可能触发用户执行非预期的操作。防御措施包括使用 CSRF Token 或 SameSite Cookie 属性。

适用场景

Session-Cookie 非常适合传统的服务端渲染应用(如 Express + EJS/Pug),以及需要严格、集中控制用户状态的系统。其优势是服务端可以随时强制用户下线(删除 Session),缺点是需要额外的存储和状态管理。


14.1.2 JWT 令牌认证

原理简述

JWT 是一种无状态的认证方案。服务端不保存任何用户登录信息,而是将用户数据(claims)编码成一个自包含的 JSON 令牌发给客户端,客户端每次请求都携带该令牌,服务端通过验证签名来确认令牌的合法性和完整性。

一个 JWT 由三部分组成,以 . 分隔:header.payload.signature

  • Header:声明令牌类型和签名算法,例如 {"alg":"HS256","typ":"JWT"}
  • Payload:存放声明(claims),如 {"userId":1,"role":"admin","iat":1516239022}。注意这部分只是 Base64 编码,并非加密,任何人都能解码查看其内容。
  • Signature:使用密钥对 header + "." + payload 进行哈希签名,保证令牌未被篡改。

常见的使用流程:

  1. 用户登录,服务端验证凭据,生成 JWT 并返回给客户端。
  2. 客户端将 JWT 存储在 localStorage 或 httpOnly Cookie 中。
  3. 后续请求中,客户端将 JWT 放在 Authorization: Bearer <token> 头部。
  4. 服务端在每个请求中验证签名并解析 payload,直接获取用户信息,无需查询数据库。
  5. 令牌过期后,需要重新登录或使用 refresh token 续期。

Node.js 中的实现

使用 jsonwebtoken 库可以方便地生成和验证 JWT。

const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

const SECRET = 'your-256-bit-secret';

// 登录
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  if (username === 'admin' && password === '123') {
    const token = jwt.sign(
      { userId: 1, role: 'admin' },   // payload
      SECRET,
      { expiresIn: '2h' }             // 有效期
    );
    res.json({ token });
  } else {
    res.status(401).json({ error: '账号或密码错误' });
  }
});

// 验证中间件
function 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: '未提供令牌' });

  jwt.verify(token, SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: '令牌无效或已过期' });
    req.user = user; // 将 payload 挂载到请求对象
    next();
  });
}

app.get('/profile', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});

令牌存储与传输安全

  • 存储位置:最常见的是前端用 localStorage 存储,但这容易受到 XSS 攻击(恶意脚本可读取并窃取)。更安全的方式是存储在 httpOnly、Secure 的 Cookie 中,同时设置 SameSite 策略以防止 CSRF,但 Cookie 的大小受限(4KB),且不能跨域携带。
  • 传输安全:必须通过 HTTPS 传输,防止令牌被中间人截获。
  • Payload 敏感信息:绝对不要在 payload 中放置密码或手机号等敏感数据,因为 payload 默认只需要 Base64 解码就能查看,除非额外使用 JWE(JSON Web Encryption)加密。

Token 过期与刷新机制

由于 JWT 是无状态的,一旦签发就无法在服务端单独使其失效(除非维护黑名单,这又回到了有状态)。通常的做法是:

  1. 短期 access token:有效期设为 15-30 分钟,用以访问资源。
  2. 长期 refresh token:有效期设为 7-30 天,只用来换取新的 access token,不直接用于资源访问。
  3. 客户端在 access token 过期时,用 refresh token 向 /refresh 接口请求新令牌。服务端验证 refresh token(以及可能需要检查是否在数据库的撤销列表中),然后下发新的 access token 和可能的 refresh token 轮替。

示例 /refresh 接口:

app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;
  if (!refreshToken) return res.status(401).json({ error: '缺少刷新令牌' });
  // 验证 refresh token(可能需要从数据库读取记录)
  jwt.verify(refreshToken, REFRESH_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: '刷新令牌无效' });
    // 生成新的 access token
    const newAccessToken = jwt.sign({ userId: user.userId }, SECRET, { expiresIn: '15m' });
    res.json({ accessToken: newAccessToken });
  });
});

登出问题的处理

在纯 JWT 无状态模式下,服务端无法主动废止已签发的 access token。解决方案包括:

  • 简短有效期:access token 有效期极短(如 5 分钟),登出后很快自然过期。
  • 黑名单机制:将需要撤销的 token 的 jti 存入 Redis,设置一个与 token 过期时间一致的有效期,在验证中间件中额外检查黑名单。
  • 不使用纯 JWT:结合服务端状态(如 Session)实现登出能力。

适用场景

JWT 特别适合前后端分离的 SPA、移动端应用、微服务间的无状态通信。因为不需要服务端存储状态,横向扩展非常容易,只要能拿到公钥或对称密钥就能验证令牌。


14.1.3 选择 Session 还是 JWT?

| 维度 | Session-Cookie | JWT |
|------|----------------|-----|
| 状态管理 | 有状态,服务端存储 | 无状态,客户端持有 |
| 扩展性 | 需要共享 Session 存储(Redis) | 天然适合分布式系统 |
| 性能 | 每次请求需查询存储 | 仅需本地计算验证签名 |
| 登出/失效 | 服务端直接删除 Session | 无法主动失效(除非用黑名单) |
| XSS 风险 | Cookie httpOnly 可防窃取 | 若存 localStorage,XSS 可窃取 |
| CSRF 风险 | 需额外防御 | 若从头部发送,无 CSRF 风险 |
| 移动端兼容 | 需处理 Cookie 管理 | 天然适合原生 App |
| 综合推荐 | 传统服务端渲染应用 | SPA、移动 App、微服务 API |

在实际项目中,你完全可以混合使用:核心 API 用 JWT 认证(利用其无状态和通用性),管理后台用 Session-Cookie(利用服务端可控性)。关键在于评估你的应用架构、安全需求以及团队维护成本。


14.1.4 通用安全实践(两种方案都适用)

  1. 密码存储:永远不要明文存储密码。使用 bcrypt 进行哈希加盐。
  2. HTTPS 强制:生产环境绝对不允许 HTTP 明文传输身份凭证。
  3. 最小权限原则:Token/Session 中只包含最少必要信息,如用户 ID,不要放角色详情(可从数据库动态获取)。
  4. 定期轮换密钥:JWT 签名密钥和 Session 密钥应定期更换,并允许多密钥并存以平滑过渡。
  5. 接口限流:对登录、注册等认证接口应用限流(express-rate-limit),防止暴力破解。

掌握这两种认证方式,你就可以为几乎所有 Node.js 项目搭建安全、可靠的身份系统。在后续章节中,我们会进一步结合 RBAC 权限模型,构建完整的用户访问控制体系。