在前后端分离与微服务架构盛行的今天,前端开发者经常在浏览器控制台遭遇令人头疼的红色报错:Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy。
跨域并非网络故障,而是浏览器为了防御恶意攻击而构筑的核心安全防线——同源策略(Same-Origin Policy, SOP)。
本文将深入拆解同源策略的底层安全逻辑、系统对比跨域判定维度,并实战解析 JSONP 历史机制、CORS 响应头规范与 OPTIONS 预检拦截,最后给出生产环境下最推荐的 Nginx 反向代理同域化最佳实践。
一、同源策略(SOP)与跨域问题本质
1. 什么是“同源”?
浏览器的同源策略规定:只有当两个页面或接口地址的 协议(Protocol)、域名(Domain/Host) 和 端口号(Port) 三者完全一致时,才被判定为“同源”。
只要上述三者中任意一个存在差异,浏览器即认定为跨域请求,并对资源的访问、DOM 的读取以及 HTTP 响应的接收进行强制隔离。
2. 同源判定矩阵对照表
假设当前页面地址为:http://www.example.com:80/index.html
| 目标请求地址 | 是否同源 | 判定原因说明 |
|---|---|---|
http://www.example.com/api/user |
同源 | 协议、域名、默认 80 端口均完全一致 |
http://www.example.com:8080/api/user |
跨域 | 端口不同(80 vs 8080) |
https://www.example.com/api/user |
跨域 | 协议不同(http vs https) |
http://api.example.com/user |
跨域 | 主/子域名不同(二级子域名不属于同源) |
http://example.com/api/user |
跨域 | 域名不同(带有 www 与不带属于不同域名) |
http://192.168.1.10/api/user |
跨域 | 即使该 IP 解析指向 example.com,域名与 IP 也被视为跨域 |
[!TIP] 带有
src属性的 HTML 标签(如<script>、<img>、<iframe>、<link>、<video>等)加载静态资源不受同源策略限制,这也是早期 CDN 资源加速与 JSONP 技术得以实现的基础。
二、为什么浏览器要限制跨域?CSRF 攻击防御
同源策略最核心的使命是防御跨站请求伪造(CSRF, Cross-Site Request Forgery)攻击与防止用户敏感数据泄露。
1. CSRF 攻击的典型链路
+----------------+ 1. 用户登录银行网站 +-------------------+
| 客户端浏览器 | ----------------------------> | 银行官网 A (受信任) |
| (存储银行 Cookie)| <---------------------------- | (颁发会话 Cookie) |
+----------------+ 2. 登录成功 +-------------------+
|
| 3. 用户在同一浏览器不小心打开钓鱼网站
v
+----------------+ 4. 钓鱼页面伪造转账请求 +-------------------+
| 恶意钓鱼网站 B | ----------------------------> | 银行官网 A |
| (恶意脚本触发) | (浏览器自动携带银行 Cookie!) | (若无同源防御将扣款) |
+----------------+ <---------------------------- +-------------------+
2. 浏览器的拦截行为机制
需要特别纠正的一个认知误区:跨域请求并不意味着请求发不出去! 通常情况下,客户端发起跨域 HTTP 请求时,浏览器确实将请求完整发送到了后端服务器,后端服务器也正常执行了业务逻辑并返回了响应;但是在浏览器端接收到响应时,检查发现服务端未声明 CORS 授权,于是直接拦截并将响应正文丢弃,向控制台抛出 CORS 错误。
三、主流跨域解决方案全景对比矩阵
在工业级工程中,解决跨域的核心方案主要有三种演进形态:
| 对比维度 | 方案一:JSONP | 方案二:CORS (跨源资源共享) | 方案三:Nginx 反向代理 (推荐) |
|---|---|---|---|
| 实现原理 | 利用 <script> 标签天然免跨域 |
服务端返回标准 CORS 响应头 | Nginx 网关代理统一域名同源转发 |
| 支持的 HTTP 方法 | 仅支持 GET | 支持 GET、POST、PUT、DELETE 等全量方法 | 支持全量方法及 WebSocket |
| 安全性 | 差 (容易遭受 XSS 脚本注入) | 强 (支持精确限制 Origin 与凭证) | 最高 (后端服务完全对公网隐蔽) |
| 前后端侵入性 | 前后端均需做特定包装改动 | 后端需添加响应头或配置拦截器 | 零侵入 (前后端均无需改动一行代码) |
| 生产适用度 | 已被现代工程全面淘汰 | 适合开放公共 API、第三方开放平台 | 大型微服务、中后台前后端分离首选 |
四、方案一:JSONP 历史机制与局限性
JSONP(JSON with Padding)是一种利用浏览器允许跨域加载 <script> 脚本特性的早期间接跨域技巧。
1. 客户端声明回调函数并动态创建 Script
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JSONP 实战示例</title>
</head>
<body>
<script>
// 1. 本地定义全局回调函数
function handleFlightResult(response) {
console.log("获取航班数据成功:", response);
alert("航班票价:" + response.price + " 元,剩余座位:" + response.tickets);
}
// 2. 动态创建 <script> 标签发起跨域请求
var script = document.createElement("script");
script.src = "http://api.flight.com/query?code=CA1234&callback=handleFlightResult";
document.head.appendChild(script);
</script>
</body>
</html>
2. 服务端返回包裹函数调用的 JavaScript 代码
服务端接收到 callback 参数后,不再返回标准的 JSON 字符串,而是返回一段执行 JavaScript 函数的脚本:
// 服务端输出响应:Content-Type: application/javascript
handleFlightResult({
"code": 200,
"flight": "CA1234",
"price": 1280,
"tickets": 8
});
[!WARNING] JSONP 的固有缺陷: 1. 无法支持 POST / PUT / DELETE 等修改操作; 2. 错误处理机制脆弱(难以捕获 404/500 等 HTTP 状态码); 3. 存在严峻的 XSS 攻击风险(若接口被篡改注入恶意 JS,页面将直接被脚本劫持)。现代 Web 强烈建议使用 CORS 或反向代理替代。
五、方案二:W3C CORS 跨源资源共享规范
CORS(Cross-Origin Resource Sharing)是 W3C 推荐的官方标准。它通过在 HTTP 响应头中注入特定的协商字段,由浏览器与服务器握手决定跨域权限。
1. 简单请求 vs 非简单请求(预检 OPTIONS)
浏览器将跨域请求划分为两类:
- 简单请求:满足方法为 GET / POST / HEAD,且请求头仅包含 Accept、Accept-Language、Content-Type(仅限 text/plain、multipart/form-data、application/x-www-form-urlencoded)。浏览器直接发出实际请求;
- 非简单请求:凡是使用了 PUT、DELETE、或者 Content-Type: application/json、或者携带了自定义 Header(如 Authorization、X-Token)的请求。浏览器必须在实际请求前先自动发出一次 OPTIONS 预检请求(Preflight Request),询问服务器是否许可。
2. 核心 CORS 响应头清单
| HTTP 响应头 | 字段作用说明 | 配置示例 |
|---|---|---|
Access-Control-Allow-Origin |
允许发起跨域访问的源地址 | http://frontend.example.com 或 * |
Access-Control-Allow-Methods |
允许跨域调用的 HTTP 方法列表 | GET, POST, PUT, DELETE, OPTIONS |
Access-Control-Allow-Headers |
允许客户端携带的自定义请求头 | Content-Type, Authorization, X-Requested-With |
Access-Control-Max-Age |
预检请求(OPTIONS)结果的本地缓存时长(秒) | 86400(24小时内同类请求无需再发 OPTIONS) |
Access-Control-Allow-Credentials |
是否允许携带用户凭证(Cookie/Authorization) | true |
[!WARNING] 关键避坑:Cookie 凭证与通配符互斥: 如果前端 Ajax 开启了
withCredentials: true,服务端在响应时: 1.Access-Control-Allow-Origin绝对不能写成通配符*,必须显式指定为请求的 Origin(如http://frontend.example.com); 2. 必须显式声明Access-Control-Allow-Credentials: true,否则浏览器同样会拦截响应并报错!
六、方案三:生产级 Nginx CORS 响应头与 OPTIONS 优化
如果系统无法改造为单域名同域架构,可在 Nginx 网关层统一拦截并注入 CORS 头,特别注意对 OPTIONS 预检请求进行 204 No Content 快速应答:
server {
listen 80;
server_name api.example.com;
location / {
# 1. 动态允许指定白名单源 (或指定特定前端域名)
add_header 'Access-Control-Allow-Origin' 'http://frontend.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Max-Age' 86400 always;
# 2. 对浏览器的 OPTIONS 预检请求直接返回 204,无需打到后端服务
if ($request_method = 'OPTIONS') {
return 204;
}
# 3. 正常业务转发至后端应用
proxy_pass http://127.0.0.1:8000;
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;
}
}
七、方案四:Nginx 反向代理同域化架构(工业级首选)
在企业级中大型项目与微服务集群中,消除跨域的最佳方案并不是去妥协跨域,而是利用 Nginx 反向代理让一切请求在浏览器看来都在“同一个源”下。
1. 架构示意图
+-------------------------+
| 客户端浏览器 |
| (访问 www.example.com) |
+-------------------------+
|
单一域名接入 (同一源)
|
v
+-------------------------+
| Nginx 统一接入网关 |
| (www.example.com:80) |
+-------------------------+
/ \
访问 / (前端静态单页) 访问 /api/ (业务接口)
/ \
v v
+--------------------+ +--------------------+
| 前端静态资源目录 | | 后端业务微服务集群 |
| /var/www/dist | | 10.0.0.12:8000 |
+--------------------+ +--------------------+
2. 标准 Nginx 同域化配置
server {
listen 80;
server_name www.example.com;
# 1. 前端 Vue / React / HTML 静态单页应用
location / {
root /var/www/frontend_dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 2. 所有 /api/ 开头的请求内部代理至后端,前端无感且完全同域
location /api/ {
proxy_pass http://10.0.0.12:8000/;
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;
# 代理缓冲与超时
proxy_connect_timeout 30s;
proxy_read_timeout 60s;
}
}
为什么首选反向代理? 1. 彻底根除跨域与 OPTIONS 预检:所有请求均为同源,网络请求开销直接减半; 2. Cookie 安全传递:无需开启复杂的跨域 Cookie 权限,原生支持 HttpOnly Cookie; 3. 安全隐蔽:后端微服务甚至无需暴露公网 IP,全量流量由 Nginx 网关内网隔离。
八、跨域选型总结与安全建议
- 同域优先原则:生产系统首选 Nginx 反向代理统一网关,实现架构层面的同域化解耦;
- 开放接口选用 CORS:对外开放 API 使用 CORS,切忌无脑配置
Access-Control-Allow-Origin: *,应建立严格的白名单鉴权; - 淘汰老旧技术:全面弃用 JSONP,避免潜在 XSS 注入风险。