跨域、CORS、CSRF、SameSite —— 前端开发中最容易混淆的一套概念,彻底讲清楚

前言

“后端开发出身的人,第一次碰前端跨域问题,十有八九会被搞晕。”

我就是这样。

我在后端写接口写得好好的,前端一连就说”跨域报错”。我心想:“我接口都写对了,你浏览器凭什么拦我?”

然后浏览器说:“这是为你好,防止 CSRF 攻击。”

我又心想:“CSRF 又是什么东西?跨域跟 CSRF 什么关系?”

然后我去搜解决方案,有人跟我说加 CORS 头,有人跟我说用 SameSite cookie,有人跟我说 CSRF token……我彻底懵了。

这篇文章就是为了当时的我写的。从最底层开始,一层一层讲清楚:

  • 为什么浏览器要限制跨域?
  • CORS 是在解决什么问题?
  • CSRF 又是什么?跟跨域什么关系?
  • SameSite Cookie 又是干什么的?
  • 代码里 trustedOrigins 到底该配置什么?

最终你会发现:这些概念并不复杂,只是没人把它们串起来讲。


目录

  1. 三个核心问题
  2. 先搞清楚:什么是”跨域”?
  3. 为什么浏览器要限制跨域?
  4. CORS —— 跨域资源共享
  5. CSRF —— 跨站请求伪造
  6. SameSite Cookie —— 给 Cookie 上的锁
  7. 把这一切串起来:一次完整的请求过程
  8. 回到代码审查的那个问题
  9. 最佳实践总结
  10. 全篇总结

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', // ❌ 不是简单 Content-Type
'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.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
<!-- 用户在 evil.com 上 -->
<!-- 以下操作会触发向 bank.com 的请求 -->

<!-- 情况 1:点击链接 -->
<a href="https://bank.com/profile">去银行</a>
SameSite=Strict → ❌ 不带 Cookie
SameSite=Lax → ✅ 带 Cookie(安全顶级导航)
SameSite=None → ✅ 带 Cookie

<!-- 情况 2:加载图片 -->
<img src="https://bank.com/transfer?amount=10000">
SameSite=Strict → ❌ 不带 Cookie
SameSite=Lax → ❌ 不带 Cookie(不是顶级导航)
SameSite=None → ✅ 带 Cookie

<!-- 情况 3:POST 表单 -->
<form action="https://bank.com/transfer" method="POST">
SameSite=Strict → ❌ 不带 Cookie
SameSite=Lax → ❌ 不带 Cookie(POST 不是安全方法)
SameSite=None → ✅ 带 Cookie

<!-- 情况 4:fetch/Ajax -->
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!

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 逐句解读

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, // 从环境变量读取
// Better Auth 会自动处理 CSRF Token
// 请求时会自动在 header 中带上 csrf token
})
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 从环境变量读取
trustedOrigins: process.env.TRUSTED_ORIGINS?.split(",") || [],
// 启用 CSRF 保护
csrf: {
enabled: true, // 启用 CSRF Token 中间件
cookie: "csrf-token", // CSRF Token 放在 Cookie 中
header: "x-csrf-token", // 前端通过这个 header 传回 Token
},
// Cookie 配置
cookies: {
session: {
sameSite: process.env.NODE_ENV === "production"
? "strict" // 生产环境尽量用 strict
: "lax", // 开发环境可以用 lax
secure: process.env.NODE_ENV === "production", // 生产环境强制 HTTPS
}
}
})
1
2
3
4
5
6
7
# .env 文件(不要提交到 Git!)
TRUSTED_ORIGINS=http://localhost:3000,http://localhost:5173
VITE_API_URL=http://localhost:8000

# .env.production(生产环境)
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 防线

作为后端开发者,理解这些概念的区别和联系,能让你在前后端协作时少走很多弯路。希望这篇文章能帮你把这一团乱麻彻底理清。