跨域、CORS、CSRF、SameSite —— 前端开发中最容易混淆的一套概念,彻底讲清楚
前言
“后端开发出身的人,第一次碰前端跨域问题,十有八九会被搞晕。”
我就是这样。
我在后端写接口写得好好的,前端一连就说”跨域报错”。我心想:“我接口都写对了,你浏览器凭什么拦我?”
然后浏览器说:“这是为你好,防止 CSRF 攻击。”
我又心想:“CSRF 又是什么东西?跨域跟 CSRF 什么关系?”
然后我去搜解决方案,有人跟我说加 CORS 头,有人跟我说用 SameSite cookie,有人跟我说 CSRF token……我彻底懵了。
这篇文章就是为了当时的我写的。从最底层开始,一层一层讲清楚:
- 为什么浏览器要限制跨域?
- CORS 是在解决什么问题?
- CSRF 又是什么?跟跨域什么关系?
- SameSite Cookie 又是干什么的?
- 代码里 trustedOrigins 到底该配置什么?
最终你会发现:这些概念并不复杂,只是没人把它们串起来讲。
目录
- 三个核心问题
- 先搞清楚:什么是”跨域”?
- 为什么浏览器要限制跨域?
- CORS —— 跨域资源共享
- CSRF —— 跨站请求伪造
- SameSite Cookie —— 给 Cookie 上的锁
- 把这一切串起来:一次完整的请求过程
- 回到代码审查的那个问题
- 最佳实践总结
- 全篇总结
1. 三个核心问题
在深入细节之前,先把我们要解决的核心问题列出来。
你在做 Web 开发时,最终要回答这三个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 问题 1:跨域访问 ┌─────────────────────────────────────────┐ │ 我的前端在 localhost:3000 │ │ 我的后端在 localhost:8000 │ │ 端口不同 → 跨域 → 浏览器拦了 │ │ 怎么让浏览器放行? │ │ │ │ 答案:CORS │ └─────────────────────────────────────────┘
问题 2:CSRF 攻击 ┌─────────────────────────────────────────┐ │ 用户登录了银行网站(bank.com) │ │ 又打开了恶意网站(evil.com) │ │ evil.com 偷偷向 bank.com 发请求 │ │ 浏览器自动带上 Cookie │ │ 银行以为是用户本人的操作 │ │ │ │ 怎么防止? │ │ 答案:CSRF Token / SameSite Cookie │ └─────────────────────────────────────────┘
问题 3:两者搞混 ┌─────────────────────────────────────────┐ │ 跨域问题的解决方法是 CORS │ │ CSRF 问题的解决方法是 CSRF Token │ │ 但 CORS 也能防 CSRF 吗? │ │ 为什么有了 CORS 还需要 CSRF Token? │ │ │ │ 答案是:CORS 防不住 CSRF! │ │ 这是最容易被搞混的地方 │ └─────────────────────────────────────────┘
|
整篇文章的核心在于:CORS 防跨域访问,CSRF 防护防跨站请求,它们是两件不同的事。
2. 先搞清楚:什么是”跨域”?
2.1 域(Origin)的定义
一个 URL 的”域”由三部分组成:
1 2 3
| http://localhost:3000/api/login └──┬──┘ └────┬───┘ └─┬─┘ └─────┘ 协议 主机 端口 路径
|
域 = 协议 + 主机 + 端口
2.2 什么叫”跨域”?
当两个 URL 的协议、主机、端口三者中有一个不同,就是跨域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 例子 1:端口不同 http://localhost:3000 ← 前端 http://localhost:8000 ← 后端 协议相同 ✔ 主机相同 ✔ 端口不同 ✘ → 跨域!
例子 2:主机不同 http://localhost:3000 ← 开发环境 http://47.109.28.100:50085 ← 生产环境 协议相同 ✔ 主机不同 ✘ 端口不同 ✘ → 跨域!
例子 3:协议不同 http://example.com https://example.com 协议不同 ✘ 主机相同 ✔ 端口(默认)✔ → 跨域!
例子 4:完全同域 http://localhost:3000/page1 http://localhost:3000/page2 协议 ✔ 主机 ✔ 端口 ✔ → 同域,不跨域!
|
2.3 一个重要的区分:跨域 vs 跨站
这是后续理解 CSRF 和 SameSite 的关键:
| 概念 |
区分依据 |
例子 |
| 跨域(Cross-Origin) |
协议 + 主机 + 端口 |
localhost:3000 vs localhost:8000 → 跨域 |
| 跨站(Cross-Site) |
注册域名(eTLD+1) |
a.example.com vs b.example.com → 同站 |
跨域不一定跨站,跨站一定跨域。
1 2 3
| 同站同域: http://example.com/page1 → http://example.com/page2 同站跨域: http://example.com:3000 → http://example.com:8000 跨站跨域: http://example.com → http://evil.com
|
3. 为什么浏览器要限制跨域?
3.1 同源策略(Same-Origin Policy)
浏览器有一个核心安全机制叫同源策略:
一个网页的脚本只能访问”同源”(同协议、同主机、同端口)的资源。
这条策略是浏览器安全的基石。没有它,整个 Web 就乱套了。
3.2 如果没有同源策略会怎样?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 你打开了银行网站(bank.com)并登录 → 浏览器有了 bank.com 的 Cookie
你没有关这个标签页 又打开了恶意网站(evil.com)
evil.com 的 JavaScript 可以: ❌ 读取 bank.com 的响应内容 ❌ 读取 bank.com 的 Cookie ❌ 操作 bank.com 的 DOM
→ 如果 evil.com 能直接读取你在 bank.com 的余额信息 → 然后偷偷发起转账 → 整个 Web 就没有安全性可言了
|
3.3 同源策略限制了什么?
1 2 3 4 5 6 7 8 9 10 11 12
| 同源策略主要限制:
1. DOM 访问 ❌ evil.com 不能读取 bank.com 的页面内容
2. Ajax 请求(XMLHttpRequest / Fetch) ❌ evil.com 不能读取 bank.com 的 API 响应 ✅ 但可以发送请求(只是读不到响应)
3. Cookie 读取 ❌ evil.com 不能读取 bank.com 的 Cookie ✅ 但浏览器会自动带上 Cookie(!)
|
注意第 2 点和第 3 点:请求可以发出去,Cookie 也会自动带上去——只是读不到响应。这正是后面 CSRF 攻击能得逞的原因。
3.4 同源策略的一个特例
同源策略对简单请求和非简单请求有不同的处理方式。这个”不同”就是 CORS 机制的由来(下一节讲)。
4. CORS —— 跨域资源共享
4.1 CORS 是什么
CORS = Cross-Origin Resource Sharing,跨域资源共享。
它是一套 HTTP 头部机制,让服务器告诉浏览器:”这个跨域请求是我允许的,放行吧。”
CORS 不是安全漏洞,而是一种”有条件的放行”。默认跨域是禁止的,服务器通过 CORS 头部显式声明”允许谁来访问我”。
4.2 CORS 是如何工作的
1 2 3 4 5 6 7 8 9
| 请求前,浏览器问服务器: "我要发一个跨域请求,你允许吗?"
服务器回答: "我允许来自 http://localhost:3000 的请求"
浏览器检查回答: "好,这个来源在允许列表里 → 放行" "这个来源不在允许列表里 → 拦截"
|
4.3 关键 CORS 头部
CORS 通过一系列 HTTP 响应头部来控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Access-Control-Allow-Origin: http://localhost:3000 # 允许哪个域访问(* 表示所有域,生产环境不建议)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 允许哪些 HTTP 方法
Access-Control-Allow-Headers: Content-Type, Authorization # 允许哪些自定义头
Access-Control-Allow-Credentials: true # 是否允许携带 Cookie(跨域请求要带 Cookie 必须设为 true)
Access-Control-Max-Age: 86400 # 预检请求的结果可以缓存多久(秒)
|
4.4 简单请求 vs 预检请求
这是 CORS 中最容易被忽略的细节。
简单请求(Simple Request)
满足以下全部条件:
1 2 3 4 5
| HTTP 方法:GET、HEAD、POST 之一 请求头:只能包含浏览器默认允许的简单头 Accept、Accept-Language、Content-Language Content-Type 限以下三种: text/plain、multipart/form-data、application/x-www-form-urlencoded
|
简单请求的流程:
1 2 3 4 5 6 7 8 9
| 浏览器直接发送请求 │ ▼ 服务器返回响应 + CORS 头部 │ ▼ 浏览器检查 CORS 头部 - 允许?→ 把响应交给前端代码 - 不允许?→ 拦截响应,前端收不到
|
预检请求(Preflight Request)
当请求不满足”简单请求”条件时:
1 2 3 4 5 6 7 8
| fetch('http://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer xxx' } })
|
预检请求的流程:
1 2 3 4 5 6 7 8 9 10 11 12
| 先发一个 OPTIONS 请求(预检) │ ▼ 服务器返回 CORS 头部(声明允许什么) │ ▼ 浏览器检查预检结果 - 允许?→ 发送真正的请求 - 不允许?→ 拦截,不发送真正请求 │ ▼ 真正请求发送 → 正常返回 → 浏览器放行
|
很多后端新手遇到跨域问题,就是 OPTIONS 预检请求这一步没处理好——后端没有正确处理 OPTIONS 请求,返回了 404 或没有 CORS 头部。
4.5 CORS 解决了什么问题?
1 2 3 4 5 6 7 8 9 10
| ✅ CORS 解决的问题: 前端 localhost:3000 调用后端 localhost:8000 的 API → 后端配置 CORS 头 → 浏览器放行
✅ CORS 是"有条件的放行" 服务器主动声明"我允许谁来访问我" 属于"白名单"机制
❌ CORS 不能解决的问题: 无法防止 CSRF 攻击(下面的章节详细解释)
|
5. CSRF —— 跨站请求伪造
5.1 CSRF 是什么
CSRF = Cross-Site Request Forgery,跨站请求伪造。读作 “sea-surf”。
CSRF 攻击就是:攻击者利用你已经登录的身份,在你不知情的情况下,以你的名义发送恶意请求。
5.2 CSRF 攻击的原理
这是 CSRF 能成功的最关键原因——浏览器自动携带 Cookie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 前提: ┌─────────────────────────────────────────┐ │ 你登录了银行网站(bank.com) │ │ 浏览器保存了 bank.com 的 Cookie │ │ 你没有登出(Cookie 仍然有效) │ └─────────────────────────────────────────┘
攻击步骤: 1. 你又打开了一个网站 evil.com 2. evil.com 页面上有个隐藏的表单或图片 <img src="https://bank.com/transfer?to=attacker&amount=10000"> <!-- 或者 --> <form action="https://bank.com/transfer" method="POST"> <input name="to" value="attacker"> <input name="amount" value="10000"> </form> 3. 浏览器发送这个请求时,自动带上 bank.com 的 Cookie 4. bank.com 看到 Cookie → 认为是你在操作 → 执行转账
|
5.3 关键:为什么 CORS 防不住 CSRF?
这是整篇文章最重要的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| CSRF 攻击 —— 用的是 "跨站" 请求 CORS 防护 —— 防的是 "跨域" 读取
关键区别: CSRF 不需要读取响应!它只需要把请求发出去就行了! evil.com 向 bank.com 发请求: 1. ✅ 请求可以发送(同源策略不阻止发送请求) 2. ✅ Cookie 自动被带上(浏览器自动行为) 3. ❌ evil.com 读不到 bank.com 的响应(被同源策略拦了) 4. ✅ 但 bank.com 已经执行了转账操作(服务器收到了请求!)
CORS 拦的是第 3 步(读不到响应),但第 4 步已经发生了!
所以: 即使完全不配置 CORS,CSRF 攻击照样可以成功。 CORS 跟防 CSRF 没有关系。
|
5.4 CSRF 的防御方法
方法 1:CSRF Token(最主流)
1 2 3 4 5
| 服务器生成一个随机的 Token,存在 Session 中 前端在提交表单时,把这个 Token 作为隐藏字段一起提交 服务器验证 Token 是否匹配
evil.com 不知道 Token 是什么 → 无法伪造请求
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 用户登录后: 服务器 → 生成 csrf_token = "random_string_xyz" 服务器 → 存入 Session 服务器 → 把 Token 嵌入到页面中(或通过 API 返回)
用户提交表单时: <form action="/transfer" method="POST"> <input type="hidden" name="csrf_token" value="random_string_xyz"> <input name="amount" value="100"> <button>转账</button> </form>
服务器收到请求时: 1. 从请求中取 csrf_token 2. 从 Session 中取 csrf_token 3. 对比是否一致 4. 不一致 → 拒绝请求(可能是 CSRF 攻击)
|
evil.com 无法伪造:因为它不知道 random_string_xyz,无法构造有效的请求。
方法 2:SameSite Cookie(现代浏览器的防御)
这个下一节单独讲。
方法 3:Referer/Origin 验证
1 2 3 4
| 服务器检查请求头的 Referer 或 Origin 字段 如果来源不是本站 → 拒绝
缺点:某些场景下 Referer 可能被禁用
|
方法 4:二次验证
1 2
| 敏感操作(转账、改密码)要求输入验证码或密码 即使 CSRF 攻击发出了请求,没有二次验证也无法执行
|
5.5 后端视角:CSRF 是后端的责任
这是后端开发出身的人需要理解的关键点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 后端 /api/transfer 收到请求 → 检查 Cookie → 有,登录状态有效 → 执行转账操作
后端认为:"请求有合法的 Cookie → 就是合法用户发的"
但实际上: 请求是 evil.com 发的(浏览器自动带了 Cookie) Cookie 是真的(你确实登录过) 但操作不是你本意(你甚至不知道)
所以后端必须做额外的验证(CSRF Token),来区分: "这个请求是用户自己发的" vs "这个请求是浏览器自动发送的"
|
6. SameSite Cookie —— 给 Cookie 上的锁
6.1 SameSite 是什么
SameSite 是 Cookie 的一个属性,告诉浏览器:“这个 Cookie 在跨站请求时,要不要自动带上?”
它是现代浏览器内置的 CSRF 防御机制。
6.2 SameSite 的三个值
1 2 3
| Set-Cookie: session_id=abc123; SameSite=Strict Set-Cookie: session_id=abc123; SameSite=Lax Set-Cookie: session_id=abc123; SameSite=None; Secure
|
| SameSite 值 |
行为 |
安全性 |
用户体验 |
| Strict |
任何跨站请求都不带 Cookie |
最安全 |
最差(从其他站点的链接点过来也不带 Cookie) |
| Lax |
大多数跨站请求不带,但安全的顶级导航(如点击链接、GET 表单)会带 |
适中 |
好(默认值) |
| None |
所有跨站请求都带 Cookie |
不安全 |
最好(但必须配合 Secure,即仅 HTTPS) |
6.3 各值的具体行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
<a href="https://bank.com/profile">去银行</a> SameSite=Strict → ❌ 不带 Cookie SameSite=Lax → ✅ 带 Cookie(安全顶级导航) SameSite=None → ✅ 带 Cookie
<img src="https://bank.com/transfer?amount=10000"> SameSite=Strict → ❌ 不带 Cookie SameSite=Lax → ❌ 不带 Cookie(不是顶级导航) SameSite=None → ✅ 带 Cookie
<form action="https://bank.com/transfer" method="POST"> SameSite=Strict → ❌ 不带 Cookie SameSite=Lax → ❌ 不带 Cookie(POST 不是安全方法) SameSite=None → ✅ 带 Cookie
fetch('https://bank.com/api/transfer', { method: 'POST' }) SameSite=Strict → ❌ 不带 Cookie SameSite=Lax → ❌ 不带 Cookie SameSite=None → ✅ 带 Cookie(但需要 CORS + credentials)
|
6.4 为什么默认是 Lax?
从 Chrome 80(2020 年)开始,如果 Cookie 没有设置 SameSite,浏览器默认视为 Lax。
1 2 3 4 5 6 7 8
| 2019 年之前: 默认行为 → SameSite=None(所有请求都带 Cookie) → CSRF 非常容易
2020 年之后: 默认行为 → SameSite=Lax → 大部分 CSRF 被自动防御 → 但需要跨站认证的场景(如第三方登录)需要显式设置为 None
|
6.5 SameSite=None 带来的问题
当你的应用需要跨站携带 Cookie 时(比如前端在 localhost:3000,后端在 localhost:8000,且用 Cookie 做认证):
1
| Set-Cookie: session=abc123; SameSite=None; Secure
|
这时:
- ✅ 跨站请求会带 Cookie
- ✅ 但必须配合 CORS 中的
credentials: 'include'
- ✅ 同时后端必须设置
Access-Control-Allow-Credentials: true
- ❌ 但 CSRF 风险增大了(因为跨站请求也会带 Cookie)
所以:SameSite=None + CORS 允许跨域 Cookie = 必须配合 CSRF Token!
6.6 对比:三种 Cookie 认证方式的 CSRF 防护能力
| Cookie 配置 |
CSRF 防护能力 |
适用场景 |
SameSite=Strict |
最强 |
银行等安全敏感场景 |
SameSite=Lax |
中等(默认) |
大多数网站 |
SameSite=None + Secure |
几乎没有内置防护 |
必须跨站认证时,必须配合 CSRF Token |
| 不用 Cookie(用 JWT 放 Header) |
天生防 CSRF |
前后端分离的 API 架构 |
7. 把这一切串起来:一次完整的请求过程
用一张图展示”一次请求”经过的所有安全关卡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 用户在前端页面(http://localhost:3000)点击"提交" │ ▼ 前端发出请求到后端(http://localhost:8000/api/submit) │ ├── 第一步:浏览器检查请求是否跨域 │ ├── 是 → 是否需要预检? │ │ ├── 需要 → 先发 OPTIONS │ │ │ → 检查服务器返回的 CORS 头 │ │ │ → CORS 不通过 → 拦截 ❌ │ │ │ → CORS 通过 → 继续发真正请求 │ │ └── 不需要 → 直接发请求 │ │ │ └── 否(同域)→ 直接发请求 │ ├── 第二步:浏览器决定是否携带 Cookie │ ├── Cookie 设置了 SameSite=Strict │ │ → 跨站 → ❌ 不携带 │ ├── Cookie 设置了 SameSite=Lax │ │ → 跨站且不是安全导航 → ❌ 不携带 │ ├── Cookie 设置了 SameSite=None │ │ → 跨站 → ✅ 携带(但需 CORS credentials) │ └── 同站 → ✅ 携带 │ ▼ 后端收到请求 │ ├── 第三步:后端验证身份(Cookie / JWT / Session) │ ├── 第四步:后端验证 CSRF Token │ ├── CSRF Token 缺失或不匹配 → 拒绝 ❌ │ └── CSRF Token 匹配 → 继续 ✅ │ ├── 第五步:后端处理业务逻辑 │ ▼ 后端返回响应 │ ├── 第六步:浏览器检查 CORS(如果是跨域) │ ├── 响应头 Access-Control-Allow-Origin 允许 → 放行 ✅ │ └── 不允许 → 拦截响应 ❌ │ ▼ 前端收到响应数据
|
每一道关卡都在解决不同的问题:
| 关卡 |
解决什么问题 |
谁的责任 |
| CORS 预检 |
判断跨域请求是否被服务器允许 |
后端配置 |
| CORS 头部 |
判断跨域响应是否可被前端读取 |
后端配置 |
| SameSite Cookie |
防止 Cookie 被跨站请求自动携带 |
后端配置 |
| CSRF Token |
验证请求是否来自用户的本意 |
后端验证 |
| 身份认证 |
验证请求者是谁 |
后端验证 |
8. 回到代码审查的那个问题
8.1 审查报告说了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ### 4. CSRF 保护缺失
发现位置:整个代码库(搜索 `csrf` 返回 0 结果)
仅依赖 `SameSite=Lax` cookie 策略,在跨域场景下不足。 特别是 `trustedOrigins` 配置了 HTTP 和非标准端口的来源:
trustedOrigins: [ "http://47.109.28.100:50085", ⚠️ 生产内网 IP 硬编码 "http://47.109.28.100:3000", "http://192.168.3.11:3000", ]
修复建议:启用 CSRF token 中间件 + trustedOrigins 从环境变量读取
|
8.2 逐句解读
“仅依赖 SameSite=Lax cookie 策略,在跨域场景下不足”
1 2 3 4 5 6 7 8 9 10 11 12 13
| SameSite=Lax 默认是不在跨站 POST 请求中携带 Cookie 的 这本身是一种 CSRF 防护
但问题在于: 你的 trustedOrigins 配置了跨域来源 你需要让这些来源能跨域带 Cookie 所以你的 Cookie 很可能设置成了 SameSite=None 或者你的应用是跨子域名部署的
一旦 SameSite=None: → 跨站请求也可以带 Cookie 了 → 浏览器不再自动帮你防 CSRF 了 → 必须自己加 CSRF Token 防护!
|
“trustedOrigins 配置了 HTTP 和非标准端口的来源”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| http://47.109.28.100:50085 ↑ ↑ ↑ HTTP IP 地址 非标准端口
问题 1:HTTP(不是 HTTPS) → SameSite=None 要求必须加 Secure 标志 → Secure 标志要求必须用 HTTPS → 但你的来源是 HTTP → 矛盾! → 有些浏览器会忽略 SameSite=None 的 Cookie
问题 2:内网 IP 硬编码 → 代码里直接写死了 IP → 换环境就要改代码 → 应该在环境变量中配置
问题 3:端口 50085 → 非标准端口进一步增加了跨域复杂度 → CORS 配置中每个端口都要单独列出
|
“修复建议”
1 2 3 4 5 6 7
| 1. 启用 CSRF Token 中间件 → Better Auth 自带这个功能 → 启用后,所有"写操作"请求都需要携带 CSRF Token
2. trustedOrigins 从环境变量读取 → 开发 / 测试 / 生产 用不同的配置 → 代码里不出现具体地址
|
8.3 到底该怎么配?
1 2 3 4 5 6 7 8
| import { createAuthClient } from "better-auth/client"
export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_API_URL, })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { betterAuth } from "better-auth"
export const auth = betterAuth({ trustedOrigins: process.env.TRUSTED_ORIGINS?.split(",") || [], csrf: { enabled: true, cookie: "csrf-token", header: "x-csrf-token", }, cookies: { session: { sameSite: process.env.NODE_ENV === "production" ? "strict" : "lax", secure: process.env.NODE_ENV === "production", } } })
|
1 2 3 4 5 6 7
| TRUSTED_ORIGINS=http://localhost:3000,http://localhost:5173 VITE_API_URL=http://localhost:8000
TRUSTED_ORIGINS=https://your-domain.com VITE_API_URL=https://api.your-domain.com
|
9. 最佳实践总结
9.1 不同架构下的配置建议
场景 1:传统服务端渲染(同域部署)
1 2 3 4 5 6 7
| 前端和后端在同一域名下: https://myapp.com/ → 前端 https://myapp.com/api/ → 后端
Cookie: SameSite=Lax(默认) CORS:不需要配置(同域) CSRF:需要 CSRF Token(传统表单提交)
|
场景 2:前后端分离同域
1 2 3 4 5 6 7 8
| 前端和后端在同域名下但端口不同: https://myapp.com:3000 → 前端 https://myapp.com:8000 → 后端
Cookie: SameSite=Lax(同站) CORS:需要配置(跨端口) CSRF:默认 SameSite=Lax 有一定防护 但如果用了 credentials + CORS,建议加 CSRF Token
|
场景 3:前后端分离跨域(最常见)
1 2 3 4 5 6 7 8 9
| 前端和后端在不同域名下: https://app.myapp.com → 前端 https://api.myapp.com → 后端
Cookie: SameSite=None + Secure CORS:需要配置(跨域名) Access-Control-Allow-Origin: https://app.myapp.com Access-Control-Allow-Credentials: true CSRF:必须加 CSRF Token!
|
场景 4:纯 API(无 Cookie,用 JWT)
1 2 3 4 5 6 7 8
| 前端和后端完全分离: Token 存储在 localStorage 或 memory 中 每次请求通过 Authorization Header 携带
Cookie:不需要(都不用 Cookie 了) CORS:需要配置 CSRF:天生免疫(CSRF 攻击依赖 Cookie 自动携带) 但不代表安全了——XSS 可以窃取 localStorage 的 Token
|
9.2 一张表看清所有方案
| 架构 |
Cookie SameSite |
CORS |
CSRF Token |
安全风险侧重 |
| 同域传统 |
Lax |
不需要 |
需要 |
传统 CSRF |
| 同站跨端口 |
Lax |
需要 |
推荐 |
端口级别的 CSRF |
| 跨子域名 |
None + Secure |
需要 |
必须 |
CSRF 风险最大 |
| 完全跨域 |
None + Secure |
需要 |
必须 |
CSRF 风险最大 |
| JWT + Header |
不用 Cookie |
需要 |
不需要 |
XSS(Token 被窃取) |
9.3 常见误区
| 误区 |
真相 |
| “加了 CORS 就能防 CSRF” |
不能。 CORS 管的是”能不能读取响应”,CSRF 不需要读响应 |
| “SameSite=Lax 就安全了” |
Lax 只防”跨站”的 POST,同站跨端口或跨子域名不防 |
| “我在开发环境没问题 = 生产环境也没问题” |
开发环境通常是 localhost(同域),生产环境跨域 → 配置完全不同 |
| “CSRF Token 跟 JWT 一样” |
完全两码事。JWT 是身份认证,CSRF Token 是操作确认 |
| “CORS 配置了 * 就行了” |
Access-Control-Allow-Origin: * 不允许带 Cookie! |
| “OPTIONS 请求不需要处理” |
OPTIONS 是预检,不处理前端就发不出真正请求 |
9.4 排查问题清单
当你遇到跨域/Cookie/CSRF 问题时,按这个顺序排查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 1️⃣ 是不是跨域? 前端 URL vs 后端 URL → 协议、主机、端口是否都一致?
2️⃣ 是不是预检请求被拦了? 打开浏览器 DevTools → Network → 看有没有 OPTIONS 请求? OPTIONS 返回了 200 吗?CORS 头齐全吗?
3️⃣ Cookie 带上了吗? DevTools → Application → Cookies → 有 Cookie 吗? SameSite 是什么值?Secure 要求 HTTPS 了吗?
4️⃣ CORS 头设置对了吗? Access-Control-Allow-Origin 包含前端域名吗? 需要带 Cookie → Allow-Credentials: true 了吗? Allow-Origin 不能用 *(当 credentials: true 时)
5️⃣ CSRF Token 需要吗? 后端有 CSRF 中间件吗? 前端在请求中带了 CSRF Token 吗?
|
10. 全篇总结
10.1 一句话记忆
| 概念 |
一句话 |
| 同源策略 |
浏览器默认禁止跨域读取资源,这是安全基石 |
| 跨域 |
协议/主机/端口有一个不同就是跨域 |
| CORS |
服务器告诉浏览器”我允许这个跨域请求”——解决的是”能不能读” |
| CSRF |
攻击者利用你的登录状态,让你在不知情下执行操作——利用的是”Cookie 自动携带” |
| SameSite |
Cookie 的属性,告诉浏览器”跨站请求要不要带 Cookie”——浏览器级的 CSRF 防御 |
| CSRF Token |
服务器验证”这个请求是用户本意发的”——服务端级的 CSRF 防御 |
| CORS 防不住 CSRF |
CORS 管读取,CSRF 不需要读取——这是最关键的区分 |
10.2 最终关系图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Web 安全 │ ┌──────────────┴──────────────┐ │ │ 跨域问题 CSRF 问题 │ │ ▼ ▼ 同源策略限制 Cookie 自动携带 │ │ ▼ ▼ CORS 解决方案 多种防御方案 (HTTP 头部声明) ┌────┼────┐ │ │ │ ▼ ▼ ▼ CSRF Token SameSite 验证来源 (服务端) (浏览器) (Referer)
|
10.3 后端开发者最容易犯的错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ❌ 错误想法 1: "我后端配了 CORS 就完事了" → 你只解决了跨域问题,没解决 CSRF 问题
❌ 错误想法 2: "既然用了 JWT + Header 认证,就不用管 CSRF 了" → 这个倒是真的,但 JWT 放 localStorage 有 XSS 风险
❌ 错误想法 3: "SameSite=Lax 是默认的,够了" → 如果你跨域名部署 + 用 Cookie 认证 + SameSite=None Lax 就不够了,必须加 CSRF Token
✅ 正确做法: 先搞清楚你的架构是哪一种 按上面的"不同架构下的配置建议"表来配
|
写在最后:跨域和 CSRF 是 Web 开发中最容易搞混的一对概念,因为它们经常同时出现、同时被讨论。但本质上:
- CORS 解决的是”浏览器让不让你读”的问题
- CSRF 防护解决的是”这个请求是不是用户本意”的问题
- SameSite 是浏览器帮你做的一道 CSRF 防线
作为后端开发者,理解这些概念的区别和联系,能让你在前后端协作时少走很多弯路。希望这篇文章能帮你把这一团乱麻彻底理清。