一文彻底搞懂 Cookie 与 Token:从底层机制到实战场景全解析

9 阅读10分钟

一文彻底搞懂 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 后,不是拼成一个字符串存起来的,而是解析后按条目独立存储。可以理解为浏览器内部维护了一张关系型表:

域名路径过期时间HttpOnlySecureSameSite
example.com/themedark2026-04-19
example.com/langzh-CN2026-04-19
example.com/PHPSESSIDabc123会话级

每条 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,每次请求放在 Authorization Header 中
  • 服务器只需验证签名和有效期,不查任何存储
  • 核心特征:身份信息存在客户端,服务端不保存状态

4.3 Session ID 与 Token 的本质类比

两者在认证流程中的角色完全对等,区别在于凭证本身承载的信息量不同:

Session IDToken (JWT)
内容一串无意义的随机字符串,如 abc123自包含的信息载体,解码后可读出用户ID、角色、过期时间等
信息存储服务端(内存/Redis/数据库)客户端(Token 本身)
验证方式拿 ID 去 Session 存储里查验证签名 + 检查过期时间

比喻: Session ID 相当于酒店房卡,刷卡后前台要去系统里查你是谁、住哪间房;Token 相当于身份证,拿到手一看就知道你是谁,只需要验证它是不是真的就行。


五、关键差异对比

维度Cookie-Session 方案Token 方案
状态有状态,服务端需维护 Session 存储无状态,服务端不保存任何信息
扩展性用户量大时 Session 存储成为瓶颈,多服务器需共享 Session天然支持水平扩展,任何节点都能验证
跨域Cookie 受同源策略限制,a.com 的 Cookie 不会发给 b.comToken 放在 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 身份证),这个话题就算彻底搞清楚了。


如果这篇文章对你有帮助,欢迎点赞收藏,你的支持是我持续输出的动力!