前端 CORS 跨域与 Cookie 策略:SameSite、跨域携带凭证实战

2991 字
15 分钟
前端 CORS 跨域与 Cookie 策略:SameSite、跨域携带凭证实战

每天清晨,数以亿计的前端应用发出跨域请求:SPA 调用后端 API,微前端子应用拉取主站数据,第三方 SDK 回传遥测信息。CORS(跨域资源共享)是这些请求的守门人——它允许或拒绝,决定了你的应用能否正常工作。

然而,绝大多数前端开发者对 CORS 的理解仅限于「后端加个 Access-Control-Allow-Origin: *」。当请求突然带上 credentials: 'include' 失败时,当 SameSite 策略默默吃掉 Cookie 时,当预检请求 403 时,排查过程往往充满玄学。

本文将从浏览器同源策略出发,深入到 CORS 的完整握手流程、SameSite Cookie 的行为差异、跨域携带凭证的坑与解法,以及如何在调试面板中快速定位问题。读完之后,你将不再「盲目加 Header」,而是真正理解每一步的原理。

一、同源策略:为什么需要 CORS?#

1.1 什么是「同源」#

浏览器的**同源策略(Same-Origin Policy)**是 Web 安全的第一道防线。两个 URL 同源,必须满足三个条件完全一致:

条件示例 A示例 B是否同源
协议https://app.example.comhttp://app.example.com❌(https vs http)
域名https://app.example.comhttps://api.example.com❌(app vs api)
端口https://example.com:3000https://example.com:8080❌(3000 vs 8080)

同源策略的核心规则:非同源的脚本无法读取对方的 DOM、Cookie、LocalStorage 等敏感数据。如果没有这条规则,恶意网站可以通过 JavaScript 读取你的网银页面 Cookie。

1.2 跨域 ≠ 被禁止#

同源策略限制的是脚本读取响应,而不是发送请求。实际上,<img><script><form> 天然就可以跨域。CORS 的存在,是让 fetch / XMLHttpRequest 这些 AJAX 请求也能安全地跨域——但前提是目标服务器明确允许

浏览器发出跨域请求 → 服务器返回 CORS Header → 浏览器检查是否允许 → 允许则放行,否则拦截

关键点:跨域拦截发生在浏览器端,不是服务端。 服务端实际上已经收到了请求并返回了响应,只是浏览器在响应到达后判断「不允许」,然后对 JavaScript 屏蔽了响应内容。

二、CORS 的两种请求流程#

2.1 简单请求(Simple Request)#

满足以下所有条件的请求走简单流程:

  • 方法:GETHEADPOST
  • Header 仅限:AcceptAccept-LanguageContent-LanguageContent-Type(仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 没有自定义 Header
  • XMLHttpRequest.upload 没有事件监听器
// 简单请求示例
fetch('https://api.example.com/data', {
method: 'GET',
});
// 浏览器实际发出的请求头:
// GET /data HTTP/1.1
// Host: api.example.com
// Origin: https://app.example.com
// (注意:Origin 是浏览器自动添加的)

浏览器在请求头中自动加上 Origin: https://app.example.com。服务端必须在响应头中回以:

Access-Control-Allow-Origin: https://app.example.com

如果响应中没有这个 Header,或者值不匹配,浏览器会拦截响应,JavaScript 拿到的是网络错误。

2.2 预检请求(Preflight Request)#

不满足简单请求条件的,浏览器会先发一个 OPTIONS 预检请求:

// 触发电检请求的条件
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 非简单 Content-Type
'X-Custom-Header': 'value', // 自定义 Header
},
body: JSON.stringify({ key: 'value' }),
});
// 浏览器实际发出的预检请求:
// OPTIONS /data HTTP/1.1
// Host: api.example.com
// Origin: https://app.example.com
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: content-type, x-custom-header

