
在前后端分离的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 => res.json())<br />
.then(data => 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">""</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">"^</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">") {</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 "</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">" always;</span>
<span class="s2"> add_header Access-Control-Allow-Headers "</span><span class="n">Content-Type</span><span class="p">,</span><span class="w"> </span><span class="n">Authorization</span><span class="s2">" always;</span>
<span class="s2"> add_header Access-Control-Allow-Credentials "</span><span class="n">true</span><span class="s2">" always;</span>
<span class="s2"> add_header Access-Control-Max-Age 86400 always;</span>
<span class="s2"> # 处理预检请求</span>
<span class="s2"> if ($request_method = "</span><span class="n">OPTIONS</span><span class="err">"</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>
汤不热吧