欢迎光临

一文搞懂Web跨域请求(CORS)的原理与完整解决方案

Web网络安全

在前后端分离的Web开发中,跨域请求是最常遇到的问题之一。当你在前端调用不同域名的API时,浏览器会抛出经典的CORS错误。很多开发者只知道加上Access-Control-Allow-Origin头就能解决,但对背后的原理一知半解。本文将从同源策略讲起,深入剖析CORS的工作机制,并给出在各种后端框架中的完整解决方案。

什么是同源策略

同源策略(Same-Origin Policy)是浏览器最核心的安全机制之一。当两个URL的协议(protocol)、域名(host)和端口(port)完全相同时,才被认为是同源的。只要有一个不同,就是跨域。


1
2
3
4
5
6
7
// 同源判断示例<br />
const current = "http://example.com:80/path";

"http://example.com:80/other"      // ✅ 同源<br />
"https://example.com:80/path"       // ❌ 协议不同<br />
"http://api.example.com:80/path"    // ❌ 域名不同<br />
"http://example.com:8080/path"      // ❌ 端口不同

同源策略限制了以下行为:无法读取非同源页面的Cookie、LocalStorage、DOM,也无法通过XHR/Fetch向非同源地址发送请求(实际上请求发出去了,但浏览器拦截了响应)。

CORS请求的两种类型

CORS(Cross-Origin Resource Sharing)是W3C标准,允许服务器声明哪些源可以访问其资源。根据请求的复杂程度,分为简单请求和预检请求两种。

简单请求需要同时满足以下条件:方法为GET、HEAD或POST之一;Content-Type为text/plain、multipart/form-data或application/x-www-form-urlencoded;不包含自定义请求头。简单请求会直接发送,浏览器根据响应头决定是否放行。


1
2
3
4
5
6
7
8
// 简单请求示例 - 直接发送<br />
fetch("https://api.example.com/data", {<br />
  method: "GET",<br />
  headers: {<br />
    "Accept": "application/json"<br />
  }<br />
}).then(res =&gt; res.json())<br />
  .then(data =&gt; console.log(data));

预检请求(Preflight):不满足简单请求条件时,浏览器会先发送一个OPTIONS方法的预检请求,询问服务器是否允许实际请求。只有预检通过后,才会发送真正的请求。


1
2
3
4
5
6
7
8
9
10
11
12
13
// 预检请求 - 浏览器自动先发OPTIONS<br />
fetch("https://api.example.com/data", {<br />
  method: "PUT",<br />
  headers: {<br />
    "Content-Type": "application/json",<br />
    "Authorization": "Bearer token123",<br />
    "X-Custom-Header": "value"<br />
  },<br />
  body: JSON.stringify({ key: "value" })<br />
});<br />
// 浏览器先发 OPTIONS /data 请求<br />
// 服务器返回 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers<br />
// 通过后才发真正的 PUT 请求

完整的CORS响应头解析

服务器需要返回一系列CORS相关的响应头来控制跨域访问权限:


1
2
3
4
5
6
Access-Control-Allow-Origin: https://example.com   # 允许的源(<em> 表示所有)<br />
Access-Control-Allow-Methods: GET, POST, PUT, DELETE  # 允许的方法<br />
Access-Control-Allow-Headers: Content-Type, Authorization  # 允许的自定义头<br />
Access-Control-Allow-Credentials: true  # 是否允许携带Cookie<br />
Access-Control-Max-Age: 86400  # 预检结果缓存时间(秒)<br />
Access-Control-Expose-Headers: X-Custom-Header  # 前端可读取的响应头

特别注意:当Access-Control-Allow-Origin设为通配符时,Access-Control-Allow-Credentials不能同时设为true。两者互斥,必须指定具体的源。

代码开发

主流后端框架的CORS配置

Node.js + Express:


1
2
3
4
5
6
7
8
9
10
11
12
13
const cors = require("cors");

// 开发环境:允许所有源<br />
app.use(cors());

// 生产环境:精确配置<br />
app.use(cors({<br />
  origin: ["https://app.example.com", "https://admin.example.com"],<br />
  methods: ["GET", "POST", "PUT", "DELETE"],<br />
  allowedHeaders: ["Content-Type", "Authorization"],<br />
  credentials: true,<br />
  maxAge: 86400<br />
}));

Python Flask:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask<br />
from flask_cors import CORS