服务端必须正确响应预检请求:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: content-type, x-custom-header
Access-Control-Max-Age: 86400
响应头作用示例值
Access-Control-Allow-Origin允许的源(必须精确匹配或用 *https://app.example.com
Access-Control-Allow-Methods允许的 HTTP 方法GET, POST, PUT, DELETE
Access-Control-Allow-Headers允许的自定义请求头content-type, authorization
Access-Control-Max-Age预检结果缓存秒数86400(24小时)
Access-Control-Allow-Credentials是否允许携带凭证true 或省略

⚠️ 常见死锁:*credentials 互斥

# ❌ 非法组合:浏览器直接拒绝
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

CORS 规范明确规定:当 Access-Control-Allow-Origin* 时,不能同时设置 Access-Control-Allow-Credentials: true。这是很多开发者踩到的第一个坑。

3.1 SameSite 的三个值#

2016 年引入的 SameSite 属性,用来控制 Cookie 在跨域场景下的发送行为。这是理解跨域携带凭证的核心。

行为适用场景
Strict完全不发送跨域 Cookie银行、支付等极高安全场景
Lax仅顶级导航的 GET 请求发送(点击链接、表单提交)Chrome 80+ 默认值
None所有跨域请求都发送需要跨域携带 Cookie 的场景(需配合 Secure
# Set-Cookie 示例
Set-Cookie: session_id=abc123; SameSite=Lax; Path=/
Set-Cookie: session_id=abc123; SameSite=None; Secure; Path=/
Set-Cookie: csrf_token=xyz; SameSite=Strict; Path=/

⚠️ Chrome 80+ 的重大变更:

从 Chrome 80(2020 年)开始,未显式设置 SameSite 的 Cookie 默认视为 SameSite=Lax。这意味着:

  • fetch() / XMLHttpRequest 跨域请求不会携带 Cookie
  • <img src> 跨域不会携带 Cookie
  • 只有 <a href> 点击跳转和 <form method="GET"> 会携带

如果你的后端没有显式设置 SameSite=None; Secure,前端设置 credentials: 'include' 也不会携带 Cookie。这不是前端的 bug,而是浏览器的安全策略。

3.2 SameSite=Lax 的 POST 表单例外#

SameSite=Lax 有一个重要的例外:顶级导航 + POST 表单也会发送 Cookie。

<!-- ✅ SameSite=Lax 的 Cookie 会发送 -->
<a href="https://api.example.com/login">点击跳转</a>
<form action="https://api.example.com/submit" method="POST">
<input name="data" value="test">
<button type="submit">提交</button>
</form>
<!-- ❌ SameSite=Lax 的 Cookie 不会发送 -->
<script>
fetch('https://api.example.com/data', {
method: 'POST',
credentials: 'include', // 即使设置了也不会带 Cookie
});
</script>

四、跨域携带凭证:credentials 实战#

4.1 三种凭证模式#

// 1. omit(默认):从不发送 Cookie
fetch('https://api.example.com/data', {
credentials: 'omit',
});
// 2. same-origin:仅同源请求发送 Cookie
fetch('https://same-origin.example.com/data', {
credentials: 'same-origin',
});
// 3. include:所有请求都尝试发送 Cookie
fetch('https://api.example.com/data', {
credentials: 'include',
});

4.2 完整的前后端配合方案#

要让跨域请求携带 Cookie,需要前后端同时正确配置,缺一不可:

前端代码#

src/api/client.ts
const API_BASE = 'https://api.example.com';
export async function fetchWithAuth(path: string, options: RequestInit = {}) {
const response = await fetch(`${API_BASE}${path}`, {
...options,
credentials: 'include', // 关键:允许跨域携带 Cookie
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// 使用
const data = await fetchWithAuth('/user/profile');

后端配置(Express / Node.js)#

// server.js - Express 后端 CORS 配置
const express = require('express');
const cors = require('cors');
const app = express();
const ALLOWED_ORIGIN = 'https://app.example.com';
// 手动配置 CORS(推荐,比 cors 中间件更灵活)
app.use((req, res, next) => {
const origin = req.headers.origin;
// 验证 Origin 是否在白名单中
if (origin === ALLOWED_ORIGIN) {
res.setHeader('Access-Control-Allow-Origin', origin); // 不能用 *
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type, authorization');
res.setHeader('Access-Control-Max-Age', '86400');
}
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
// 设置 Cookie
app.post('/api/login', (req, res) => {
const token = generateToken(req.body.username);
res.cookie('session_id', token, {
httpOnly: true, // 防 XSS
secure: true, // 仅 HTTPS(SameSite=None 必须)
sameSite: 'none', // 允许跨域发送
path: '/',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
});
res.json({ success: true });
});
// 验证 Cookie
app.get('/api/profile', (req, res) => {
const sessionId = req.cookies?.session_id;
if (!sessionId) {
return res.status(401).json({ error: '未登录' });
}
const user = verifyToken(sessionId);
res.json({ user });
});
app.listen(3000);

后端配置(Python FastAPI)#

# main.py - FastAPI 后端 CORS 配置
from fastapi import FastAPI, Response, Request
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 使用 FastAPI 内置 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # 精确匹配,不能用 ["*"]
allow_credentials=True, # 允许携带凭证
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["content-type", "authorization"],
max_age=86400,
)
@app.post("/api/login")
async def login(response: Response):
token = generate_token("user123")
# 关键:same_site="none" + secure=True
response.set_cookie(
key="session_id",
value=token,
httponly=True,
secure=True,
samesite="none",
max_age=7 * 24 * 60 * 60,
path="/",
)
return {"success": True}
@app.get("/api/profile")
async def get_profile(request: Request):
session_id = request.cookies.get("session_id")
if not session_id:
return {"error": "未登录"}
user = verify_token(session_id)
return {"user": user}

4.3 部署环境的关键约束#

约束说明违反后果
HTTPSSameSite=None 的 Cookie 必须设置 Secure 属性Cookie 不会被设置
精确 OriginAccess-Control-Allow-Origin 不能是 *浏览器拒绝携带 Cookie
域名匹配Cookie 的 Domain 必须匹配请求域名Cookie 不会发送
Path 匹配Cookie 的 Path 必须匹配请求路径Cookie 不会发送
端口独立:3000:8080 是不同源各自需要独立的 CORS 配置

五、常见场景与解决方案#

5.1 场景一:开发环境跨域调试#

本地开发时前端 http://localhost:5173,后端 http://localhost:3000,需要携带 Cookie:

// vite.config.ts - 使用 Vite Proxy(开发时推荐)
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
cookieDomainRewrite: 'localhost', // 重写 Cookie Domain
},
},
},
});

使用 Proxy 方案,浏览器认为请求是同源的,完全绕开 CORS。这是开发环境的最佳实践。

5.2 场景二:子域名跨域#

前端 https://app.example.com,后端 https://api.example.com,Cookie 在 example.com 域下:

# 后端设置 Cookie
Set-Cookie: session_id=abc; Domain=.example.com; SameSite=None; Secure; Path=/
# ↑ 注意前面的点:.example.com 表示所有子域名共享
# 后端 CORS 响应
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
// 前端请求
fetch('https://api.example.com/data', {
credentials: 'include', // 会携带 Domain=.example.com 的 Cookie
});

5.3 场景三:多环境动态 Origin#

生产环境有多个前端域名(主站、管理后台、H5),需要动态匹配:

// Node.js 多 Origin 动态验证
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
'https://m.example.com',
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});

