Cookie 安全完全指南:HttpOnly、SameSite、CHIPS 与现代会话防护

深入解析 Cookie 安全机制,涵盖 HttpOnly、Secure、SameSite、CHIPS 分区 Cookie、__Host- 前缀等现代安全特性,附 Node.js 与 Spring Boot 实战代码,帮助开发者构建安全的会话管理方案。

安全与密码 2026-05-31 15 分钟

浏览器 Cookie 是 Web 会话管理的基石,但也是最常被误解和误用的安全机制之一。根据 OWASP 2025 年度报告,超过 60% 的 Web 应用存在 Cookie 配置不当的问题,其中 HttpOnly 缺失和 SameSite 未设置是最常见的两类漏洞。在第三方 Cookie 逐步淘汰、CHIPS(Cookies Having Independent Partitioned State)成为新标准的 2026 年,理解 Cookie 安全不仅是防御 XSS 和 CSRF 的基础,更是现代 Web 架构设计的必修课。

🔐 一、Cookie 安全属性深度解析

很多开发者知道 HttpOnlySecure 这两个属性,但对它们的工作原理和组合效果却知之甚少。我们先从基础开始,逐个拆解每个安全属性的实际作用。

1.1 HttpOnly:阻断 XSS 的 Cookie 窃取

HttpOnly 属性是防御 XSS(Cross-Site Scripting)攻击的第一道防线。当一个 Cookie 被标记为 HttpOnly 时,JavaScript 无法通过 document.cookie 读取它,这意味着即使攻击者成功注入了 XSS 脚本,也无法直接窃取用户的会话令牌。

// ❌ 危险:未设置 HttpOnly 的 Cookie
// 服务端设置
Set-Cookie: session_id=abc123; Path=/

// 攻击者的 XSS 脚本可以轻松窃取
const stolen = document.cookie
// => "session_id=abc123" — 攻击者拿到了用户的会话!
// ✅ 安全:设置了 HttpOnly 的 Cookie
// 服务端设置
Set-Cookie: session_id=abc123; Path=/; HttpOnly

// 攻击者的 XSS 脚本无法读取
const stolen = document.cookie
// => "" — 空字符串,Cookie 被保护了!

⚠️ 警告: HttpOnly 不能防御 XSS 本身,它只是阻止 XSS 攻击者直接窃取 Cookie。攻击者仍然可以通过 XSS 发起伪造请求(借助浏览器自动携带 Cookie 的特性)。所以 HttpOnly 必须与 CSP(Content Security Policy)配合使用。

1.2 SameSite:CSRF 防护的终极方案

SameSite 属性是近年来 Cookie 安全最重要的进展。它通过限制跨站请求中 Cookie 的发送行为,从浏览器层面阻断了 CSRF(Cross-Site Request Forgery)攻击。

SameSite 有三个值,它们的行为差异非常大:

