身份验证:Session 与 Cookie

在 Web 应用发展的早期,Session 和 Cookie 就是实现用户状态管理的基础设施。即使在 JWT 流行的今天,它们仍然广泛应用于传统服务端渲染的应用中。本章将深入讲解 Cookie 和 Session 的工作原理,以及在 Express 中如何使用它们构建安全的用户认证系统。

1. 为什么需要 Session 和 Cookie?

HTTP 协议是无状态的,这意味着服务器无法识别连续请求是否来自同一个用户。为了维护用户状态(如登录状态、购物车信息),我们需要一种机制在客户端和服务器之间传递标识信息。这就是 Cookie 和 Session 的用武之地。

2. Cookie 基础

Cookie 是由服务器发送给客户端(浏览器)的一小段数据,浏览器会存储它,并在后续请求中自动携带(通常通过 Cookie 请求头)。Cookie 主要用于会话管理、个性化设置和跟踪用户行为。

2.1 Cookie 的属性

  • Name=Value:键值对。
  • Domain:指定哪些域名可以访问该 Cookie。
  • Path:指定哪些路径可以发送 Cookie。
  • Expires / Max-Age:设置 Cookie 的生命周期。若不设置,则为会话 Cookie(浏览器关闭即失效)。
  • Secure:仅通过 HTTPS 发送。
  • HttpOnly:禁止客户端 JavaScript 访问 Cookie,有效防止 XSS 攻击窃取 Cookie。
  • SameSite:控制跨站请求是否携带 Cookie,用于防范 CSRF 攻击。可设为 StrictLaxNone

2.2 在 Express 中设置和读取 Cookie

Express 原生支持设置 Cookie,但读取请求中的 Cookie 需要 cookie-parser 中间件。

npm install cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser());

app.get('/set-cookie', (req, res) => {
  // 设置一个 Cookie,过期时间为 1 天
  res.cookie('username', 'john', { maxAge: 24 * 60 * 60 * 1000, httpOnly: true });
  res.send('Cookie 已设置');
});

app.get('/get-cookie', (req, res) => {
  const username = req.cookies.username;
  res.send(`Cookie 中的用户名是: ${username}`);
});

3. Session 机制

Session 是服务器端存储的用户特定数据。服务器为每个用户创建一个唯一的会话 ID(通常存储在 Cookie 中),并通过该 ID 关联会话数据。当用户发送请求时,浏览器自动携带包含会话 ID 的 Cookie,服务器根据 ID 查找对应的会话数据。

Session 数据可以存储在内存、文件、数据库(如 Redis、MongoDB)中,生产环境通常使用 Redis 等外部存储以实现多实例共享。

3.1 Session 工作流程

  1. 用户登录成功后,服务器创建一个会话,生成唯一的会话 ID,将会话 ID 存入 Cookie 发送给客户端。
  2. 服务器将会话数据(如用户 ID、角色)存储起来。
  3. 客户端后续请求自动携带该 Cookie。
  4. 服务器解析 Cookie 中的会话 ID,查找对应的会话数据,从而识别用户身份。
  5. 用户注销时,服务器销毁会话,客户端 Cookie 也会被清除(或过期)。

4. 在 Express 中使用 express-session

express-session 是 Express 生态中最流行的会话中间件。它负责生成会话 ID、存储会话数据、设置 Cookie 等。

安装:

npm install express-session

4.1 基本配置

const session = require('express-session');

app.use(session({
  secret: 'your-secret-key', // 用于签名会话 ID Cookie 的密钥
  resave: false,             // 是否每次请求都重新保存会话
  saveUninitialized: false,  // 是否保存未初始化的会话
  cookie: {
    secure: false,           // 如果为 true,则仅通过 HTTPS 发送 Cookie
    httpOnly: true,          // 防止 XSS 攻击
    maxAge: 1000 * 60 * 60 * 24 // 24 小时
  }
}));

关键选项说明:

  • secret:用于签名 Cookie 的密钥,防止篡改。生产环境应使用强密钥,并存储在环境变量中。
  • resave:通常设为 false,避免每次请求都重新保存未修改的会话。
  • saveUninitialized:设为 false 可减少存储未登录用户的空会话。
  • cookie:设置 Cookie 的属性,与 res.cookie 类似。

4.2 使用 Session 存储数据

会话数据被挂载到 req.session 对象上,可以像普通对象一样读写。