⚠️ 安全警告: 不要用 req.headers.origin 直接回写(res.setHeader('Access-Control-Allow-Origin', req.headers.origin)),这等于 *。必须用白名单验证。

六、调试指南:如何快速定位 CORS 问题#

6.1 Chrome DevTools 检查清单#

打开 DevTools → Network 面板 → 点击失败的请求:

  1. Headers 标签 → General:确认 Request URL 的协议/域名/端口
  2. Headers 标签 → Response Headers:检查是否有 Access-Control-Allow-* 系列头
  3. Headers 标签 → Request Headers:确认 Origin 是否正确发送
  4. Application 标签 → Cookies:检查 Cookie 的 SameSite、Secure、Domain、Path 属性
  5. Console 面板:查看具体的 CORS 错误信息

6.2 典型错误与排查路径#

错误信息原因解决方法
Access to fetch at '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header服务端未返回 CORS Header后端添加 Access-Control-Allow-Origin
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'*credentials: 'include' 冲突后端改用精确 Origin
Cookie "session_id" will be soon rejected because it has the "SameSite" attribute set to "None" without the "Secure" attributeSameSite=None 缺少 SecureCookie 加 Secure 属性,并使用 HTTPS
Response to preflight request doesn't pass access control check: It does not have HTTP ok status预检请求返回非 2xx 状态码后端 OPTIONS 返回 204
NetworkError when attempting to fetch resource可能是网络问题或 CORS 拦截检查 Network 面板的实际响应

6.3 用 curl 绕过浏览器验证#

当不确定是后端问题还是浏览器问题时,用 curl 直接请求:

Terminal window
# 模拟浏览器请求(带 Origin)
curl -v -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type"
# 检查响应头中的 CORS 字段
# 如果 curl 能收到正确的 CORS Header,说明是前端代码问题
# 如果 curl 也收不到,说明是后端配置问题

