Files
cs-note/hhs/DEV/鉴权策略/Cookie/SameSite 与 CSRF 防护.md
T
2026-05-24 11:42:38 +08:00

8.7 KiB
Raw Blame History

tags, create time
tags create time
Cookie
CSRF
SameSite
web-security
CORS
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 或老旧浏览器的场景。

常见坑与调试技巧

[!warning] 遇到 Cookie "消失"了?按顺序检查

  1. Domain 匹配:Cookie 的 Domain 必须是当前域或其父域。localhost 下设 Domain=example.com 会被忽略
  2. Path 匹配:请求路径必须以 Cookie 的 Path 为前缀
  3. Secure + HTTPS:Secure Cookie 在 HTTP 下不会被发送。本地开发用 http://localhost 时不要设 Secure
  4. SameSite + 跨域:跨站请求在 Lax/Strict 下不带 Cookie,这是预期行为
  5. 过期时间:检查 Expires/Max-Age 是否已过期
  6. 超过大小限制:单个 Cookie 超过 4KB 会被静默丢弃

前后端分离开发中,前端 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 不能设为通配符 *,必须指定具体的源。这是浏览器安全策略的硬性要求。

关联笔记