app = Flask(<strong>name</strong>)
<h1 id="_1">全局配置</h1>
CORS(app, resources={<br />
    r"/api/*": {<br />
        "origins": ["https://app.example.com"],<br />
        "methods": ["GET", "POST", "PUT", "DELETE"],<br />
        "allow_headers": ["Content-Type", "Authorization"],<br />
        "supports_credentials": True,<br />
        "max_age": 86400<br />
    }<br />
})

Nginx反向代理配置:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {<br />
    listen 443 ssl;<br />
    server_name api.example.com;
<div class="codehilite"><pre><span></span><code><span class="nt">location</span><span class="w"> </span><span class="o">/</span><span class="nt">api</span><span class="o">/</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="err">#</span><span class="w"> </span><span class="err">动态设置允许的源</span>
<span class="w">    </span><span class="err">set</span><span class="w"> </span><span class="err">$cors_origin</span><span class="w"> </span><span class="err">&quot;&quot;</span><span class="p">;</span>
<span class="w">    </span><span class="err">if</span><span class="w"> </span><span class="err">($http_origin</span><span class="w"> </span><span class="err">~*</span><span class="w"> </span><span class="err">&quot;^</span><span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="p">(</span><span class="n">app</span><span class="o">|</span><span class="n">admin</span><span class="p">)</span><span class="err">\</span><span class="o">.</span><span class="n">example</span><span class="err">\</span><span class="o">.</span><span class="n">com</span><span class="err">$</span><span class="s2">&quot;) {</span>
<span class="s2">        set $cors_origin $http_origin;</span>
<span class="s2">    }</span>

<span class="s2">    add_header Access-Control-Allow-Origin $cors_origin always;</span>
<span class="s2">    add_header Access-Control-Allow-Methods &quot;</span><span class="n">GET</span><span class="p">,</span><span class="w"> </span><span class="n">POST</span><span class="p">,</span><span class="w"> </span><span class="n">PUT</span><span class="p">,</span><span class="w"> </span><span class="n">DELETE</span><span class="p">,</span><span class="w"> </span><span class="n">OPTIONS</span><span class="s2">&quot; always;</span>
<span class="s2">    add_header Access-Control-Allow-Headers &quot;</span><span class="n">Content-Type</span><span class="p">,</span><span class="w"> </span><span class="n">Authorization</span><span class="s2">&quot; always;</span>
<span class="s2">    add_header Access-Control-Allow-Credentials &quot;</span><span class="n">true</span><span class="s2">&quot; always;</span>
<span class="s2">    add_header Access-Control-Max-Age 86400 always;</span>

<span class="s2">    # 处理预检请求</span>
<span class="s2">    if ($request_method = &quot;</span><span class="n">OPTIONS</span><span class="err">&quot;</span><span class="p">)</span><span class="w"> </span><span class="err">{</span>
<span class="w">        </span><span class="n">return</span><span class="w"> </span><span class="mi">204</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="nt">proxy_pass</span><span class="w"> </span><span class="nt">http</span><span class="o">://</span><span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">1</span><span class="p">:</span><span class="nd">8000</span><span class="o">/;</span>
<span class="err">}</span>

}

常见问题排查清单

遇到CORS问题时,按以下顺序排查:

1. 多个CORS头冲突:Nginx和应用框架同时设置了CORS头,导致出现重复的Access-Control-Allow-Origin。解决方案是在Nginx层面统一处理,去掉应用层的CORS中间件。

2. Cookie无法携带:前端需要设置credentials: ‘include’,后端必须指定具体的Origin且Allow-Credentials为true。


1
2
3
4
// 前端携带Cookie的正确方式<br />
fetch("https://api.example.com/data", {<br />
  credentials: "include"  // 不能用 * 的 Origin<br />
});

3. 自定义响应头前端读不到:需要在Access-Control-Expose-Headers中声明,否则前端虽然收到了响应,但JavaScript无法访问该头部。

4. 预检请求被404:确保服务器正确处理OPTIONS方法请求,很多开发者只配置了GET/POST路由,忽略了OPTIONS。

总结

CORS的核心要点:同源策略是浏览器安全基石,CORS是服务器主动放开限制的机制;区分简单请求和预检请求有助于理解请求流程;生产环境务必指定具体域名而非通配符*;Nginx反代时注意避免与应用层CORS头重复。掌握这些,跨域问题将不再是开发障碍。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 一文搞懂Web跨域请求(CORS)的原理与完整解决方案
分享到: 更多 (0)