浏览器 Web 安全基础系列

  1. 浏览器怎么判断是不是同一个网站

  2. Cookie 为什么会自动携带

  3. Session 登录状态为什么可能被劫持(当前篇)

  4. HTTPS 为什么仍然不够

  5. CORS 到底在限制什么

  6. CSRF 是怎么冒用登录状态的

  7. CSRF 如何防御

  8. XSS 为什么能在你的网站执行

  9. CSP 与浏览器安全响应头

  10. Clickjacking 点击劫持

  11. 第三方资源为什么危险

  12. 防盗链到底防什么

Cookie 只是 Session ID 的运输工具。真正决定“谁已经登录、能登录多久、什么时候失效”的,是服务端 Session 管理。很多系统 Cookie 属性配置得不错,却因为 Session 不轮换、退出不失效或超时策略混乱而留下风险。

Session 生命周期

Session ID 必须不可预测

Session ID 相当于临时登录凭证。不能使用自增 ID、用户名、时间戳或 Math.random()

// 错误:容易猜测
const sid = `${userId}-${Date.now()}`;

应使用密码学安全随机数:

import crypto from 'node:crypto';

function createSessionId() {
  return crypto.randomBytes(32).toString('base64url');
}

服务端只通过 Session ID 查找会话,不应该从 ID 本身解析用户权限。

Session 固定攻击

Session fixation 的思路不是偷走用户已经登录后的 Session,而是先让受害者使用攻击者已知的 Session ID,再等待用户登录。如果登录后服务端继续沿用原 ID,攻击者也能使用它进入账号。

Session 固定攻击

危险流程:

攻击者得到 sid=KNOWN
→ 诱导用户使用 sid=KNOWN
→ 用户登录
→ 服务端在原 Session 上标记 authenticated=true
→ 攻击者继续使用 sid=KNOWN

核心防御是:身份等级发生变化时重新生成 Session ID。

function login(req, res, user) {
  destroySession(req.sessionId);

  const newSessionId = createSessionId();
  sessions.set(newSessionId, {
    userId: user.id,
    createdAt: Date.now(),
    lastSeenAt: Date.now()
  });

  setSessionCookie(res, newSessionId);
}

除了登录,权限提升、管理员二次认证、找回密码完成后也适合轮换 Session ID。

两类超时不能混为一谈

空闲超时

用户一段时间没有活动就失效,例如 30 分钟:

const IDLE_TIMEOUT = 30 * 60 * 1000;

if (Date.now() - session.lastSeenAt > IDLE_TIMEOUT) {
  sessions.delete(sessionId);
  return unauthorized();
}

每次有效请求更新 lastSeenAt

绝对超时

无论用户是否持续操作,到达固定时长后都必须重新登录,例如 12 小时:

const ABSOLUTE_TIMEOUT = 12 * 60 * 60 * 1000;

if (Date.now() - session.createdAt > ABSOLUTE_TIMEOUT) {
  sessions.delete(sessionId);
}

只有空闲超时会让持续活动的被盗 Session 长期有效;只有绝对超时又可能频繁打断正常用户。两者通常组合使用。

退出登录必须服务端失效

只清除浏览器 Cookie 不够:

// 只清浏览器,不删除服务端 Session
res.clearCookie('__Host-session');

如果旧 Session ID 曾被复制,攻击者仍可能使用。正确做法:

sessions.delete(req.sessionId);
res.clearCookie('__Host-session', { path: '/' });

删除 Cookie 时 Domain、Path 等属性要与设置时匹配。

修改密码和账号风险事件

修改密码后通常应让其他 Session 失效,至少提供“退出其他设备”。可以给用户维护 Session 列表:

{
  id: 'session-id',
  userId: 1001,
  deviceName: 'Edge on Windows',
  createdAt: 123,
  lastSeenAt: 456
}

设备信息只能用于展示和风险判断,不能单独作为强身份凭证,因为 User-Agent、IP 等都可能变化或伪造。

分布式 Session

多实例部署时,把 Session 只存进单个 Node.js 进程内存会出现:

  • 请求落到另一个实例后“掉登录”;

  • 实例重启后全部失效;

  • 无法统一注销和查看设备。

常见方案是 Redis 等共享存储,并设置 TTL。即使使用共享存储,仍要考虑随机 ID、轮换、超时和注销。

伪代码:

await redis.set(`session:${sid}`, JSON.stringify(session), {
  EX: 30 * 60
});

“记住我”不能等同于无限 Session

较安全的设计是使用长期、可撤销、可轮换的 Remember Token,换取新的短期 Session,而不是把普通 Session 有效期直接设置为一年。数据库只保存 Token 的哈希值,泄露后可以单独撤销。

常见误区

Session ID 足够长就不需要 HTTPS

不对。再随机的 ID 通过明文 HTTP 传输,也可能被中间人截获。

JWT 不需要 Session 管理

JWT 仍然有过期、撤销、刷新 Token 轮换和设备管理问题,只是状态位置不同。

IP 一变就强制退出最安全

移动网络、代理和 IPv4/IPv6 切换会造成大量误伤。IP 更适合作为风险信号,而非唯一绑定条件。

小结

安全 Session 的关键不是某个库,而是一整套生命周期:安全随机 ID、登录后轮换、空闲和绝对超时、服务端注销、风险事件批量失效以及分布式一致管理。

参考资料