【Nginx】同源策略深度解析与 CORS / 反向代理跨域实战指南

admin / Nginx / 已更新 2026-09-22 / 3914 字 · 约 21 分钟 / 516 次阅读 /

在前后端分离与微服务架构盛行的今天,前端开发者经常在浏览器控制台遭遇令人头疼的红色报错: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,且请求头仅包含 AcceptAccept-LanguageContent-Type(仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded)。浏览器直接发出实际请求; - 非简单请求:凡是使用了 PUTDELETE、或者 Content-Type: application/json、或者携带了自定义 Header(如 AuthorizationX-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 网关内网隔离。


八、跨域选型总结与安全建议

  1. 同域优先原则:生产系统首选 Nginx 反向代理统一网关,实现架构层面的同域化解耦;
  2. 开放接口选用 CORS:对外开放 API 使用 CORS,切忌无脑配置 Access-Control-Allow-Origin: *,应建立严格的白名单鉴权;
  3. 淘汰老旧技术:全面弃用 JSONP,避免潜在 XSS 注入风险。
admin

admin

云原生架构 · 全栈开发

感谢阅读本文!记录系统架构思考与实战复盘。专注于 Python、Django、PostgreSQL 与云原生容器化工程实践。