跨域解决方案
What — 是什么
浏览器的同源策略(Same-Origin Policy)限制不同源的 JS 读取资源,跨域解决方案用于合法绕过这一限制实现前后端通信。
同源策略
同源定义: 协议(Protocol)+ 域名(Host)+ 端口(Port)完全一致才算同源,任一不同即为跨域。
| URL A | URL B | 是否同源 | 原因 |
|---|---|---|---|
https://example.com/a | https://example.com/b | 同源 | 协议/域名/端口一致 |
https://example.com | http://example.com | 跨域 | 协议不同(https vs http) |
https://example.com | https://www.example.com | 跨域 | 域名不同(子域也算不同源) |
https://example.com | https://example.com:8080 | 跨域 | 端口不同 |
https://example.com | https://192.168.1.1 | 跨域 | 域名与 IP 不同 |
同源策略的完整限制范围:
| 限制维度 | 受限行为 | 说明 |
|---|---|---|
| DOM 访问 | 跨源 iframe 的 JS 无法读写对方 DOM | document.getElementById 等操作被拒绝 |
| Cookie / Storage | 无法读取跨源的 Cookie、LocalStorage、IndexedDB | 即使是同域不同端口也不行 |
| XMLHttpRequest / Fetch | 跨源网络请求的响应被浏览器拦截 | 请求已发出,只是 JS 无法读取响应 |
| 嵌入资源 | 不受限 | <img>、<script>、<link>、<video> 等可跨域加载 |
注意:跨域请求实际上已经发出并到达了服务器,服务器也返回了响应,只是浏览器拦截了 JS 对响应的读取。这是同源策略的关键——它不是阻止请求发出,而是阻止读取响应。
嵌入资源的例外(不受同源限制):
<script src="...">— 加载跨域 JS(JSONP 的基础)<link href="...">— 加载跨域 CSS<img src="...">— 加载跨域图片(但 Canvas 读取跨域图片数据会被污染)<video>/<audio>— 加载跨域媒体<iframe>— 嵌入跨域页面(但 JS 无法访问内部 DOM)
CORS(Cross-Origin Resource Sharing)
核心概念:
- CORS:服务端设置
Access-Control-Allow-*响应头,浏览器据此判断是否允许跨域,是 W3C 标准方案 - 预检请求:非简单请求先发
OPTIONS请求确认是否允许 - 代理:开发时通过同源服务器转发请求,绕过浏览器限制
简单请求与非简单请求
简单请求的完整条件(必须同时满足):
- 方法:仅限
GET、POST、HEAD - Header:仅限浏览器自动设置的 Header 和以下安全 Header:
AcceptAccept-LanguageContent-LanguageContent-Type(仅限下述三种值)DPR、Downlink、Save-Data、Viewport-Width、Width
- Content-Type:仅限以下三种:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
- 请求中没有使用
ReadableStream对象 - 请求中没有注册任何事件监听器(XMLHttpRequest)
非简单请求(触发预检的条件,任一即触发):
- 使用
PUT、DELETE、PATCH等方法 - 设置了自定义 Header(如
Authorization、X-Token) Content-Type为application/json- 使用了
ReadableStream
预检请求完整流程
浏览器 服务器
| |
| 1. OPTIONS /api/data |
| Access-Control-Request-Method: PUT
| Access-Control-Request-Headers: Authorization, Content-Type
| Origin: https://frontend.com |
| ─────────────────────────────────────>|
| |
| 2. 204 No Content |
| Access-Control-Allow-Origin: https://frontend.com
| Access-Control-Allow-Methods: GET, POST, PUT, DELETE
| Access-Control-Allow-Headers: Authorization, Content-Type
| Access-Control-Allow-Credentials: true
| Access-Control-Max-Age: 86400 |
|<─────────────────────────────────────|
| |
| 3. PUT /api/data |
| Authorization: Bearer xxx |
| Content-Type: application/json |
| Origin: https://frontend.com |
| ─────────────────────────────────────>|
| |
| 4. 200 OK |
| Access-Control-Allow-Origin: https://frontend.com
| Access-Control-Allow-Credentials: true
|<─────────────────────────────────────|
流程说明:
- 浏览器发现请求不满足简单请求条件,自动发出
OPTIONS预检请求 - 服务器返回允许的方法、Header 和 Origin
- 预检通过后,浏览器发送实际请求
- 服务器返回实际响应(同样需要 CORS 头)
- 如果预检失败,浏览器不会发送实际请求,控制台报 CORS 错误
CORS 响应头完整列表
| 响应头 | 说明 | 示例 |
|---|---|---|
Access-Control-Allow-Origin | 允许的源,* 或具体 URL | https://example.com |
Access-Control-Allow-Methods | 允许的 HTTP 方法 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 允许的请求 Header | Content-Type, Authorization |
Access-Control-Allow-Credentials | 是否允许携带 Cookie | true |
Access-Control-Max-Age | 预检结果缓存时间(秒) | 86400 |
Access-Control-Expose-Headers | 允许 JS 读取的响应 Header | X-Total-Count, X-Request-Id |
关键细节:
Allow-Origin与Allow-Credentials: true不能同时使用*,必须指定具体 OriginExpose-Headers默认只暴露 6 个简单响应头:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,自定义 Header 需要Expose-Headers声明才能被 JS 读取Max-Age缓存期间浏览器不会再次发送预检请求,不同浏览器有上限(Chrome 7200s / Firefox 86400s)
跨域与 Cookie
Cookie 的跨域行为非常特殊,需要理解以下属性:
| 属性 | 作用 | 跨域影响 |
|---|---|---|
Domain | Cookie 的域名作用域 | 默认当前域名,不含子域;设置 .example.com 含所有子域 |
Path | Cookie 的路径作用域 | 默认 /,仅匹配路径下发送 |
SameSite | 跨站发送策略 | Strict / Lax / None(见下表) |
Secure | 仅 HTTPS 发送 | 跨域 + HTTPS 场景必设 |
HttpOnly | JS 不可读 | 防止 XSS 窃取,与跨域无直接关系 |
SameSite 属性详解:
| SameSite 值 | 跨站 Cookie 行为 | 典型场景 |
|---|---|---|
Strict | 完全禁止跨站发送,包括从外部链接跳转 | 银行等高安全场景 |
Lax(默认) | 允许顶级导航的 GET 请求携带,其余禁止 | 大多数网站默认行为 |
None | 允许跨站发送,但必须同时设置 Secure | 第三方登录、嵌入式场景 |
从 Chrome 80 开始,
SameSite默认值从None改为Lax,这意味着跨站 POST 请求、iframe 内请求、AJAX 请求都不会携带 Cookie,除非显式设置SameSite=None; Secure。
跨域携带 Cookie 的完整条件:
- 前端:
fetch设置credentials: 'include',或XMLHttpRequest设置withCredentials = true - 后端:
Access-Control-Allow-Credentials: true - 后端:
Access-Control-Allow-Origin必须是具体值,不能是* - Cookie 属性:
SameSite=None; Secure(跨站场景) - Cookie 的
Domain必须匹配请求域名
其他跨域方案
JSONP
原理: <script> 标签的 src 不受同源策略限制,服务器返回 callback(data) 形式的 JS 代码,前端通过回调函数获取数据。
局限: 只支持 GET、无状态码、XSS 风险、错误处理困难。
postMessage
原理: window.postMessage() 允许跨源窗口之间安全传递消息,是 iframe / window.open 跨域通信的标准方案。
局限: 仅限窗口间消息通信,不适用于 HTTP 请求。
WebSocket
原理: WebSocket 协议不受同源策略限制,建立连接时使用 HTTP Upgrade 握手,之后转为全双工通信。
局限: 需要服务端支持 WebSocket 协议。
document.domain(已废弃)
原理: 将两个页面的 document.domain 设置为相同的主域,可使其”同源”。
局限: 仅适用于子域之间(a.example.com 与 b.example.com),Chrome 115+ 已废弃。
window.name
原理: window.name 在不同页面加载间保持不变,跨域 iframe 加载后通过 iframe.contentWindow.name 读取。
局限: 仅支持字符串数据,容量约 2MB,属于 Hack 方案,不推荐使用。
Why — 为什么
同源策略的安全意义
同源策略是浏览器最基础的安全防线,缺失将导致严重安全问题:
1. CSRF(跨站请求伪造)防护
没有同源策略,恶意网站 evil.com 可以用用户的 Cookie 向 bank.com 发起转账请求,并读取响应内容确认转账结果。同源策略阻止了 evil.com 的 JS 读取 bank.com 的响应,即使请求已被发出。
2. XSS 数据窃取防护
如果攻击者在 example.com 注入了恶意脚本,同源策略阻止该脚本读取 bank.com 的 DOM 和 Cookie(即使两个标签页同时打开),防止跨站数据窃取。
3. 数据隔离
不同源的页面互不干扰:
- 用户在
gmail.com的邮件内容不会被evil.com的 JS 读取 facebook.com的登录状态不会被tracker.com直接获取- 不同公司的 SaaS 应用互不影响
本质:同源策略让”信任边界”以源为单位,一个源内的资源互相信任,跨源访问必须显式授权。
跨域场景分类
| 场景类型 | 典型案例 | 推荐方案 |
|---|---|---|
| 开发环境跨域 | localhost:3000 → localhost:8080 | Vite/Webpack 代理 |
| 生产环境-前后端分离 | app.example.com → api.example.com | Nginx 同源代理 / CORS |
| 生产环境-第三方 API | 调用微信/支付宝开放平台 | CORS(由第三方配置) |
| 微前端跨域 | 主应用加载远程子应用 | postMessage / CORS |
| iframe 嵌入 | 父页面与跨域 iframe 通信 | postMessage |
| 实时通信 | 跨域 WebSocket 连接 | WebSocket(无跨域限制) |
| 旧系统兼容 | 不支持 CORS 的老接口 | JSONP |
方案完整对比
| 维度 | CORS | Nginx 代理 | JSONP | postMessage | WebSocket |
|---|---|---|---|---|---|
| 支持方法 | 所有 HTTP | 所有 HTTP | 仅 GET | 消息通信 | 全双工消息 |
| 改动量 | 后端加 Header | Nginx 配置 | 前后端改 | 前端改 | 前后端改 |
| 安全性 | 好(浏览器校验) | 好(服务端控制) | 差(XSS 风险) | 中(需校验 Origin) | 好 |
| 预检开销 | 有(非简单请求) | 无 | 无 | 无 | 无 |
| 浏览器兼容 | IE10+ | 无限制 | IE6+ | IE8+ | IE10+ |
| Cookie 支持 | 需配置 Credentials | 同源自动携带 | 需手动处理 | 不涉及 | 不涉及 |
| 适用场景 | 通用 HTTP 请求 | 开发/生产 | 旧系统兼容 | iframe 通信 | 实时双向通信 |
How — 怎么用
快速上手
后端 CORS 配置(Node.js Express):
const cors = require('cors');
// 允许所有源(开发环境)
app.use(cors());
// 生产环境:指定源
app.use(cors({
origin: ['https://example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 允许携带 Cookie
maxAge: 86400, // 预检缓存 24 小时
}));
代码示例
示例 1:前端代理(Vite 开发环境)
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
Webpack 代理配置:
// webpack.config.js / vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};
示例 2:Nginx 反向代理(生产环境)
server {
listen 80;
server_name example.com;
location / {
root /var/www/html; # 前端静态文件
}
location /api/ {
proxy_pass http://backend:8080/; # 转发到后端
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
示例 3:手动处理 CORS 预检
// 后端手动处理(无框架时)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400');
// 预检请求直接返回 204
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
示例 4:JSONP 完整实现(含 Promise 封装、超时处理、错误处理)
/**
* JSONP 请求封装
* @param {string} url - 请求地址
* @param {object} params - 请求参数
* @param {object} options - 配置项
* @param {string} options.callbackName - 回调函数名(默认 jsonp_cb_${timestamp})
* @param {number} options.timeout - 超时时间(默认 10000ms)
* @returns {Promise<any>}
*/
function jsonp(url, params = {}, options = {}) {
const {
callbackName = `jsonp_cb_${Date.now()}_${Math.random().toString(36).slice(2)}`,
timeout = 10000,
} = options;
return new Promise((resolve, reject) => {
// 超时定时器
const timer = setTimeout(() => {
cleanup();
reject(new Error(`JSONP request timeout: ${url}`));
}, timeout);
// 定义全局回调函数
window[callbackName] = (data) => {
cleanup();
resolve(data);
};
// 清理函数:移除 script 标签和全局回调
function cleanup() {
clearTimeout(timer);
delete window[callbackName];
if (script.parentNode) {
script.parentNode.removeChild(script);
}
}
// 构建 URL
const queryStr = Object.entries({ ...params, callback: callbackName })
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join('&');
const fullUrl = `${url}?${queryStr}`;
// 创建 script 标签
const script = document.createElement('script');
script.src = fullUrl;
script.onerror = () => {
cleanup();
reject(new Error(`JSONP request failed: ${url}`));
};
// 添加到 DOM 触发请求
document.head.appendChild(script);
});
}
// 使用示例
jsonp('https://api.example.com/data', { id: 1 }, { timeout: 5000 })
.then((data) => console.log('数据:', data))
.catch((err) => console.error('错误:', err));
// Node.js 后端配合
app.get('/api/data', (req, res) => {
const callback = req.query.callback;
const data = { name: 'Alice', age: 25 };
// 返回 callback(data) 形式的 JS
res.type('text/javascript').send(`${callback}(${JSON.stringify(data)})`);
});
示例 5:postMessage 跨窗口通信
iframe 父子页面通信:
<!-- 父页面:https://parent.com -->
<iframe id="child" src="https://child.com/page"></iframe>
<script>
const iframe = document.getElementById('child');
// 向子页面发送消息
iframe.onload = () => {
iframe.contentWindow.postMessage(
{ type: 'PARENT_MSG', data: 'Hello from parent' },
'https://child.com' // 必须指定目标 Origin,不要用 '*'
);
};
// 接收子页面消息
window.addEventListener('message', (event) => {
// 安全校验:验证 Origin
if (event.origin !== 'https://child.com') return;
console.log('收到子页面消息:', event.data);
});
</script>
<!-- 子页面:https://child.com/page -->
<script>
// 接收父页面消息
window.addEventListener('message', (event) => {
// 安全校验:验证 Origin
if (event.origin !== 'https://parent.com') return;
console.log('收到父页面消息:', event.data);
// 向父页面回复消息
event.source.postMessage(
{ type: 'CHILD_REPLY', data: 'Hello from child' },
event.origin
);
});
</script>
window.open 跨窗口通信:
// 打开新窗口
const popup = window.open('https://other.com/page', '_blank', 'width=600,height=400');
// 向新窗口发送消息
popup.postMessage({ type: 'MAIN_MSG', data: 'Hello' }, 'https://other.com');
// 新窗口中接收
window.addEventListener('message', (event) => {
if (event.origin !== 'https://main.com') return;
console.log('收到主窗口消息:', event.data);
});
安全要点:发送时始终指定
targetOrigin,接收时始终校验event.origin,不要使用'*'。
示例 6:WebSocket 跨域通信
// 前端:WebSocket 连接不受同源策略限制
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
console.log('WebSocket 连接已建立');
ws.send(JSON.stringify({ type: 'greeting', data: 'Hello' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
};
ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
ws.onclose = (event) => {
console.log('WebSocket 关闭:', event.code, event.reason);
};
// Node.js 后端(ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
// 可通过 req.headers.origin 校验来源
const origin = req.headers.origin;
const allowedOrigins = ['https://example.com', 'https://admin.example.com'];
if (!allowedOrigins.includes(origin)) {
ws.close(4001, 'Origin not allowed');
return;
}
ws.on('message', (message) => {
console.log('收到:', message.toString());
ws.send(JSON.stringify({ type: 'echo', data: message.toString() }));
});
});
示例 7:CORS 中间件生产级配置(动态 Origin 白名单)
// corsMiddleware.js — 生产级 CORS 中间件
const allowedOrigins = [
'https://www.example.com',
'https://admin.example.com',
'https://m.example.com',
];
// 支持正则匹配(适合多环境部署)
const originRegex = /^https:\/\/[a-z0-9-]+\.example\.com$/;
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (!origin) {
// 非浏览器请求或同源请求,无需 CORS
return next();
}
// 动态判断 Origin 是否允许
const isAllowed = allowedOrigins.includes(origin) || originRegex.test(origin);
if (!isAllowed) {
return res.status(403).json({ error: 'CORS origin not allowed' });
}
// 设置 CORS 响应头
res.header('Access-Control-Allow-Origin', origin); // 动态设置,不用 *
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400');
// 暴露自定义响应头供前端读取
res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Request-Id');
// 预检请求直接返回
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
}
module.exports = corsMiddleware;
// 使用
app.use(corsMiddleware);
示例 8:Fetch API credentials 三种模式
// 1. same-origin(默认):同源请求携带 Cookie,跨域不携带
fetch('https://api.example.com/user', {
credentials: 'same-origin',
});
// 2. include:无论同源跨域都携带 Cookie
fetch('https://api.example.com/user', {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
// 3. omit:任何请求都不携带 Cookie
fetch('https://api.example.com/public-data', {
credentials: 'omit',
});
// XMLHttpRequest 对比
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/user');
xhr.withCredentials = true; // 等同于 credentials: 'include'
xhr.send();
| credentials 模式 | 同源 | 跨域 | 典型场景 |
|---|---|---|---|
same-origin | 携带 | 不携带 | 默认行为,适合大多数场景 |
include | 携带 | 携带 | 跨域携带 Cookie 登录态 |
omit | 不携带 | 不携带 | 公开 API,无需认证 |
示例 9:Nginx 完整生产配置(含 HTTPS、CORS Header、代理)
# HTTP → HTTPS 重定向
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS 主配置
server {
listen 443 ssl http2;
server_name example.com;
# SSL 证书
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 安全 Header
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 前端静态资源
location / {
root /var/www/html;
try_files $uri $uri/ /index.html; # SPA 路由回退
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
# API 代理(同源方案,浏览器无跨域)
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 代理支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时配置
proxy_connect_timeout 60s;
proxy_read_timeout 120s;
proxy_send_timeout 60s;
}
# 如果 API 与前端不同域,需要 CORS Header
location /external-api/ {
proxy_pass http://backend:8080/;
# CORS Header(由 Nginx 添加,无需后端处理)
add_header Access-Control-Allow-Origin "https://example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 86400 always;
# 预检请求处理
if ($request_method = OPTIONS) {
return 204;
}
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
示例 10:跨域上传文件(FormData + CORS)
// 前端:跨域上传文件
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('userId', '123');
try {
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData, // 不设 Content-Type,浏览器自动设 multipart/form-data + boundary
credentials: 'include', // 携带 Cookie
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status}`);
}
const result = await response.json();
console.log('上传成功:', result);
return result;
} catch (error) {
console.error('上传错误:', error);
throw error;
}
}
// 使用
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', (e) => {
uploadFile(e.target.files[0]);
});
// 后端:Express 处理跨域文件上传
const multer = require('multer');
const upload = multer({ dest: 'uploads/', limits: { fileSize: 10 * 1024 * 1024 } }); // 10MB 限制
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
});
});
注意:上传文件时不要手动设置
Content-Type: multipart/form-data,浏览器会自动设置带 boundary 的完整 Content-Type,手动设置会导致服务端无法解析。
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 预检请求失败 | 后端未处理 OPTIONS | 添加 OPTIONS 处理,返回 204 |
| Cookie 不发送 | withCredentials 未设置 | 前端 fetch 加 credentials: 'include',后端 Allow-Credentials: true |
Allow-Origin 不能用 * + Credentials | 浏览器安全限制 | 必须指定具体 Origin |
| 部分接口跨域部分不跨域 | 只需要某些路由跨域 | 按路由设置 CORS,而非全局 |
| 自定义响应头前端读不到 | 默认只暴露 6 个简单头 | 后端设置 Access-Control-Expose-Headers |
| 301/302 重定向后 CORS 丢失 | 重定向后 Origin 变为 null | 避免跨域请求中的重定向,或在重定向目标也配置 CORS |
| SameSite=Lax 阻止跨站 Cookie | Chrome 80+ 默认值变更 | 跨站场景设置 SameSite=None; Secure |
| CORS Header 被多次 add_header 覆盖 | Nginx 的 add_header 在 if 块中覆盖外层 | 使用 always 参数,避免嵌套 if |
| 预检请求耗时过长 | 每次都发 OPTIONS | 设置 Access-Control-Max-Age 缓存预检结果 |
| 本地文件跨域 | file:// 协议的 Origin 为 null | 使用本地 HTTP 服务器开发,不用 file:// 直接打开 |
最佳实践
- 生产用 Nginx 同源代理,开发用 Vite proxy
- CORS 指定具体 Origin,不用
* - 预检缓存
Max-Age减少 OPTIONS 请求 - 需要 Cookie 时
credentials: 'include'+ 后端Allow-Credentials postMessage始终校验origin,不用'*'- JSONP 仅用于旧系统兼容,新项目用 CORS
- Nginx 代理场景添加
X-Forwarded-For/X-Real-IP保留客户端信息 - 文件上传不要手动设
Content-Type
面试题
Q1: 什么是同源策略?为什么需要它?
同源策略要求协议、域名、端口完全一致才同源。它限制跨源 JS 读取 DOM、Cookie 和网络响应,防止恶意网站窃取用户在其他站点的数据,是浏览器最基础的安全机制。注意:跨域请求实际上已发出并到达服务器,只是浏览器拦截了响应。
Q2: CORS 的预检请求是什么?什么情况下会触发?
预检请求是浏览器在发送跨域”非简单请求”前自动发出的
OPTIONS请求,用于确认服务器是否允许实际请求。触发条件:使用了 PUT/DELETE/PATCH 等非简单方法、自定义 Header、Content-Type 非application/x-www-form-urlencoded/multipart/form-data/text/plain。可通过Access-Control-Max-Age缓存预检结果减少请求。
Q3: JSONP 的原理和局限性是什么?
JSONP 利用
<script>标签不受同源策略限制的特点,动态创建 script 标签请求带回调函数名的 URL,服务器返回callback(data)形式的 JS 执行。局限性:只支持 GET 请求、无法获取响应状态码、存在 XSS 安全风险、错误处理困难。现代项目应优先使用 CORS。
Q4: 为什么生产环境推荐 Nginx 反向代理解决跨域?
Nginx 代理让前端和 API 同源,浏览器感知不到跨域,无需 CORS 配置,避免了预检请求的额外开销。同时 Nginx 可统一管理负载均衡、限流、缓存等,前后端部署解耦。但需注意代理会增加一跳网络延迟。
Q5: Cookie 的 SameSite 属性对跨域有什么影响?
SameSite控制跨站请求是否携带 Cookie:Strict完全禁止跨站发送(包括从外部链接跳转);Lax(Chrome 80+ 默认)允许顶级导航 GET 请求携带,其余禁止;None允许跨站发送但必须配合Secure(仅 HTTPS)。跨域 AJAX 携带 Cookie 需SameSite=None; Secure,否则浏览器不会在跨站请求中附上 Cookie。
Q6: postMessage 如何安全地进行跨窗口通信?
发送方必须指定
targetOrigin(不要用'*'),接收方必须校验event.origin是否在白名单内,同时验证event.data的结构和类型。典型模式:父页面iframe.contentWindow.postMessage(data, 'https://child.com'),子页面window.addEventListener('message', (e) => { if (e.origin !== 'https://parent.com') return; ... })。还应注意校验消息格式,防止恶意数据注入。
Q7: 为什么 WebSocket 不受同源策略限制?
WebSocket 设计为全双工通信协议,其连接建立通过 HTTP Upgrade 握手完成,但握手完成后不再使用 HTTP 协议。同源策略是浏览器对 HTTP 请求/响应的安全限制,WebSocket 作为独立协议不在其约束范围内。不过服务端仍可通过
OriginHeader 校验来决定是否接受连接,浏览器也会在连接时携带 Origin 信息。
Q8: 如何区分简单请求和非简单请求?
简单请求必须同时满足:① 方法为 GET/POST/HEAD;② Content-Type 仅限
text/plain、multipart/form-data、application/x-www-form-urlencoded;③ 不包含自定义 Header;④ 没有使用ReadableStream。任一条件不满足即为非简单请求,浏览器会先发 OPTIONS 预检。实际开发中application/json+AuthorizationHeader 是最常见的触发预检组合。
相关链接: