478 lines
19 KiB
Markdown
478 lines
19 KiB
Markdown
---
|
||
tags: [Cookie, HTTP, session, web-security, authentication, backend, frontend]
|
||
create time: 2026-05-22 10:00
|
||
---
|
||
|
||
# Cookie — HTTP 状态管理机制
|
||
|
||
## 概述
|
||
|
||
Cookie 是浏览器提供的一种**客户端状态存储机制**,最初由 Netscape 在 1994 年提出,现由 RFC 6265 标准化。它的核心使命很简单:让无状态的 HTTP 协议能够"记住"用户——浏览器在每次请求时自动附带 Cookie,服务端据此识别身份、维持会话。
|
||
|
||
> [!tip] Cookie ≠ Session ≠ JWT
|
||
> 这三个概念经常被混为一谈,但它们解决的是不同层面的问题:
|
||
> - **Cookie**:存储与传输机制(浏览器行为)
|
||
> - **Session**:服务端会话管理策略
|
||
> - **JWT**:令牌数据格式(一种数据结构)
|
||
>
|
||
> 三者可以任意组合,也可以独立使用。
|
||
|
||
> [!question] 思考
|
||
> 如果 HTTP 天生无状态,那"登录后跳转页面不用重新登录"这个体验是怎么实现的?
|
||
> —— 答案就是 Cookie。登录成功后服务端通过 `Set-Cookie` 响应头在浏览器种下一个标识,后续每次请求浏览器自动带上这个标识,服务端一看就知道"哦,是你"。
|
||
|
||
## Cookie 的本质
|
||
|
||
### 结构
|
||
|
||
一个 Cookie 本质上就是一个 **键值对**,附带一组控制其行为的属性:
|
||
|
||
```
|
||
name=value; Domain=example.com; Path=/; Expires=Thu, 01 Jan 2027 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax
|
||
```
|
||
|
||
| 属性 | 说明 | 默认值 |
|
||
|------|------|--------|
|
||
| `name=value` | 键值对,Cookie 的核心数据 | — |
|
||
| `Domain` | Cookie 所属域名,子域自动继承 | 当前域 |
|
||
| `Path` | Cookie 生效的 URL 路径前缀 | `/` |
|
||
| `Expires` / `Max-Age` | 过期时间或存活时长 | 会话结束即失效 |
|
||
| `HttpOnly` | 禁止 JavaScript 访问 | `false` |
|
||
| `Secure` | 仅在 HTTPS 下传输 | `false` |
|
||
| `SameSite` | 跨站请求限制策略 | 浏览器各异(见子文档) |
|
||
|
||
> [!warning] 大小限制
|
||
> 单个 Cookie 的大小上限约 **4KB**(含所有属性),每个域名下通常限制 **~50 个** Cookie。如果需要存储大量数据,应该用服务端 Session 或数据库,而不是往 Cookie 里塞。
|
||
|
||
### 会话 Cookie vs 持久 Cookie
|
||
|
||
| 类型 | 持久性 | 典型场景 |
|
||
|------|--------|----------|
|
||
| **会话 Cookie** | 浏览器关闭即删除(无 `Expires`/`Max-Age`) | 临时状态、一次性提示 |
|
||
| **持久 Cookie** | 存活至过期时间 | "记住我"、用户偏好 |
|
||
|
||
### Cookie 前缀:`__Host-` 与 `__Secure-`
|
||
|
||
Cookie 名称可以带前缀来**强制约束安全属性**。浏览器看到这些前缀时,会校验对应属性是否满足要求,不满足则直接拒绝写入——相当于"编译期"安全检查。
|
||
|
||
| 前缀 | 强制要求 | 效果 |
|
||
|------|----------|------|
|
||
| `__Secure-` | 必须设 `Secure`(HTTPS) | 确保该 Cookie 永远不会通过 HTTP 明文传输 |
|
||
| `__Host-` | 必须设 `Secure` + `Path=/` + **不能设 `Domain`** | 除了 HTTPS 限制外,还锁定到当前主机,防止子域覆盖攻击 |
|
||
|
||
```
|
||
# 合法
|
||
Set-Cookie: __Host-session_id=abc123; Secure; Path=/
|
||
|
||
# 非法(设了 Domain,浏览器拒绝)
|
||
Set-Cookie: __Host-session_id=abc123; Secure; Path=/; Domain=example.com
|
||
|
||
# 非法(没设 Secure,浏览器拒绝)
|
||
Set-Cookie: __Secure-token=xyz; Path=/
|
||
```
|
||
|
||
> [!question] 为什么 `__Host-` 禁止设 `Domain`?
|
||
> 设了 `Domain=.example.com` 后,子域 `evil.example.com` 可以覆盖掉 `example.com` 上的同名 Cookie——这是**子域 Cookie 注入**攻击。`__Host-` 前缀通过禁止 `Domain` 来彻底杜绝此类问题。
|
||
|
||
> [!tip] 前缀是防御纵深的一部分
|
||
> 即使你的代码没写 `Secure`,只要用了 `__Host-` 前缀,浏览器会帮你挡住。在团队协作中,这是一种"防呆"机制——不是每个人都记得加安全属性,但前缀可以强制保证。
|
||
|
||
## HTTP 流程
|
||
|
||
理解 Cookie 的关键是搞清楚它在 HTTP 层面是怎么流转的:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant B as Browser
|
||
participant S as Server
|
||
|
||
Note over B: 首次访问(无 Cookie)
|
||
B->>S: GET /login
|
||
S-->>B: 200 OK + Set-Cookie: session_id=abc123; HttpOnly; Secure
|
||
|
||
Note over B: 浏览器自动存储 Cookie
|
||
B->>B: 存储 session_id=abc123
|
||
|
||
Note over B: 后续请求自动携带
|
||
B->>S: GET /dashboard (Cookie: session_id=abc123)
|
||
S->>S: 读取 Cookie,识别用户
|
||
S-->>B: 200 OK(个性化内容)
|
||
|
||
Note over B: 登出,服务端清除 Cookie
|
||
B->>S: POST /logout
|
||
S-->>B: 200 OK + Set-Cookie: session_id=; Max-Age=0
|
||
B->>B: 浏览器删除 Cookie
|
||
```
|
||
|
||
> [!note] 关键机制
|
||
> Cookie 的核心设计在于**浏览器自动管理**:收到 `Set-Cookie` 后自动存储,后续请求同域名时自动附加到 `Cookie` 头——开发者不需要手动处理传输。这是它和 `Authorization` 头(JWT 常用方式)的根本区别。
|
||
|
||
### 跨域 Cookie 与 CORS
|
||
|
||
在前后端分离的架构中,前端 `api.example.com` 请求后端 `backend.example.com` 是**跨域请求**。浏览器的同源策略默认不允许跨域发送 Cookie,需要 CORS 配合:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as "前端 api.example.com"
|
||
participant B as "后端 backend.example.com"
|
||
|
||
Note over F: "fetch credentials include"
|
||
F->>B: "OPTIONS /api/data"
|
||
Note over B: "Access-Control-Allow-Origin: https://api.example.com"
|
||
Note over B: "Access-Control-Allow-Credentials: true"
|
||
B-->>F: "204 No Content"
|
||
|
||
F->>B: "GET /api/data Cookie: session_id=abc123"
|
||
B-->>F: "200 OK"
|
||
```
|
||
|
||
服务端关键响应头:
|
||
|
||
```
|
||
Access-Control-Allow-Origin: https://api.example.com # 不能用 *,必须指定域名
|
||
Access-Control-Allow-Credentials: true # 允许携带凭证
|
||
```
|
||
|
||
前端对应的请求配置:
|
||
|
||
```typescript
|
||
// fetch API
|
||
fetch("https://backend.example.com/api/data", {
|
||
credentials: "include", // 关键:跨域请求也带 Cookie
|
||
});
|
||
|
||
// axios 全局配置
|
||
axios.defaults.withCredentials = true;
|
||
```
|
||
|
||
> [!warning] `Allow-Origin: *` 和 `Allow-Credentials: true` 不能同时出现
|
||
> 当 `credentials: "include"` 时,`Access-Control-Allow-Origin` 不能设为通配符 `*`——这是 CORS 规范的硬性约束。服务端必须回显请求头中的 `Origin` 值(注意:只允许白名单中的域)。
|
||
|
||
> [!question] 为什么 `localhost:3000` 请求 `localhost:8080` 也报 CORS 错误?
|
||
> 浏览器的同源策略看的是 **协议 + 域名 + 端口** 三元组。即使域名相同,端口不同(3000 vs 8080)也算跨域。开发环境的 proxy(如 Vite 的 `server.proxy`)之所以能解决这个问题,是因为它把请求代理到了同一个源,请求根本没离开浏览器的同源范围。
|
||
|
||
## 服务端操作(Go)
|
||
|
||
### 标准库:读写 Cookie
|
||
|
||
```go
|
||
import "net/http"
|
||
|
||
// 写入 Cookie
|
||
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||
http.SetCookie(w, &http.Cookie{
|
||
Name: "session_id",
|
||
Value: "abc123",
|
||
Path: "/",
|
||
Domain: "example.com",
|
||
MaxAge: 3600 * 24, // 24 小时
|
||
HttpOnly: true, // 防 XSS
|
||
Secure: true, // 仅 HTTPS
|
||
SameSite: http.SameSiteLaxMode,
|
||
})
|
||
}
|
||
|
||
// 读取 Cookie
|
||
func getCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||
cookie, err := r.Cookie("session_id")
|
||
if err != nil {
|
||
http.Error(w, "session not found", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
// cookie.Value 即为 "abc123"
|
||
}
|
||
|
||
// 删除 Cookie(将 MaxAge 设为负数)
|
||
func deleteCookieHandler(w http.ResponseWriter, r *http.Request) {
|
||
http.SetCookie(w, &http.Cookie{
|
||
Name: "session_id",
|
||
Value: "",
|
||
MaxAge: -1, // 立即删除
|
||
})
|
||
}
|
||
```
|
||
|
||
> [!tip] 读取失败的常见原因
|
||
> `r.Cookie()` 返回 `http.ErrNoCookie` 时,说明该 Cookie 不存在。但更隐蔽的问题是:Cookie 设了 `Domain=".example.com"` 但你在 `localhost` 测试——跨域的 Cookie 不会被浏览器存储。开发环境记得不要设 `Domain`。
|
||
|
||
### Gin 框架
|
||
|
||
Gin 对 Cookie 做了简单封装:
|
||
|
||
```go
|
||
// 写入
|
||
c.SetCookie("session_id", "abc123", 86400, "/", "example.com", true, true)
|
||
|
||
// 读取
|
||
val, err := c.Cookie("session_id")
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
||
return
|
||
}
|
||
|
||
// 删除
|
||
c.SetCookie("session_id", "", -1, "/", "", false, false)
|
||
```
|
||
|
||
> [!question] 标准库 vs Gin,什么时候用哪个?
|
||
> 如果你用的是 Gin 框架,当然直接用 `c.SetCookie()` / `c.Cookie()`,没必要引入标准库的 `http.SetCookie`。但如果你用的是其他框架(Echo、Fiber、Chi),它们对 Cookie 的封装方式各不相同,而底层都依赖 `net/http` 的 `http.Cookie` 结构体——掌握标准库的写法,换个框架也能无缝上手。
|
||
|
||
### Cookie 存 Session ID:完整流程
|
||
|
||
这是 Cookie 最经典的用法——配合服务端 Session:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["用户登录 POST login"] --> B{"凭据正确"}
|
||
B -->|"是"| C["生成 session_id = random UUID"]
|
||
C --> D["存储到 Redis, session_id 映射用户信息"]
|
||
D --> E["Set-Cookie: session_id=xxx; HttpOnly"]
|
||
E --> F["返回登录成功"]
|
||
|
||
B -->|"否"| G["返回 401"]
|
||
|
||
H["后续请求 GET api/user"] --> I["浏览器自动带 Cookie"]
|
||
I --> J["读取 session_id"]
|
||
J --> K{"Redis 中存在"}
|
||
K -->|"是"| L["取出用户信息, 放行"]
|
||
K -->|"否"| M["返回 401"]
|
||
```
|
||
|
||
Go + Redis 实现:
|
||
|
||
```go
|
||
import "github.com/google/uuid"
|
||
|
||
func LoginHandler(c *gin.Context) {
|
||
// ... 校验用户名密码 ...
|
||
|
||
// 生成 Session ID
|
||
sessionID := uuid.New().String()
|
||
|
||
// 存入 Redis,设置过期时间
|
||
redis.Set(ctx, "session:"+sessionID, userID, 24*time.Hour)
|
||
|
||
// 种到 Cookie
|
||
c.SetCookie("session_id", sessionID, 86400, "/", "", true, true)
|
||
c.JSON(200, gin.H{"message": "login success"})
|
||
}
|
||
|
||
func AuthMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
sessionID, err := c.Cookie("session_id")
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "no session"})
|
||
return
|
||
}
|
||
|
||
userID, err := redis.Get(ctx, "session:"+sessionID).Result()
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "session expired"})
|
||
return
|
||
}
|
||
|
||
c.Set("userID", userID)
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
## 客户端操作(React / TypeScript)
|
||
|
||
> [!warning] 不要用 JS 操作 HttpOnly Cookie
|
||
> `HttpOnly` 标记的 Cookie 无法被 `document.cookie` 读取——这正是它的安全价值。前端代码中能读写的只有**非 HttpOnly** 的 Cookie(如语言偏好、主题设置等)。涉及认证的 Cookie 应始终设为 `HttpOnly`,由后端控制。
|
||
|
||
```typescript
|
||
// 读取非 HttpOnly 的 Cookie(如用户偏好)
|
||
function getCookie(name: string): string | null {
|
||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||
return match ? decodeURIComponent(match[2]) : null;
|
||
}
|
||
|
||
// 写入 Cookie
|
||
function setCookie(name: string, value: string, days: number) {
|
||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
|
||
}
|
||
|
||
// 删除 Cookie
|
||
function deleteCookie(name: string) {
|
||
document.cookie = `${name}=; Max-Age=0; path=/`;
|
||
}
|
||
```
|
||
|
||
> [!note] 前端认证的典型模式
|
||
> 如果使用 Cookie 做认证(而非 `Authorization` 头 + JWT),前端**不需要手动操作认证 Cookie**。只要后端登录接口种好了 `HttpOnly` Cookie,后续所有 `fetch` / `axios` 请求会自动携带:
|
||
>
|
||
> ```typescript
|
||
> // withCredentials 让跨域请求也带上 Cookie
|
||
> axios.defaults.withCredentials = true;
|
||
> ```
|
||
>
|
||
> 前端唯一要做的就是确保请求配置正确,认证流程完全由浏览器 + 后端完成。
|
||
|
||
> [!question] 为什么有时候浏览器"明明有 Cookie"却不发送?
|
||
> 常见原因排查清单:
|
||
> 1. **跨域请求没设 `credentials: "include"`**——同域自动带,跨域需要显式声明
|
||
> 2. **`Domain` 设错了**——比如后端在 `.example.com` 种了 Cookie,但前端跑在 `localhost`
|
||
> 3. **`SameSite=Strict`** 且请求来自其他站点——`Lax` 允许顶级导航,`Strict` 连顶级导航也不发
|
||
> 4. **`Secure` 但用了 HTTP**——本地开发最容易踩的坑
|
||
>
|
||
> 这些问题在开发环境中很常见,尤其当后端用 `.localhost` 或 IP 地址时。遇到"Cookie 消失"时,**打开 DevTools → Application → Cookies** 逐项检查属性,比看代码有效 10 倍。
|
||
|
||
## 安全机制
|
||
|
||
Cookie 的安全问题本质上围绕两个攻击面:**XSS(跨站脚本)** 和 **CSRF(跨站请求伪造)**。
|
||
|
||
### 防御 XSS:HttpOnly + Secure
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph XSS_Attack["XSS 攻击流程"]
|
||
A["恶意脚本注入页面"] --> B["JS 读取认证 Cookie"]
|
||
B --> C["发送到攻击者服务器"]
|
||
end
|
||
|
||
subgraph Defense["防御"]
|
||
D["HttpOnly, JS 无法读取 Cookie"]
|
||
E["Secure, 仅 HTTPS 传输"]
|
||
F["输入转义, 从源头阻止注入"]
|
||
end
|
||
```
|
||
|
||
| 属性 | 防御效果 | 设置建议 |
|
||
|------|----------|----------|
|
||
| `HttpOnly` | 阻止 JS 读取,XSS 无法窃取 Cookie | 认证类 Cookie **必须** 设置 |
|
||
| `Secure` | 仅 HTTPS 传输,防止中间人截获 | 生产环境 **必须** 设置 |
|
||
| CSP 头 | 限制页面可执行的脚本来源 | 配合使用,多一层防护 |
|
||
|
||
### 防御 CSRF
|
||
|
||
CSRF 的本质:浏览器会自动带上目标域的 Cookie,攻击者借此伪造用户请求。
|
||
|
||
详细的 SameSite 属性解析和 CSRF 攻防策略,见 → [[SameSite 与 CSRF 防护]]
|
||
|
||
> [!question] 为什么 JWT + `Authorization` 天然防 CSRF?
|
||
> 因为 `Authorization` 头不是浏览器自动发送的——它需要 JS 代码显式设置。而 CSRF 攻击依赖的是浏览器自动携带 Cookie 这一行为。没有自动携带,就没有 CSRF。但代价是:JWT 需要前端手动管理令牌的存储和注入。
|
||
|
||
### Partitioned Cookie(CHIPS)
|
||
|
||
自 2024 年起,Chrome 逐步淘汰**第三方 Cookie**(即跨站 Cookie),这一趋势已在 2025-2026 年全面落地。它直接影响嵌入式场景:支付回调、第三方登录、`<iframe>` 嵌入等。
|
||
|
||
**问题本质**:在 `site-a.com` 的页面里嵌入 `analytics.com` 的资源,浏览器会带上 `analytics.com` 的 Cookie——这种跨站携带行为既支持了合法场景,也被用于跨站追踪。
|
||
|
||
**解决方案**:`Partitioned` 属性,让 Cookie 按**顶级站点**隔离存储:
|
||
|
||
```
|
||
# 传统方式:analytics.com 的 Cookie 在所有嵌入它的站点间共享
|
||
Set-Cookie: track_id=abc123; SameSite=None; Secure
|
||
|
||
# Partitioned:Cookie 按 site-a.com 和 site-b.com 分别隔离
|
||
Set-Cookie: track_id=abc123; SameSite=None; Secure; Partitioned
|
||
```
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph Traditional["传统第三方 Cookie"]
|
||
A["site-a.com 嵌入 analytics.com"] --> C["共享同一份 Cookie"]
|
||
B["site-b.com 嵌入 analytics.com"] --> C
|
||
end
|
||
|
||
subgraph Partitioned["Partitioned Cookie (CHIPS)"]
|
||
D["site-a.com 嵌入 analytics.com"] --> E["独立 Cookie 分区 site-a.com"]
|
||
F["site-b.com 嵌入 analytics.com"] --> G["独立 Cookie 分区 site-b.com"]
|
||
end
|
||
```
|
||
|
||
> [!tip] 什么时候需要关注 CHIPS?
|
||
> 如果你的系统有**跨站 Cookie 需求**(如 SSO 单点登录、嵌入式支付、跨站 `<iframe>`),且目标用户使用 Chrome 115+,就必须加上 `Partitioned`。同站应用不受影响。这是向后兼容的——不支持的浏览器会忽略这个属性。
|
||
|
||
## 对比与选型
|
||
|
||
### Cookie vs localStorage vs sessionStorage
|
||
|
||
| 维度 | Cookie | localStorage | sessionStorage |
|
||
|------|--------|-------------|----------------|
|
||
| 容量 | ~4KB | ~5MB | ~5MB |
|
||
| 自动发送 | ✅ 每次请求自动携带 | ❌ | ❌ |
|
||
| 服务端可读 | ✅ | ❌ | ❌ |
|
||
| JS 访问 | 受限(HttpOnly 不可读) | ✅ | ✅ |
|
||
| 过期策略 | 可设过期时间 | 永久(手动删除) | 标签页关闭即失效 |
|
||
| 安全性 | HttpOnly + Secure 较高 | 易被 XSS 读取 | 易被 XSS 读取 |
|
||
|
||
> [!info] 选型决策树
|
||
>
|
||
> - **认证令牌** → `HttpOnly` + `Secure` Cookie(最安全)或 `Authorization` 头 + 内存存储(次安全)
|
||
> - **用户偏好设置**(语言、主题)→ 普通 Cookie 或 `localStorage`
|
||
> - **临时表单数据** → `sessionStorage`
|
||
> - **大量缓存数据** → `localStorage`(注意容量上限)
|
||
|
||
### Session vs JWT:认证架构选型
|
||
|
||
| 维度 | Session + Cookie | JWT(无状态令牌) |
|
||
|------|------------------|-------------------|
|
||
| **状态存储** | 服务端(Redis / DB) | 客户端(令牌自包含) |
|
||
| **水平扩展** | 需要共享 Session 存储(Redis 集群) | 天然支持——任何节点都能独立验签 |
|
||
| **吊销能力** | 直接删除 Session 即失效 | 难——Token 签发后无法主动撤销(需黑名单) |
|
||
| **服务端开销** | 每次请求查 Redis | 只需 CPU 验签(极轻量) |
|
||
| **负载大小** | Cookie 仅传 ~36B UUID | JWT 通常 500B~1KB(含 payload + 签名) |
|
||
| **适用场景** | 传统 Web、对吊销要求高 | 微服务、跨域 API、移动端 |
|
||
|
||
> [!question] JWT "无状态" 是真的无状态吗?
|
||
> 严格来说,JWT 只是**不需要服务端存储会话数据**,但它仍然有状态——用户角色变更了怎么办?Token 被盗了怎么办?这些场景下你需要维护一个**黑名单**(通常存在 Redis 里),这时 JWT 又变成了"有状态"的。
|
||
>
|
||
> 所以"无状态"是一种**理想模型**。在生产环境中,纯无状态 JWT 很少见,通常还是会搭配一个轻量级的 Token 黑名单或版本号机制。不要被"无状态"的字面意思误导了。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["认证需求"] --> B{"需要随时强制下线"}
|
||
B -->|"是"| C["Session + Cookie"]
|
||
B -->|"否"| D{"微服务, 跨域多"}
|
||
D -->|"是"| E["JWT + Authorization 头"]
|
||
D -->|"否"| F{"需要极低延迟"}
|
||
F -->|"是"| E
|
||
F -->|"否"| C
|
||
|
||
C --> C1["Redis 存 Session"]
|
||
E --> E1["签名验签"]
|
||
E --> E2["可选: Redis 黑名单"]
|
||
|
||
style C fill:#e8f4f8
|
||
style E fill:#fde8e8
|
||
```
|
||
|
||
### Cookie vs JWT 传输
|
||
|
||
这不是二选一,而是**两种组合策略**:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["认证方案选择"] --> B{"你的架构"}
|
||
B -->|"前后端分离 SPA"| C["JWT + Authorization 头"]
|
||
B -->|"传统 Web, SSR"| D["Session ID + Cookie"]
|
||
|
||
C --> C1["前端手动管理令牌"]
|
||
C --> C2["天然防 CSRF"]
|
||
C --> C3["需防 XSS 窃取 localStorage"]
|
||
|
||
D --> D1["浏览器自动管理"]
|
||
D --> D2["需防 CSRF 攻击"]
|
||
D --> D3["HttpOnly 防 XSS"]
|
||
|
||
style C fill:#e8f4f8
|
||
style D fill:#fde8e8
|
||
```
|
||
|
||
> [!tip] 最佳实践:两者结合
|
||
> 现代生产环境最常用的策略是**混合模式**:
|
||
> - **Access Token(JWT)**:短生命周期(15 分钟),存 `Authorization` 头,用于 API 调用
|
||
> - **Refresh Token**:长生命周期(7 天),存 `HttpOnly` + `Secure` + `SameSite=Strict` Cookie,仅用于刷新 Access Token
|
||
>
|
||
> 这样既享受了 JWT 的无状态优势,又利用了 Cookie 的安全特性。
|
||
|
||
## 关联笔记
|
||
|
||
- [[hhs/DEV/鉴权策略/JWT/JWT]] — JWT 令牌格式与认证流程
|
||
- [[JWT vs Cookie]] — JWT 与 Cookie 认证方案全景对比
|
||
- [[SameSite 与 CSRF 防护]] — SameSite 属性详解与 CSRF 攻防
|