8.7 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-05-22 10:00 |
SameSite 与 CSRF 防护
概述
SameSite 是 Cookie 的一个安全属性,用来控制浏览器在跨站请求中是否发送 Cookie。它是防御 CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击最直接的手段——不需要额外的 Token,不需要检查 Origin 头,只需要一个 Cookie 属性就能从浏览器层面阻断攻击。
[!question] 什么是 CSRF? 想象你登录了银行网站
bank.com,浏览器里存着银行的认证 Cookie。此时你打开了一个恶意网站evil.com,该页面偷偷向bank.com/api/transfer发了一个 POST 请求——由于浏览器会自动带上bank.com的 Cookie,银行服务器以为是你本人操作,转账就成功了。你什么都没点,钱就没了。 这就是 CSRF。
SameSite 三个值
一图总览
flowchart LR
subgraph Origin["当前页面 origin.com"]
A["发起请求"]
end
A --> B{"SameSite 设置?"}
B -->|"Strict"| C["跨站请求不发送 Cookie"]
B -->|"Lax"| D["顶级导航 GET 发送,其他不发送"]
B -->|"None"| E["所有请求都发送(必须 Secure)"]
style C fill:#c8e6c9
style D fill:#fff9c4
style E fill:#ffcdd2
Strict — 最严格
Set-Cookie: session_id=abc; SameSite=Strict; Secure; HttpOnly
行为:只有从本站发起的请求才会携带 Cookie。即使是用户点击外部链接跳转进来,第一次请求也不带 Cookie。
优点:CSRF 彻底无效。 缺点:用户体验差——从搜索引擎或书签点击链接进入网站时,用户会发现"明明之前登录了,怎么要重新登录?"
[!note] 适用场景 高敏感操作的保护,比如银行后台管理、密码修改页面。可以只在这些路由的 Cookie 上设
Strict。
Lax — 平衡之选(浏览器默认)
Set-Cookie: session_id=abc; SameSite=Lax; Secure; HttpOnly
行为:
| 请求方式 | 同站请求 | 跨站请求 |
|---|---|---|
<a> 链接跳转(GET) |
✅ 发送 | ✅ 发送 |
<form> 提交(POST) |
✅ 发送 | ❌ 不发送 |
fetch / XMLHttpRequest |
✅ 发送 | ❌ 不发送 |
<img> / <iframe> 嵌入 |
✅ 发送 | ❌ 不发送 |
核心逻辑:只允许"顶级导航 + GET"的跨站请求携带 Cookie。这意味着用户点击链接跳转到你的网站是 OK 的,但恶意网站通过表单提交或 AJAX 发起的 POST/PUT/DELETE 请求不会带 Cookie。
[!tip] 为什么 Lax 是默认值?
Strict太严影响正常体验,None完全不防护。Lax在安全和可用性之间取得了最佳平衡:用户从外部链接正常跳转不受影响,而 CSRF 攻击中常见的跨站 POST/PUT 请求被阻断。Chrome 从 2020 年起将Lax作为未显式设置SameSite时的默认值。
None — 完全开放(必须配合 Secure)
Set-Cookie: session_id=abc; SameSite=None; Secure
行为:所有请求(无论同站跨站)都发送 Cookie。
注意:使用 None 必须同时设置 Secure,否则浏览器会拒绝该 Cookie。这是 Chrome 80+ 的强制要求。
[!warning] 什么时候需要 None? 只有在你确实需要跨站发送 Cookie 的场景下才用
None,典型情况:
- 第三方嵌入的支付组件(Stripe、PayPal)
- 跨域单点登录(SSO)
- 嵌入式 iframe 中需要认证的组件
使用
None时必须配合其他 CSRF 防护手段(CSRF Token、Origin 检查等)。
CSRF 攻防详解
攻击方式
sequenceDiagram
participant U as 用户
participant B as 浏览器
participant Bank as bank.com
participant Evil as evil.com
U->>Bank: 1. 登录成功,浏览器存储认证 Cookie
U->>Evil: 2. 访问恶意网站
Evil->>Evil: 3. 构造恶意请求(隐藏表单 / JS fetch)
Evil->>B: 4. 触发向 bank.com 的请求
B->>Bank: 5. 自动携带 bank.com 的 Cookie
Bank-->>B: 6. 以为是合法请求,执行操作
Note over U: 用户毫不知情,资产已被转移
防御方案
方案一:SameSite Cookie(首选)
Set-Cookie: session_id=abc; SameSite=Lax; Secure; HttpOnly
简单直接,无需服务端额外逻辑。Lax 已经阻断了绝大多数 CSRF 攻击向量。
方案二:CSRF Token(传统方案)
服务端生成一个随机 Token,嵌入到表单中,提交时验证:
// 生成 CSRF Token
func GenerateCSRFToken() string {
token := make([]byte, 32)
rand.Read(token)
return base64.URLEncoding.EncodeToString(token)
}
// 中间件:验证 CSRF Token
func CSRFProtection() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" {
c.Next() // GET 请求不校验
return
}
// 方式一:从请求头获取(前端 JS 注入)
token := c.GetHeader("X-CSRF-Token")
// 方式二:从表单字段获取
if token == "" {
token = c.PostForm("_csrf")
}
sessionToken, _ := c.Cookie("csrf_token")
if token != sessionToken {
c.AbortWithStatusJSON(403, gin.H{"error": "CSRF token mismatch"})
return
}
c.Next()
}
}
// React:在请求头中注入 CSRF Token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
axios.interceptors.request.use((config) => {
if (config.method !== 'get') {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
[!note] CSRF Token 的原理 攻击者可以构造请求,但无法读取目标网站的页面内容(受同源策略保护)。所以攻击者拿不到嵌入在页面中的 CSRF Token,也就无法通过验证。
方案三:检查 Origin / Referer 头
func OriginCheck() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin == "" {
origin = c.GetHeader("Referer")
}
// 验证来源是否为可信域
if !strings.HasSuffix(origin, "example.com") {
c.AbortWithStatusJSON(403, gin.H{"error": "invalid origin"})
return
}
c.Next()
}
}
[!warning] Origin 头可以被伪造吗? 浏览器安全模型规定:通过代码(fetch/XHR)发起的跨域请求,
Origin头由浏览器自动设置,JS 无法修改。但某些老旧浏览器或非浏览器客户端(如 curl)可能不带Origin头,所以不能作为唯一防线。
防御方案对比
| 方案 | 防护强度 | 实现复杂度 | 兼容性 | 推荐度 |
|---|---|---|---|---|
SameSite=Lax |
⭐⭐⭐ | 零成本 | 现代浏览器均支持 | ⭐⭐⭐⭐⭐ |
| CSRF Token | ⭐⭐⭐⭐⭐ | 中等 | 全部浏览器 | ⭐⭐⭐⭐ |
| Origin 检查 | ⭐⭐⭐ | 低 | 全部(需处理空值) | ⭐⭐⭐ |
| 双重 Cookie 验证 | ⭐⭐⭐⭐ | 低 | 全部浏览器 | ⭐⭐⭐ |
[!info] 最佳实践
SameSite=Lax+ CSRF Token 双保险。SameSite从浏览器层面阻断大部分攻击,CSRF Token 兜底处理SameSite=None或老旧浏览器的场景。
常见坑与调试技巧
1. Cookie 不生效排查清单
[!warning] 遇到 Cookie "消失"了?按顺序检查
- Domain 匹配:Cookie 的
Domain必须是当前域或其父域。localhost下设Domain=example.com会被忽略- Path 匹配:请求路径必须以 Cookie 的
Path为前缀- Secure + HTTPS:
SecureCookie 在 HTTP 下不会被发送。本地开发用http://localhost时不要设Secure- SameSite + 跨域:跨站请求在
Lax/Strict下不带 Cookie,这是预期行为- 过期时间:检查
Expires/Max-Age是否已过期- 超过大小限制:单个 Cookie 超过 4KB 会被静默丢弃
2. 跨域 Cookie 的正确配置
前后端分离开发中,前端 localhost:3000 调后端 localhost:8080,需要:
// 后端 CORS 配置
cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowCredentials: true, // 允许携带 Cookie
// 不能同时用 AllowOrigins: ["*"] + AllowCredentials: true
}
// 前端配置
axios.defaults.withCredentials = true; // 允许跨域携带 Cookie
[!warning]
Access-Control-Allow-Origin不能用*当AllowCredentials: true时,Access-Control-Allow-Origin不能设为通配符*,必须指定具体的源。这是浏览器安全策略的硬性要求。
关联笔记
- Cookie — Cookie 核心概念与使用方式
- hhs/DEV/鉴权策略/JWT/JWT — JWT 令牌格式与认证流程