一文彻底搞懂 Cookie 与 Token:从底层机制到实战场景全解析
本文从 Cookie 的底层传输机制、浏览器存储原理,到 Token 认证方案的本质区别,结合流程图和代码示例,力求把这个问题讲透。
一、先厘清概念:Cookie 和 Token 不在同一维度
很多人把 Cookie 和 Token 放在一起比较,但其实它们不是同一层面的东西:
- Cookie 是一种存储和传输机制。 它是浏览器提供的能力:服务器通过
Set-Cookie响应头把数据存到浏览器里,之后浏览器每次请求同域地址时自动带上。它解决的是 "怎么把凭证带过去" 的问题。 - Token 是一种认证凭证。 比如 JWT,它本身是一段包含用户身份信息的加密字符串。它解决的是 "怎么证明你是谁" 的问题。
两者不是对立关系。你完全可以把 Token 存在 Cookie 里传输,也可以存在 localStorage 里通过 HTTP Header 手动传。
二、Cookie 的底层机制全解析
2.1 Cookie 的本质:纯文本键值对
Cookie 在 HTTP 协议中的存在形式非常朴素——就是字符串。
服务端设置时,通过响应头逐条写入:
HTTP/1.1 200 OK
Set-Cookie: theme=dark; Path=/; Max-Age=2592000
Set-Cookie: lang=zh-CN; Path=/; HttpOnly
Set-Cookie: PHPSESSID=abc123; Path=/; HttpOnly; Secure
注意:每条 Cookie 必须单独一个 Set-Cookie 头,不能合并,这是 HTTP 规范规定的。在代码层面就是多次调用:
// PHP
setcookie("theme", "dark", time() + 2592000);
setcookie("lang", "zh-CN", time() + 2592000);
// session_start() 自动加一条 PHPSESSID
// Java Servlet
response.addCookie(new Cookie("theme", "dark"));
response.addCookie(new Cookie("lang", "zh-CN"));
浏览器发送时,把同域下所有 Cookie 拼成一行发回去:
Cookie: theme=dark; lang=zh-CN; PHPSESSID=abc123
就这么简单,key=value 用分号拼接,纯文本。
2.2 Cookie 值没有格式约束
Cookie 的 value 部分完全自由,你想存什么都行:
# 普通字符串
theme=dark
# 一个数字
visit_count=42
# 一段 JSON(需要 URL 编码)
prefs=%7B%22fontSize%22%3A14%2C%22color%22%3A%22red%22%7D
# 框架加密后的客户端 Session 数据
laravel_session=eyJpdiI6IkFCQ0RF...
# 一个毫无意义的随机 ID(Session ID)
PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120
至于这个字符串是明文还是加密的、是 JSON 还是随机 ID,完全由写入方自己决定,HTTP 协议不关心。
2.3 谁能写入 Cookie
不只是服务端。客户端 JS 同样可以写入:
document.cookie = "theme=dark; max-age=2592000; path=/";
但有一个重要限制:如果服务端设置 Cookie 时加了 HttpOnly 标志,那这条 Cookie 客户端 JS 完全读不到也改不了,只有浏览器发请求时会自动带上。Session ID 通常都会加这个标志,防止 XSS 攻击时被 JS 偷走。
2.4 浏览器如何存储 Cookie
浏览器收到 Set-Cookie 后,不是拼成一个字符串存起来的,而是解析后按条目独立存储。可以理解为浏览器内部维护了一张关系型表:
| 域名 | 路径 | 键 | 值 | 过期时间 | HttpOnly | Secure | SameSite |
|---|---|---|---|---|---|---|---|
| example.com | / | theme | dark | 2026-04-19 | 否 | 否 | — |
| example.com | / | lang | zh-CN | 2026-04-19 | 是 | 否 | — |
| example.com | / | PHPSESSID | abc123 | 会话级 | 是 | 是 | — |
每条 Cookie 是一条独立记录,包含完整的键、值和所有属性。实际存储位置取决于浏览器实现,比如 Chrome 用的是本地的一个 SQLite 数据库文件。
2.5 浏览器发送时的筛选规则
浏览器每次发请求前,会对这张表执行一次 多条件筛选,用伪 SQL 来表达:
SELECT key, value FROM cookies
WHERE domain MATCHES '当前请求域名'
AND path MATCHES '当前请求路径'
AND (secure = false OR 当前协议是HTTPS)
AND expires > NOW()
AND samesite规则通过;
筛选出来的结果集,拼成 key=value; key=value 发出去。
各属性的含义:
| 属性 | 作用 |
|---|---|
Domain | 作用域名,example.com 的 Cookie 不会发给 other.com |
Path | 作用路径,Path=/admin 的 Cookie 访问 /api 时不会带上 |
Secure | 只有 HTTPS 请求才会携带 |
Max-Age / Expires | 过期时间,过期后自动从存储表中删除 |
HttpOnly | 禁止 JS 访问,防 XSS |
SameSite | 控制跨站请求时是否携带,防 CSRF |
注意:这些属性不会发送给服务端,它们是浏览器自己用的控制信息。服务端收到的永远只是那一行 key=value; key=value 的纯文本。
2.6 服务端如何解析 Cookie
服务端框架在请求进入业务代码之前,就已经自动把 Cookie: a=1; b=2; c=3 按分号拆分、解码、封装成了语言原生的数据结构。你永远不需要手动 split:
// PHP — 直接就是关联数组
$_COOKIE['theme'] // "dark"
$_COOKIE['lang'] // "zh-CN"
$_COOKIE['PHPSESSID'] // "abc123"
// Java Servlet — 直接就是对象数组
Cookie[] cookies = request.getCookies();
for (Cookie c : cookies) {
c.getName(); // "theme"
c.getValue(); // "dark"
}
2.7 Cookie 完整生命周期流程图
┌──────────────────────────────────────────────────┐
服务端 │ 浏览器 │ 服务端
│ │
Set-Cookie: a=1 ──→ 解析属性,存入结构化表 │
Set-Cookie: b=2 ──→ 解析属性,存入结构化表 │
Set-Cookie: c=3 ──→ 解析属性,存入结构化表 │
│ │
│ 发起新请求时: │
│ ① 多条件筛选(域名+路径+Secure+过期+SameSite) │
│ ② 提取匹配的键值对 │
│ ③ 拼接为字符串 │
│ ──→ 收到 Cookie: a=1; b=2; c=3
│ │ 框架自动拆分为对象/数组/字典
│ │ 业务代码直接使用
└──────────────────────────────────────────────────┘
三个阶段的形态各不相同:设置时一条一条来,存储时结构化独立保存,发送时才拼成分号分隔的字符串。
三、Cookie 里常见的内容分类
打开浏览器开发者工具看 Cookie,你往往会看到一堆内容。它们的来源和用途完全不同:
3.1 Session ID(框架自动写入)
最核心的一条,由框架自动管理。你只需要调用类似 session_start() 的方法,框架自动完成生成 → 设置 → 读取的全流程。
JSESSIONID=3F2504E0-4F89-11D3-9A0C-0305E82C3301 # Java (Tomcat)
PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120 # PHP
ASP.NET_SessionId=abcdef123456 # ASP.NET
3.2 客户端 Session(框架加密写入)
有些框架不把 Session 存在服务端,而是把整个 Session 数据加密后直接塞进 Cookie 返回给浏览器。比如 Laravel 默认的 cookie driver、Django 的 cookie session backend、Rails 的 CookieStore。
laravel_session=eyJpdiI6IkpRb0FQ...(一长串加密数据)
这和 JWT 的思路很像——信息存在客户端,服务端无状态。
3.3 业务数据(开发者手动写入)
开发者主动设置的用户偏好等信息:
setcookie("theme", "dark", time() + 86400 * 30);
setcookie("lang", "zh-CN", time() + 86400 * 30);
3.4 认证辅助信息
比如"记住我"功能的持久化 Token、CSRF Token 等:
remember_token=a3f5b8c9e2d1...
XSRF-TOKEN=encrypted_string...
3.5 第三方 Cookie
广告追踪、Google Analytics、社交分享插件等第三方脚本写入的追踪标识。不是你的后端写的,而是引入的第三方 JS 自动写入。
四、真正的对比:Cookie-Session 方案 vs Token 方案
当我们说"Cookie vs Token"时,真正在比较的是两种认证方案。
4.1 Cookie-Session 方案(有状态)
┌────────┐ ┌────────┐ ┌──────────────┐
│ 浏览器 │ ① POST /login │ 服务器 │ ② 创建 Session │ Session 存储 │
│ │ ─────────────────────→ │ │ ──────────────→ │ (内存/Redis) │
│ │ │ │ │ │
│ │ ③ Set-Cookie: │ │ │ sid → { │
│ │ JSESSIONID=abc123 │ │ │ userId: 1 │
│ │ ←───────────────────── │ │ │ role: admin│
│ │ │ │ │ } │
│ │ ④ Cookie: JSESSIONID= │ │ ⑤ 查询 Session │ │
│ │ abc123 │ │ ──────────────→ │ │
│ │ ─────────────────────→ │ │ ←────────────── │ │
│ │ │ │ ⑥ 返回用户信息 │ │
└────────┘ └────────┘ └──────────────┘
- 用户登录后,服务器创建 Session 对象存在服务端(内存、Redis 等)
- 把 Session ID 通过 Cookie 返回给浏览器
- 后续请求浏览器自动带上 Cookie,服务器用 Session ID 去查存储来识别用户
- 核心特征:身份信息存在服务端,Cookie 里只是一把钥匙
4.2 Token 方案(无状态)
┌────────┐ ┌────────┐
│ 客户端 │ ① POST /login │ 服务器 │
│ │ ─────────────────────→ │ │ ② 生成 JWT:
│ │ │ │ header.payload.signature
│ │ ③ 返回 Token │ │ payload = {
│ │ { token: "eyJhb..." } │ │ userId: 1,
│ │ ←───────────────────── │ │ role: "admin",
│ │ │ │ exp: 1234567890
│ │ ④ Authorization: │ │ }
│ │ Bearer eyJhb... │ │
│ │ ─────────────────────→ │ │ ⑤ 验证签名 + 检查过期
│ │ │ │ 直接从 Token 中读取用户信息
│ │ │ │ 无需查询任何存储
└────────┘ └────────┘
- 用户登录后,服务器把用户信息、权限、过期时间等直接编码进 JWT 返回
- 客户端存储 Token,每次请求放在
AuthorizationHeader 中 - 服务器只需验证签名和有效期,不查任何存储
- 核心特征:身份信息存在客户端,服务端不保存状态
4.3 Session ID 与 Token 的本质类比
两者在认证流程中的角色完全对等,区别在于凭证本身承载的信息量不同:
| Session ID | Token (JWT) | |
|---|---|---|
| 内容 | 一串无意义的随机字符串,如 abc123 | 自包含的信息载体,解码后可读出用户ID、角色、过期时间等 |
| 信息存储 | 服务端(内存/Redis/数据库) | 客户端(Token 本身) |
| 验证方式 | 拿 ID 去 Session 存储里查 | 验证签名 + 检查过期时间 |
比喻: Session ID 相当于酒店房卡,刷卡后前台要去系统里查你是谁、住哪间房;Token 相当于身份证,拿到手一看就知道你是谁,只需要验证它是不是真的就行。
五、关键差异对比
| 维度 | Cookie-Session 方案 | Token 方案 |
|---|---|---|
| 状态 | 有状态,服务端需维护 Session 存储 | 无状态,服务端不保存任何信息 |
| 扩展性 | 用户量大时 Session 存储成为瓶颈,多服务器需共享 Session | 天然支持水平扩展,任何节点都能验证 |
| 跨域 | Cookie 受同源策略限制,a.com 的 Cookie 不会发给 b.com | Token 放在 Header 里手动传输,不受域名限制 |
| CSRF 风险 | Cookie 由浏览器自动携带,天然存在 CSRF 风险 | 需要代码主动取出并放入 Header,天然免疫 CSRF |
| 适用范围 | 仅限浏览器环境 | 任何能发 HTTP 请求的客户端 |
| 传输方式 | 浏览器自动携带 | 代码手动设置 Header |
六、哪些场景必须用 Token
6.1 移动端 App
iOS 和 Android 原生应用没有浏览器的 Cookie 管理机制,用 Token 放在请求头里是标准做法。
6.2 微服务架构
服务间调用如果用 Session,每个服务都得访问共享的 Session 存储,耦合很重。JWT 自包含身份信息,服务之间传递 Token 即可各自验证,不依赖中心化存储。
Token
用户 ──→ 网关 ──→ 订单服务(自己验证 Token)
──→ 支付服务(自己验证 Token)
──→ 用户服务(自己验证 Token)
6.3 第三方授权(OAuth 2.0)
比如"用微信登录某 App",微信授权后返回的 Access Token 由 App 持有并调用微信 API。这种跨平台、跨系统的授权场景不可能靠 Cookie 来实现。
6.4 跨域系统
前端部署在 app.example.com,API 在 api.example.com,甚至多个子系统共享同一套用户体系。Cookie 的域名绑定让跨域非常麻烦,Token 没有这个限制。
6.5 无状态 API 服务
面向大量客户端的公开 API(如开放平台),服务端不可能为每个调用者维护 Session,用 Token(API Key / JWT)做认证是唯一合理的选择。
6.6 单点登录(SSO)
用户在一个入口登录后要在多个不同域名的系统间通行,Cookie 受域名限制很难实现,通常都是通过 Token 来传递和验证身份。
七、一张图总结
┌─────────────────────────────────────────────────────────────────┐
│ 认证方案选择 │
├────────────────────────────┬────────────────────────────────────┤
│ Cookie-Session 方案 │ Token 方案 │
│ │ │
│ 浏览器 ←→ 单一域名服务器 │ 任意客户端 ←→ 任意服务端 │
│ │ │
│ ┌──────┐ Cookie自动带 │ ┌──────┐ Header手动带 │
│ │浏览器 │ ───────────────→ │ │客户端 │ ───────────────→ │
│ └──────┘ SessionID │ └──────┘ Token(JWT) │
│ ↓ │ ↓ │
│ ┌──────┐ 查Session存储 │ ┌──────┐ 验证签名即可 │
│ │服务器 │ → [Redis/内存] │ │服务器 │ 无需任何存储 │
│ └──────┘ │ └──────┘ │
│ │ │
│ 适合:传统Web,单体应用 │ 适合:移动端、微服务、跨域、 │
│ │ SSO、开放API、OAuth │
└────────────────────────────┴────────────────────────────────────┘
八、总结
- Cookie 是浏览器提供的"搬运工",负责存储和传输数据。
- Session 是服务端的"记忆",存储用户状态,靠 Session ID 索引。
- Token 是客户端自带的"身份证",自包含用户信息,服务端无需记忆。
只要你的场景超出了"单一域名下的浏览器访问"这个范畴,Token 基本就是必选项。
理解了 Cookie 的底层机制(逐条设置 → 结构化存储 → 筛选拼接 → 框架解析),再理解了 Session ID 和 Token 的本质区别(钥匙 vs 身份证),这个话题就算彻底搞清楚了。
如果这篇文章对你有帮助,欢迎点赞收藏,你的支持是我持续输出的动力!