属性值 跨站 GET 请求 跨站 POST 请求 跨站 <img>/<iframe> CSRF 防护 推荐场景
None ✅ 发送 ✅ 发送 ✅ 发送 ❌ 无防护 跨站 SSO(必须配合 Secure
Lax ✅ 发送 ❌ 不发送 ❌ 不发送 ⚠️ 部分防护 大多数 Web 应用的默认推荐
Strict ❌ 不发送 ❌ 不发送 ❌ 不发送 ✅ 完整防护 高安全场景(银行、支付)
// ❌ 过去的默认行为(现在已不安全)
Set-Cookie: session_id=abc123; Path=/

// ✅ 推荐:使用 SameSite=Lax(现代浏览器的默认值)
Set-Cookie: session_id=abc123; Path=/; SameSite=Lax; HttpOnly; Secure

// ✅ 高安全场景:使用 SameSite=Strict
// 注意:用户从外部链接点击进入时不会携带 Cookie,需要二次验证
Set-Cookie: session_id=abc123; Path=/; SameSite=Strict; HttpOnly; Secure

💡 提示: 从 Chrome 80+ 开始,未设置 SameSite 的 Cookie 默认行为已从 None 改为 Lax。这意味着如果你的代码没有显式设置 SameSite,跨站 POST 请求将不会携带 Cookie,这可能会破坏一些依赖 Cookie 的第三方集成。

1.3 Secure 与 __Host- 前缀

Secure 属性确保 Cookie 只在 HTTPS 连接中传输,防止中间人攻击窃取 Cookie。而 __Host- 前缀是更进一步的安全加固:

// 使用 __Host- 前缀的 Cookie 必须满足:
// 1. 必须设置 Secure
// 2. 不能设置 Domain(锁定到当前域名)
// 3. 必须设置 Path=/

// ✅ 使用 __Host- 前缀的高安全 Cookie
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

// ❌ 这会失败 — __Host- 不能设置 Domain
Set-Cookie: __Host-session=abc123; Path=/; Secure; Domain=example.com

__Host- 前缀的价值在于它能防止子域名覆盖上级域名的 Cookie。考虑这个攻击场景:攻击者控制了 evil.example.com,设置了一个 Domain=.example.com 的 Cookie 来覆盖 example.com 的会话。如果原始 Cookie 使用了 __Host- 前缀,这种攻击就无法成功。

🚀 二、CHIPS 与分区 Cookie:第三方 Cookie 的替代方案

2026 年,Chrome 已经全面淘汰第三方 Cookie,取而代之的是 CHIPS(Cookies Having Independent Partitioned State)。这是理解现代 Cookie 安全的关键变化。

2.1 什么是 CHIPS?

传统的第三方 Cookie 有一个致命问题:它允许跨站追踪。广告网络通过在多个网站上嵌入 <iframe><script>,利用第三方 Cookie 追踪用户行为。

CHIPS 通过"分区"机制解决了这个问题:Cookie 的作用域不再只是 Domain + Path,而是变成了 Top-Level Site + Domain + Path。这意味着 tracker.comsite-a.comsite-b.com 上设置的 Cookie 将被隔离,无法互相访问。

// 传统第三方 Cookie(已废弃)
// tracker.com 在 site-a.com 上设置
Set-Cookie: user_id=123; Domain=tracker.com; SameSite=None; Secure

// 在 site-b.com 的请求中,这个 Cookie 也会被发送
// => tracker.com 可以跨站追踪用户!

// CHIPS 分区 Cookie(新标准)
// tracker.com 在 site-a.com 上设置
Set-Cookie: user_id=123; Domain=tracker.com; SameSite=None; Secure; Partitioned

// 在 site-b.com 的请求中,这个 Cookie 不会被发送
// => 用户隐私得到保护

2.2 CHIPS 迁移实战

如果你的服务涉及跨站 Cookie(如嵌入式支付、第三方登录、CDN 服务),需要尽快迁移到 CHIPS:

// Node.js Express 示例:设置分区 Cookie
const express = require('express');
const app = express();

app.get('/embed/widget', (req, res) => {
  // ❌ 旧方式:第三方 Cookie(Chrome 已阻止)
  res.cookie('widget_session', 'abc123', {
    domain: '.widget-service.com',
    secure: true,
    sameSite: 'none',
  });

  // ✅ 新方式:CHIPS 分区 Cookie
  res.cookie('widget_session', 'abc123', {
    domain: '.widget-service.com',
    secure: true,
    sameSite: 'none',
    partitioned: true,  // 关键:添加分区标记
  });

  res.json({ status: 'ok' });
});
// Spring Boot 示例:设置分区 Cookie
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;

@RestController
public class WidgetController {

    @GetMapping("/embed/widget")
    public ResponseEntity<Map<String, String>> getWidget(
            HttpServletResponse response) {

        Cookie cookie = new Cookie("widget_session", "abc123");
        cookie.setDomain(".widget-service.com");
        cookie.setSecure(true);
        cookie.setPath("/");
        cookie.setAttribute("SameSite", "None");
        cookie.setAttribute("Partitioned", ""); // CHIPS 分区标记

        response.addCookie(cookie);

        return ResponseEntity.ok(Map.of("status", "ok"));
    }
}

📌 记住: 使用 Partitioned 属性时,SameSite=NoneSecure 是必须的。分区 Cookie 天然要求 HTTPS 环境。

2.3 CHIPS 对架构的影响

CHIPS 不仅仅是加一个属性那么简单,它对系统架构有深远影响:

影响维度 第三方 Cookie 时代 CHIPS 时代 需要的改造
跨站 SSO 一个 Cookie 全站通用 每个站点独立的 Cookie 改用 OAuth2 + 前端 Token
嵌入式分析 <iframe> 共享 Cookie Cookie 被分区隔离 改用 Server-Side 事件上报
CDN 认证 共享认证 Cookie 无法跨站共享 改用签名 URL 或 Token
支付网关 嵌入式 Cookie 追踪 Cookie 被分区 改用 Payment Request API

⚠️ 三、常见安全陷阱与避坑指南

3.1 陷阱一:Cookie 覆盖攻击

子域名可以为父域名设置 Cookie,这是一个常被忽视的安全风险:

// 攻击者控制了 evil.example.com,执行以下代码
document.cookie = "session_id=hacked; Domain=.example.com; Path=/";

// 现在访问 example.com 时,这个恶意 Cookie 会被发送
// 如果服务端只检查 session_id 的值而不验证来源,就会被攻击

防御方案:

// ✅ 使用 __Host- 前缀阻止 Cookie 覆盖
// 服务端设置(只能通过 HTTPS 从精确域名设置)
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

// ✅ 服务端验证 Cookie 的完整性
const crypto = require('crypto');

function signCookie(value, secret) {
  const signature = crypto
    .createHmac('sha256', secret)
    .update(value)
    .digest('base64url');
  return `${value}.${signature}`;
}

function verifyCookie(signed, secret) {
  const [value, sig] = signed.split('.');
  const expected = crypto
    .createHmac('sha256', secret)
    .update(value)
    .digest('base64url');
  return sig === expected ? value : null;
}

// 设置签名 Cookie
const sessionId = generateSessionId();
const signed = signCookie(sessionId, process.env.COOKIE_SECRET);
res.cookie('__Host-session', signed, {
  path: '/',
  secure: true,
  httpOnly: true,
  sameSite: 'lax',
  partitioned: false,
});

3.2 陷阱二:LocalStorage 替代 Cookie 的误区

很多开发者认为"把 Token 存在 LocalStorage 就安全了",这是一个严重的误解:

存储方式 XSS 防护 CSRF 防护 自动发送 推荐场景
Cookie + HttpOnly ✅ JS 无法读取 ⚠️ 需要 SameSite ✅ 浏览器自动 传统 Web 应用
LocalStorage + Bearer Token ❌ XSS 可读取 ✅ 不会自动发送 ❌ 需手动添加 SPA + API 网关
In-Memory Token ✅ 刷新即丢失 ✅ 不会自动发送 ❌ 需手动添加 高安全 SPA
Cookie(无 HttpOnly) ❌ XSS 可读取 ❌ 可能被 CSRF ✅ 浏览器自动 ❌ 不推荐

关键结论: 最安全的 SPA 方案是将 Access Token 存储在内存中(非 LocalStorage),配合 HttpOnly Refresh Cookie。这样即使 XSS 发生,攻击者只能拿到短期有效的 Access Token;而 Refresh Token 由于 HttpOnly 保护无法被 JavaScript 读取。

3.3 陷阱三:Session 固定攻击

Session 固定(Session Fixation)是一种容易被忽视的攻击方式。攻击者先获取一个有效的 Session ID,然后诱导用户使用这个 Session ID 登入:

// ❌ 危险:登录后不重新生成 Session ID
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = authenticate(username, password);
  if (user) {
    req.session.userId = user.id; // 复用了旧的 Session ID
    res.json({ success: true });
  }
});

