本文档旨在深入解析 HTTP Cookie 的工作原理、核心属性、安全机制以及在现代 Web 开发中的最佳实践。
1. Cookie 的本质:HTTP 的状态记忆
HTTP 协议本身是无状态 (Stateless)的。如果没有 Cookie,服务器无法区分两个请求是否来自同一个用户。
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
核心流程图解:
浏览器-> 发送登录请求 ->服务器
服务器-> 验证通过 -> 发送HTTP 响应 (Response):
- 注意:HTTP 响应不仅仅包含 HTML 内容 (Body),还包含响应头 (Headers)。
Set-Cookie正是最重要的响应头之一。
HTTP/1.1 200 OK Content-Type: application/json Set-Cookie: session_id=xyz; HttpOnly; Path=/ <-- 就在这里! {"status": "success"}- 注意:HTTP 响应不仅仅包含 HTML 内容 (Body),还包含响应头 (Headers)。
浏览器-> 收到响应,解析 Header,自动保存 Cookie -> (本地存储)
浏览器-> 发起新请求 (如获取个人资料) -> 发送HTTP 请求 (Request):
- 注意:
Cookie字段和Content-Type字段在协议层面完全是同一种东西,都是请求头。区别仅在于Cookie是浏览器自动注入的,而Content-Type通常是代码指定的。
GET /api/me HTTP/1.1 Host: example.com Content-Type: application/json <-- 普通 Header (代码控制) Cookie: session_id=xyz; theme=dark <-- 自动携带的 Header (浏览器控制)- 注意:
服务器-> 读取
Cookie头,识别用户身份 ->响应数据
2. 核心属性深度解析 (Key Attributes)
一个 Cookie 不仅仅是Name=Value,它的一系列属性决定了它的安全性、生命周期和作用域。
2.1 HttpOnly (安全之锁)
- 作用:禁止客户端 JavaScript (通过
document.cookie) 访问该 Cookie。 - 目的:防御 XSS (跨站脚本攻击)。即使黑客的代码在页面上运行,也无法读取用户的 Session ID。
- 代码示例:
Set-Cookie: token=123456; HttpOnly
2.2 Secure (传输安全)
- 作用:浏览器只有在请求协议为HTTPS时才会发送该 Cookie。
- 目的:防止 Cookie 在未加密的 HTTP 连接中被中间人 (MITM) 窃听。
2.3 SameSite (防 CSRF 神器)
控制 Cookie 是否随跨站请求发送。
Strict:完全禁止跨站发送。只有当前网页 URL 与请求目标一致时才发送。最安全,但用户体验稍差(如从邮件链接点进来可能是未登录状态)。Lax(默认):允许部分“安全”的跨站请求(如链接跳转<a>、预加载<link>)携带 Cookie,但 POST 表单提交不携带。平衡了安全与体验。None:允许跨站发送(必须同时开启Secure)。用于第三方 Cookie 场景(如广告追踪、嵌入式 iframe)。
2.4 Domain & Path (作用域)
Domain:指定 Cookie 所属的域名。- 如果不指定,默认为当前域名(不包含子域名)。
- 如果指定
Domain=example.com,则api.example.com等子域名也能共享该 Cookie。
Path:指定 Cookie 在哪个路径下有效(默认为/,即全站有效)。
2.5 Max-Age / Expires (生命周期)
- Session Cookie:不设置过期时间。浏览器关闭后自动删除。
- Permanent Cookie:设置了
Max-Age(秒数) 或Expires(日期)。即使关闭浏览器,只要没过期,数据依然存在硬盘上。
3. 实战代码演示 (Node.js + 原生 JS)
为了涵盖完整的生命周期,我们将分为三个场景进行演示。
场景 1:浏览器 JS 操作 Cookie (非 HttpOnly)
适用于存储用户偏好(如主题、语言),不涉及敏感信息。
// === 1. 写入 Cookie ===// 设置一个名为 'theme' 的 cookie,有效期 7 天constdays=7;constdate=newDate();date.setTime(date.getTime()+(days*24*60*60*1000));// 注意:JS 设置的 Cookie 默认没有 HttpOnly 属性document.cookie=`theme=dark; expires=${date.toUTCString()}; path=/`;// === 2. 读取 Cookie ===// document.cookie 返回的是一个长字符串:"theme=dark; other=value"console.log("所有 Cookie:",document.cookie);// 解析特定 Cookie 的辅助函数functiongetCookie(name){constvalue=`;${document.cookie}`;constparts=value.split(`;${name}=`);if(parts.length===2)returnparts.pop().split(';').shift();}console.log("Theme 值:",getCookie('theme'));// 输出 "dark"场景 2:服务端验证登录并注入 Cookie (Set-Cookie)
这是最关键的一步。当用户登录成功后,后端生成 Token 并通过Set-Cookie头下发给浏览器。这里以Java Spring Boot为例。
importorg.springframework.http.ResponseCookie;importorg.springframework.http.HttpHeaders;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.servlet.http.HttpServletResponse;@RestControllerpublicclassAuthController{@PostMapping("/api/login")publicStringlogin(HttpServletResponseresponse){// 1. 验证用户名密码 (伪代码)// User user = authService.verify(credentials);// 2. 生成 Session ID 或 JWT TokenStringuserId="user_123";StringsessionToken="signed_token_for_"+userId;// 3. 注入 Cookie (核心步骤)// 使用 Spring 的 ResponseCookie 构建器(支持 SameSite 属性)ResponseCookiecookie=ResponseCookie.from("auth_token",sessionToken).httpOnly(true)// 【安全】禁止前端 JS 读取 (防 XSS).secure(true)// 【安全】仅通过 HTTPS 传输.sameSite("Strict")// 【安全】禁止跨站发送 (防 CSRF).path("/")// 全站有效.maxAge(3600)// 有效期 1 小时 (单位:秒).build();// 将 Cookie 添加到响应头response.addHeader(HttpHeaders.SET_COOKIE,cookie.toString());return"{\"status\": \"success\", \"message\": \"Logged in!\"}";}}场景 3:服务端提取 Request Cookie 并解析用户信息
当浏览器发起后续请求(如/api/me)时,会自动带上 Cookie。后端使用@CookieValue注解轻松获取。
importorg.springframework.web.bind.annotation.CookieValue;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassUserController{@GetMapping("/api/me")publicStringgetUserInfo(@CookieValue(name="auth_token",required=false)Stringtoken){System.out.println("收到的 Token: "+token);// 1. 验证 Token 是否存在if(token==null){// 返回 401 Unauthorizedreturn"{\"error\": \"未登录 (无 Cookie)\"}";}// 2. 验证/解析 Token (伪代码)// UserData userData = tokenService.verifyAndDecode(token);// 3. 返回用户信息return"{\"user\": {\"id\": \"user_123\", \"name\": \"Alice\"}}";}}4. 终极对比:Cookie vs LocalStorage
| 特性 | Cookie (HttpOnly) | LocalStorage |
|---|---|---|
| 主要用途 | 身份验证 (Session ID) | 用户偏好、缓存数据 |
| JS 访问权限 | 不可见(安全) | 可见(易被 XSS 窃取) |
| 数据传输 | 自动携带(每次请求 Header) | 手动携带(需 JS 写入 Header) |
| 容量限制 | 小 (~4KB) | 大 (~5MB) |
| SSR 支持 | 完美支持(首屏即可识别用户) | 不支持(需等待 JS 执行) |
| 性能影响 | 数据过大会消耗带宽 | 无网络消耗 |
4.1 核心拷问:为什么不用 LocalStorage 存用户信息/Token?
尽管 LocalStorage 容量大且 API 简单,但在存储身份令牌 (Auth Token)时,它有两个致命缺陷:
安全性漏洞 (XSS 风险)
- LocalStorage 对当前页面运行的所有 JS 代码都是全透明的。
- 一旦你的网站出现 XSS 漏洞(被注入了恶意脚本),黑客只需一行代码
localStorage.getItem('token')就能偷走用户的 Token。 - 对比:设置了
HttpOnly的 Cookie 是不可见的,黑客即使能运行脚本,也读不到 Token。
服务端渲染 (SSR) 失效
- LocalStorage 存储在硬盘上,只有 JS 代码能读取。浏览器发起 HTTP 请求时,不会带上 LocalStorage 的内容。
- 这意味着服务器在收到首屏请求时,根本不知道用户是谁,只能渲染“未登录”状态的 HTML。页面加载后,必须等 JS 运行、读取 Storage、再发 AJAX 请求才能获取用户信息。这会导致页面闪烁和 SEO 问题。
- 对比:Cookie 会随请求自动发送,服务器在渲染 HTML 时就已经知道用户身份,可以直接返回包含用户信息的完整页面。
最佳实践总结:
- 敏感 Token:永远存放在HttpOnly Cookie中。
- 非敏感数据:存放在 LocalStorage 中以节省带宽。
5. 安全进阶:前端/黑客能否伪造 Cookie?
这是一个常见疑问:既然 Cookie 只是 Header,JS 能不能构造一个假的 Cookie 发给后端?
答案:几乎不可能成功。存在三道防线:
浏览器底层拦截 (Forbidden Headers)
- JS 的
fetch或XMLHttpRequestAPI严禁程序员手动设置Cookie请求头。 - 如果你尝试
headers: {'Cookie': 'admin=true'},浏览器会直接忽略或抛错。
- JS 的
HttpOnly 保护 (防覆盖)
- 即使黑客尝试通过
document.cookie = "auth=fake"在本地写入假的 Cookie,但如果该 Cookie 已经被服务端标记为HttpOnly,JS 是无法覆盖它的。
- 即使黑客尝试通过
后端签名 (Signed Cookies)
- 即使黑客绕过了前两道防线,或者在无 HttpOnly 的情况下修改了 Cookie 值。
- 签名机制:后端在颁发 Cookie 时,不仅存储值,还会用服务器独有的Secret Key生成一个签名(Signature)。
- 校验:当请求回来时,服务器会重新计算签名。如果黑客篡改了值但算不出正确的签名,服务器会直接拒绝请求。