前端调接口时报错 Access to XMLHttpRequest at 'https://api.example.com/xxx' from origin 'https://www.example.com' has been blocked by CORS policy,接口在 Postman 里明明是通的,浏览器却死活不让过——这就是跨域(CORS)问题。它最大的特点就是"后端没错、前端没错、浏览器在拦",而且原因往往藏在响应头和预检请求里,光看前端代码很难定位。
这篇文章把 CORS 讲清楚:先解释同源策略和"为什么会跨域",再讲清 CORS 的三个关键响应头(Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers),然后重点区分简单请求与预检请求(OPTIONS)——后者是绝大多数 CORS 配错的根源。接着分别给出 Nginx 层和后端框架(Spring、Express、Django)的配置方法,覆盖"带 Cookie"这种最容易踩坑的场景,最后给出一张常见报错信息的解读表和几条安全红线。
先记住一句核心:跨域是浏览器的安全策略,解药在服务端的响应头里。前端能做的很有限,真正的修复动作几乎都在后端或反向代理那一侧。
一、同源策略:跨域从哪来
同源策略(Same-Origin Policy)是浏览器的安全机制:一个页面的脚本,默认只能访问同源的资源。同源指三段完全一致:
协议 + 域名 + 端口 三者都相同才算同源
https://www.example.com:443/page
↑协议 ↑域名 ↑端口
只要有一个不同,就是跨域:
| 页面地址 | 请求地址 | 是否同源 | 原因 |
|---|---|---|---|
https://a.com |
https://a.com/api |
同源 | 完全一致 |
https://a.com |
https://b.com/api |
跨域 | 域名不同 |
https://a.com |
http://a.com/api |
跨域 | 协议不同 |
https://a.com |
https://a.com:8080/api |
跨域 | 端口不同 |
https://www.a.com |
https://api.a.com |
跨域 | 子域名也不同源 |
注意:跨域限制只在浏览器里生效。用 curl、Postman、服务端之间调用都没有这个限制,所以"后端测试通过、浏览器报错"是完全正常的。
二、CORS 的三个关键响应头
CORS(Cross-Origin Resource Sharing,跨域资源共享)是通过响应头来"授权"跨域的机制。核心是这几个头,由服务端返回:
| 响应头 | 作用 | 示例 |
|---|---|---|
Access-Control-Allow-Origin |
允许哪些来源访问 | https://www.example.com 或 * |
Access-Control-Allow-Methods |
允许哪些方法 | GET, POST, PUT, DELETE, OPTIONS |
Access-Control-Allow-Headers |
允许哪些请求头 | Content-Type, Authorization |
Access-Control-Allow-Credentials |
是否允许带 Cookie | true |
Access-Control-Max-Age |
预检结果缓存多久(秒) | 86400 |
Access-Control-Expose-Headers |
允许前端读取哪些响应头 | X-Total-Count |
其中 Access-Control-Allow-Origin 是必填的,没有它浏览器一定拒绝。它的值要么是具体的来源,要么是 *(允许所有来源)。
三、简单请求与预检请求:关键分水岭
CORS 请求分两类,这是理解一切 CORS 问题的关键。
简单请求:同时满足以下条件,浏览器直接发请求,响应里带上 Allow-Origin 即可。
- 方法是
GET、HEAD、POST之一; - 请求头只用了"安全头"(如
Accept、Accept-Language、Content-Language、Content-Type的少数值); Content-Type是application/x-www-form-urlencoded、multipart/form-data、text/plain之一。
预检请求(Preflight):不满足简单请求条件时,浏览器先发一个 OPTIONS 请求问服务器"我能不能这么调",服务器返回允许信息后,浏览器才发真正的请求。
浏览器 ──OPTIONS /api (预检)──> 服务器
<──允许信息(Allow-*)──
浏览器 ──真正的请求──> 服务器
<──响应──
触发预检的常见情况:
- 用了
PUT、DELETE、PATCH等方法; Content-Type: application/json(这是最常见的触发原因);- 带了自定义头(如
Authorization、X-Token)。
结论:绝大多数"AJAX 调 JSON 接口"都是预检请求,所以服务端必须能正确处理 OPTIONS 请求并返回正确的 CORS 头,否则浏览器第一步就被拦,真实请求根本发不出去。CORS 配错的根源,八成是 OPTIONS 没处理好。
四、Nginx 层配置 CORS
如果 API 由 Nginx 反代,可以在 Nginx 统一加 CORS 头。注意几个坑:add_header 只在特定状态码下生效(默认 2xx/3xx),OPTIONS 的 204 要单独处理;add_header 有继承规则(子层级有 add_header 会覆盖父级)。
server {
listen 80;
server_name api.example.com;
location /api/ {
# 允许的来源:正式环境建议写具体域名,不要用 *
add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Token' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '86400' always;
# 预检请求:直接返回 204,不打后端
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Token' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '86400' always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
要点:
always参数很重要:它让这些头在所有状态码(包括 4xx、5xx)下都返回,否则报错响应里没有 CORS 头,前端会看到一个"CORS 错误"而不是真正的错误码,掩盖了真实问题。- OPTIONS 用
return 204:预检请求不该打到后端,Nginx 直接回 204 最快。注意if在 location 里的使用限制,更规范的写法是把 CORS 头放在http或server层的add_header,但要注意继承覆盖问题。 - 改完校验重载:
nginx -t && nginx -s reload。
多个来源时可用 map 动态返回:
map $http_origin $cors_origin {
default "";
"https://www.example.com" $http_origin;
"https://m.example.com" $http_origin;
"~^https://.*\.example\.com$" $http_origin;
}
# 然后 add_header 'Access-Control-Allow-Origin' $cors_origin always;
五、后端框架层配置 CORS
很多团队更喜欢在后端统一处理 CORS,方式因框架而异。
Spring Boot(全局配置):
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Express(Node.js):
const cors = require('cors');
app.use(cors({
origin: 'https://www.example.com', // 不要用 '*' 配合 credentials
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true,
maxAge: 86400
}));
Django:
# settings.py
INSTALLED_APPS += ['corsheaders']
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', *MIDDLEWARE]
CORS_ALLOWED_ORIGINS = ["https://www.example.com"]
CORS_ALLOW_CREDENTIALS = True
注意:如果同时在 Nginx 和后端都加了 CORS 头,会出现重复的 Access-Control-Allow-Origin,浏览器同样会拒绝("header contains multiple values")。所以要二选一,或者明确让某一层不加。
六、带 Cookie 的跨域(credentials)
这是最容易翻车的场景。当前端设置 withCredentials = true(axios 的 withCredentials: true,fetch 的 credentials: 'include')要携带 Cookie 时:
- 服务端必须返回
Access-Control-Allow-Credentials: true; - 服务端
Access-Control-Allow-Origin必须是具体域名,绝不能是*,否则浏览器直接拒绝; - Cookie 本身也要满足
SameSite策略(现代浏览器默认SameSite=Lax,跨站需SameSite=None; Secure)。
// 前端
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include' // 携带 Cookie
});
# 服务端:注意不能用 *
add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
一句话:* 和 credentials: true 天生互斥,凡是要带 Cookie,来源就必须写死具体域名。
七、常见 CORS 报错信息解读
浏览器控制台的报错措辞不同,指向的问题也不同。
| 报错信息(节选) | 含义 | 修复方向 |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present |
响应完全没带 CORS 头 | 服务端加 Allow-Origin |
...header contains multiple values '*, *' |
重复设置了 CORS 头 | Nginx 和后端去掉一层 |
The value of the 'Access-Control-Allow-Origin' header ... must not be '*' |
带 Cookie 却用了 * |
改成具体域名 |
Method PUT is not allowed by Access-Control-Allow-Methods |
预检返回的方法不含 PUT | 补全 Allow-Methods |
Request header field authorization is not allowed |
自定义头未被允许 | 加到 Allow-Headers |
Response to preflight request doesn't pass |
OPTIONS 预检失败 | 处理 OPTIONS 请求 |
Redirect is not allowed for a preflight request |
OPTIONS 被 3xx 重定向 | 别对 OPTIONS 做跳转 |
排查 CORS 的固定动作:打开 DevTools Network,先看那条 OPTIONS 请求(如果有),看它的响应头里有没有正确的 Access-Control-Allow-*,再看真实请求的响应头。99% 的问题都能这样定位。
# 用 curl 模拟预检请求,绕过浏览器直接看响应头
curl -i -X OPTIONS https://api.example.com/api/user \
-H 'Origin: https://www.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type, authorization'
八、安全注意事项
- 不要长期使用
Access-Control-Allow-Origin: *:它意味着任何网站都能跨域读取你的接口数据,配合credentials更是被浏览器禁止。生产环境应白名单具体来源。 Allow-Credentials: true必须配对具体 Origin:这是浏览器强制的安全约束,不是可选项。Allow-Headers不要写成*又期望自定义头生效:带 credentials 时*在部分规范下不生效,建议显式列出。- 优先用同源或反向代理:把前端和 API 放在同一域名下(如前端
www.a.com、API 走www.a.com/api反代到后端),从根本上避免跨域,比配 CORS 更省心也更安全。
风险提示:修改 Nginx 或后端 CORS 相关配置前先备份;add_header 的继承规则容易造成"某个 location 的 CORS 头莫名消失",改完务必用 curl -i 从多个路径验证响应头是否如预期,并在 nginx -t 通过后再 reload。
常见问题
Q:接口在 Postman 里能通,浏览器就报 CORS,是谁的问题?
是浏览器在拦。Postman/curl 不受同源策略约束,浏览器受。修复必须在服务端响应头上做,前端改代码解决不了(除非改用代理)。
Q:为什么我的请求先发了一个 OPTIONS?能不发吗?
那是预检请求。当方法不是简单方法、Content-Type 是 application/json、或带了自定义头时,浏览器就会先预检。可以:改用简单请求条件(如 Content-Type 用表单类型)、减少自定义头,或服务端正确处理 OPTIONS 并设 Access-Control-Max-Age 缓存预检结果。
Q:Access-Control-Allow-Origin 设了 * 还是报错?
先确认响应里是不是重复出现了该头(Nginx 和后端都加了),重复会导致拒绝;再确认前端是否带 credentials,带 Cookie 时 * 一律无效,必须写具体域名。
Q:报错说 Redirect is not allowed for a preflight request,怎么办?
说明 OPTIONS 预检被 301/302 跳转了。常见于 HTTP 跳 HTTPS、带斜杠跳转。要让 OPTIONS 请求直接返回 204,或保证跳转后仍是 OPTIONS 且带 CORS 头。
Q:add_header 加了 CORS 头,但报错时前端看不到 CORS 头?
因为默认 add_header 只在 2xx/3xx 生效,4xx/5xx 响应不带这些头,于是前端报的是 CORS 错误而非真实错误码。给 add_header 加 always 参数即可让它在所有状态码下返回。
总结
跨域是浏览器的同源策略在起作用,根源在服务端响应头缺失或错误;判定标准是"协议+域名+端口"三者是否完全一致。
核心响应头是 Access-Control-Allow-Origin(必填),配合 Allow-Methods、Allow-Headers、Allow-Credentials;触发预检(OPTIONS)的典型情况是 JSON 请求、PUT/DELETE 方法和自定义头。
配 CORS 的最大坑是预检请求:服务端必须能正确处理 OPTIONS 并返回 CORS 头,否则真实请求根本发不出;配 Nginx 时给 add_header 加 always、用 return 204 处理预检。
带 Cookie 时 Allow-Origin 必须写具体域名、绝不能是 *;同一份 CORS 头不要在 Nginx 和后端重复设置,否则会因重复值被拒。
最省心的方案是把前端和 API 放同源(反向代理),从源头消除跨域;确需跨域则白名单来源、显式列头、用 curl 复验响应头。
企业QQ咨询