七、Cookie 安全最佳实践#

// 生产环境 Session Cookie 推荐配置
{
httpOnly: true, // 防 XSS(JavaScript 无法读取)
secure: true, // 仅 HTTPS 传输
sameSite: 'none', // 跨域携带(需 secure: true)
path: '/', // 全站有效
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
// domain: '.example.com', // 跨子域共享(按需启用)
}

7.2 CSRF Token 防护#

当使用 Cookie 携带凭证时,CSRF 攻击成为现实威胁。防御方案:

// 方案一:Double Submit Cookie(推荐)
// 1. 后端设置两个 Cookie
Set-Cookie: session_id=abc; HttpOnly; Secure; SameSite=None
Set-Cookie: csrf_token=xyz; Secure; SameSite=None // JS 可读
// 2. 前端读取 csrf_token 并放入请求头
const csrfToken = getCookie('csrf_token');
fetch('/api/data', {
method: 'POST',
credentials: 'include',
headers: {
'X-CSRF-Token': csrfToken,
},
});
// 3. 后端验证 Header 中的 token 与 Cookie 中的 token 一致
// 攻击者无法读取 Cookie(同源策略),也无法伪造 Header(预检拦截)
方案优点缺点
Double Submit Cookie实现简单,无需服务端存储依赖子域名隔离
SameSite Cookie浏览器原生防护旧浏览器不支持
Custom Header + 预检双重保障增加一次预检请求

八、总结#

CORS 和 Cookie 策略的核心逻辑可以用一句话概括:浏览器是守门人,服务端是规则制定者,前端是遵守者

层次角色关键动作
服务端规则制定者返回正确的 Access-Control-Allow-*Set-Cookie
浏览器守门人检查同源策略、SameSite、CORS Header
前端遵守者设置 credentials: 'include',处理错误

排查 CORS 问题的三个原则:

  1. 看 DevTools:Network 面板的 Request/Response Headers 比任何猜测都准确
  2. 分步验证:先确认后端返回正确的 CORS Header,再检查 Cookie 属性,最后看前端代码
  3. 理解 SameSite:Chrome 80+ 的默认 Lax 是大多数跨域 Cookie 问题的根源

延伸阅读#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

前端 CORS 跨域与 Cookie 策略:SameSite、跨域携带凭证实战
https://boke.hackerdream.xyz/posts/frontend-cors-cookie-policy/
作者
晴天
发布于
2026-06-02
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
前端 Observer API 全家桶:Intersection、Resize 与 MutationObserver 深度实战
浏览器与性能 从"DOM 变了怎么办"出发,系统讲清 IntersectionObserver、ResizeObserver、MutationObserver 三大原生 API 的工作机制、踩坑点与组合实战,并给出一套可复用的生产级懒加载 + 无限滚动 + 自适应布局 SDK。
2
CSS Subgrid 深度实战:告别嵌套布局的对齐噩梦
前端架构 全面解析 CSS Subgrid 的核心原理与实战技巧,通过卡片列表、表单、仪表盘等真实场景演示如何用 subgrid 优雅解决嵌套网格对齐难题,附浏览器兼容方案。
3
H5 唤起 App 完整方案:Scheme、Universal Link、wx-open-launch-app 与微信兜底
前端开发 H5 页面唤起原生 App 是前端开发的经典难题——微信拦截 scheme、iOS Universal Link 配置繁琐、Android Intent 行为不一。本文给出两套方案的完整流转流程:系统浏览器走 Scheme/Universal Link,微信内走 wx-open-launch-app + 右上角浏览器引导兜底。附流程图、关键代码和生产环境检查清单。
4
这个 GitHub 爆火的 WCAG 无障碍 Skill 到底能干嘛?实测来了
AI工具 16.7 万星项目 everything-claude-code 中的无障碍(Accessibility)Skill,教你让网站和 App 对所有用户友好,覆盖 Web/iOS/Android 三端,含完整代码示例和最佳实践清单。
5
Web Crypto 安全认证实战:从 JWT 签名到端到端加密
浏览器与安全 深入浏览器原生 Web Crypto API 在认证场景的实战用法,从 HMAC 签名、JWT 验签、ECDH 密钥交换到端到端加密通道,配套完整可运行代码、攻击模型分析与生产级最佳实践。
随机文章 随机推荐
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
155
分类
24
标签
387
总字数
345,424
运行时长
0
最后活动
0 天前

目录