// ✅ 安全:登录后重新生成 Session ID
const crypto = require('crypto');

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = authenticate(username, password);
  if (user) {
    // 保存旧 Session 数据
    const oldSession = { ...req.session };

    // 重新生成 Session ID
    req.session.regenerate((err) => {
      if (err) return res.status(500).json({ error: 'Session error' });

      // 恢复 Session 数据
      Object.assign(req.session, oldSession);
      req.session.userId = user.id;

      res.json({ success: true });
    });
  }
});

3.4 陷阱四:Cookie 的 Domain 属性陷阱

// ❌ 危险:设置过宽的 Domain
Set-Cookie: session=abc; Domain=.com; Secure
// 这会让所有 .com 网站都能访问这个 Cookie!

// ❌ 常见错误:多加了一个点
Set-Cookie: session=abc; Domain=.example.com; Secure
// 虽然浏览器会忽略开头的点,但语义上不清晰

// ✅ 正确:不设置 Domain(默认锁定到当前精确域名)
Set-Cookie: session=abc; Path=/; Secure; HttpOnly; SameSite=Lax
// 只有 example.com 可以访问,子域名不能访问

// ✅ 需要子域名共享时:明确设置
Set-Cookie: session=abc; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Lax
// example.com 和 sub.example.com 都可以访问