// 登录处理
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  // 假设验证通过
  req.session.userId = 123;
  req.session.username = username;
  res.send('登录成功');
});

// 受保护路由
app.get('/dashboard', (req, res) => {
  if (req.session.userId) {
    res.send(`欢迎回来,${req.session.username}`);
  } else {
    res.status(401).send('请先登录');
  }
});

// 注销
app.post('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      return res.status(500).send('注销失败');
    }
    res.clearCookie('connect.sid'); // 清除会话 Cookie(默认名为 connect.sid)
    res.send('已注销');
  });
});

5. 会话存储选项

默认情况下,express-session 将会话数据存储在内存中。这会导致:

  • 进程重启后数据丢失。
  • 无法在多实例间共享会话。

因此生产环境应使用外部存储,如 Redis、MongoDB 等。express-session 支持通过 store 选项接入多种存储引擎。

例如使用 connect-redis

npm install redis connect-redis
const redis = require('redis');
const RedisStore = require('connect-redis')(session);
const redisClient = redis.createClient();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false
}));

6. Session 与 JWT 的比较

特性 Session JWT
存储位置 服务器端(内存/数据库) 客户端(浏览器)
状态性 有状态(服务器需要维护会话) 无状态(服务器只需验证签名)
扩展性 需要共享存储(如 Redis)才能跨服务器 天然支持分布式,只需所有服务共享密钥
安全性 Cookie 可设为 HttpOnly,防御 XSS 较好;但需防范 CSRF 若存储在 localStorage,易受 XSS 攻击;使用 HttpOnly Cookie 可缓解
注销机制 直接销毁服务器会话即可 需维护黑名单或依赖短时效 + 刷新 token

7. 安全最佳实践

  • 使用 HttpOnly Cookie:防止客户端脚本读取会话 ID。
  • 设置 Secure 标志:生产环境启用 HTTPS 时,设置 secure: true,确保 Cookie 仅通过加密连接传输。
  • 使用 SameSite 属性:设为 LaxStrict 可有效缓解 CSRF 攻击。
  • 使用强 secret:通过环境变量注入,并定期更换。
  • 定期清理过期会话:避免存储膨胀。
  • 实现会话固定防护:登录后应重新生成会话 ID(req.session.regenerate())。
  • 在注销时销毁会话并清除客户端 Cookie。

8. 完整示例:用户登录与访问控制

下面是一个完整的 Express 应用,演示基于 Session 的认证:

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());

app.use(session({
  secret: 'my-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, maxAge: 1000 * 60 * 30 } // 30分钟
}));

// 模拟用户数据库
const users = [
  { id: 1, username: 'alice', password: '123456' },
  { id: 2, username: 'bob', password: 'abcdef' }
];

// 登录页面(简易 HTML)
app.get('/login', (req, res) => {
  res.send(`
    <form method="POST" action="/login">
      <input type="text" name="username" placeholder="用户名">
      <input type="password" name="password" placeholder="密码">
      <button type="submit">登录</button>
    </form>
  `);
});

// 处理登录
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (user) {
    req.session.userId = user.id;
    req.session.username = user.username;
    res.redirect('/profile');
  } else {
    res.send('用户名或密码错误');
  }
});

// 受保护页面
app.get('/profile', (req, res) => {
  if (!req.session.userId) {
    return res.redirect('/login');
  }
  res.send(`
    <h1>欢迎 ${req.session.username}</h1>
    <a href="/logout">注销</a>
  `);
});

// 注销
app.get('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      return res.send('注销失败');
    }
    res.clearCookie('connect.sid');
    res.redirect('/login');
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

9. 常见问题

  • 为什么需要 session 密钥? 用于签名 Cookie,防止客户端篡改会话 ID。
  • session 数据可以存储在哪些地方? 内存(默认)、Redis、MongoDB、MySQL 等。选择取决于性能和共享需求。
  • 如何应对跨站请求伪造(CSRF)? 除了 SameSite Cookie,还可以使用 CSRF Token(如 csurf 中间件)。
  • session 在 API 中可用吗? 可以,但通常 RESTful API 倾向于无状态认证(如 JWT)。Session 更多用于服务端渲染的 Web 应用。
总结: Session 和 Cookie 是 Web 认证的经典方式,它们简单可靠,特别适合传统多页应用。通过本章学习,你应该掌握了在 Express 中配置和使用会话的完整流程,并了解了安全注意事项。在构建现代 API 时,可以结合业务需求选择 Session 或 JWT。