💡 四、现代 Cookie 安全最佳实践

4.1 完整的 Cookie 安全配置清单

根据你的应用场景,选择合适的安全配置:

// 场景 1:传统 Web 应用的会话 Cookie(推荐配置)
Set-Cookie: __Host-session=abc123;
  Path=/;
  Secure;
  HttpOnly;
  SameSite=Lax;
  Max-Age=3600

// 场景 2:需要跨站使用的 Cookie(如嵌入式小部件)
Set-Cookie: widget_token=xyz789;
  Domain=.widget-service.com;
  Path=/;
  Secure;
  HttpOnly;
  SameSite=None;
  Partitioned;
  Max-Age=86400

// 场景 3:纯前端可读的 Cookie(如主题偏好,非敏感数据)
Set-Cookie: theme=dark;
  Path=/;
  SameSite=Lax;
  Max-Age=31536000

4.2 Express.js 完整安全配置

// cookie-security.js — 生产级 Cookie 安全中间件
const crypto = require('crypto');
const express = require('express');
const session = require('express-session');

const app = express();

// 生产级 Session 配置
app.use(session({
  secret: process.env.SESSION_SECRET, // 至少 32 字节的随机密钥
  name: '__Host-session',             // 使用 __Host- 前缀
  cookie: {
    secure: true,                     // 仅 HTTPS
    httpOnly: true,                   // JS 不可读
    sameSite: 'lax',                  // CSRF 防护
    maxAge: 60 * 60 * 1000,           // 1 小时过期
    path: '/',                        // 全站可用
    // 不设置 domain — 锁定到当前域名
  },
  resave: false,                      // 不强制保存未修改的 Session
  saveUninitialized: false,           // 不保存空 Session
  rolling: true,                      // 每次请求刷新过期时间
}));

// 登录后重新生成 Session ID(防 Session 固定)
app.post('/api/login', async (req, res) => {
  const user = await authenticateUser(req.body);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  // 防止 Session 固定攻击
  const sessionData = { ...req.session };
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'Session error' });
    Object.assign(req.session, sessionData);
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ success: true });
  });
});

// 登出时销毁 Session
app.post('/api/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout error' });
    res.clearCookie('__Host-session');
    res.json({ success: true });
  });
});

4.3 安全审计脚本

// 自动化 Cookie 安全审计脚本 audit-cookies.js
const https = require('https');

function auditCookies(url) {
  return new Promise((resolve) => {
    https.get(url, (res) => {
      const cookies = res.headers['set-cookie'] || [];
      const results = cookies.map((cookie) => ({
        cookie: cookie.split(';')[0],
        httpOnly: /httponly/i.test(cookie),
        secure: /secure/i.test(cookie),
        sameSite: /samesite=/i.test(cookie),
        partitioned: /partitioned/i.test(cookie),
      }));
      resolve(results);
    });
  });
}

// 使用示例:auditCookies('https://your-site.com').then(console.log)

📊 五、总结与行动清单

Cookie 安全不是一个可以"设置后遗忘"的话题。随着浏览器策略的持续演进,开发者需要持续关注最新的安全特性。

立即行动清单:

  • ✅ 所有会话 Cookie 设置 HttpOnly; Secure; SameSite=Lax
  • ✅ 敏感场景使用 __Host- 前缀防止 Cookie 覆盖
  • ✅ 登入/登出时重新生成 Session ID
  • ✅ 跨站 Cookie 迁移到 CHIPS(Partitioned 属性)
  • ✅ 定期审计 Cookie 配置,使用自动化脚本
  • ❌ 不要将敏感数据存在非 HttpOnly 的 Cookie 中
  • ❌ 不要使用 SameSite=None 而不加 Partitioned
  • ❌ 不要设置过宽的 Domain 属性

关键结论: 2026 年的 Cookie 安全最佳实践是:使用 __Host- 前缀 + HttpOnly + Secure + SameSite=Lax 的组合作为默认配置,仅在明确需要跨站场景时才使用 SameSite=None; Partitioned。记住,Cookie 安全是纵深防御的一层,必须与 CSP、HTTPS、输入验证等其他安全措施配合使用。

相关工具推荐:使用 jsjson.comJSON 格式化工具 调试 Cookie 相关的 API 响应,MD5/SHA 哈希工具 验证 Cookie 签名的正确性。

📚 相关文章