在现代 Web 开发中,"前后端分离"已成为主流架构模式:前端(React/Vue 等)通过 HTTP API 与后端(Node.js/Java/Go 等)交互。然而,这种解耦也带来了高频问题------跨域(CORS)错误 。开发者常被浏览器控制台中的 Blocked by CORS policy 困扰。本文将系统梳理跨域的根本原因、常见场景 ,并给出生产环境推荐的最优解决方案。
一、什么是"跨域"?为什么浏览器要限制它?
1. 同源策略(Same-Origin Policy)
浏览器出于安全考虑,限制一个源(Origin)的脚本如何与另一个源的资源进行交互。
同源 = 协议 + 域名 + 端口 完全一致
例如:
https://app.example.com:443与http://app.example.com(协议不同)、https://api.example.com(子域名不同)、https://app.example.com:8080(端口不同)均属于跨域。
2. 跨域 ≠ 请求失败
- 请求其实发出去了 !但浏览器在收到响应后,若发现不符合 CORS 规则,会拦截响应数据,不让前端 JavaScript 读取。
- 这是浏览器的安全机制,服务器本身不受影响(可用 Postman 直接调通)。
二、前后端分离项目中跨域的常见原因
| 场景 | 示例 | 说明 |
|---|---|---|
| 开发环境本地调试 | 前端 localhost:3000 → 后端 localhost:8080 |
端口不同,触发跨域 |
| 测试/预发环境分离部署 | 前端 test-fe.example.com → 后端 test-api.example.com |
子域名不同 |
| API 网关未正确透传头 | 请求经 Nginx 转发,但未处理 Origin 和 CORS 头 |
导致后端无法识别跨域请求 |
| 前端携带凭证(credentials)但后端未允许 | fetch 设置 credentials: 'include',但后端未设 Access-Control-Allow-Credentials: true |
浏览器拒绝响应 |
| 自定义请求头未被允许 | 前端发送 X-Token: abc,但后端未在 Access-Control-Allow-Headers 中声明 |
触发预检(Preflight)失败 |
⚠️ 注意:简单请求(如 GET/POST + JSON)可能不触发预检,但带自定义头、非标准 Content-Type(如 application/json)等会触发 OPTIONS 预检请求。
三、跨域解决方案全景图
❌ 不推荐方案(仅限开发)
| 方案 | 问题 |
|---|---|
禁用浏览器安全策略 (如 Chrome 启动加 --disable-web-security) |
仅本地测试可用,无法解决生产问题,且危险 |
| JSONP | 仅支持 GET,无错误处理,安全性差,已淘汰 |
| 代理插件(如 CORS Unblock) | 用户不可控,仅调试用 |
四、最优解决方案:服务端正确配置 CORS
核心原则:跨域问题应在服务端解决,而非绕过。
1. 后端设置 CORS 响应头(以 Node.js/Express 为例)
php
// 使用 cors 中间件(推荐)
const cors = require('cors');
const corsOptions = {
origin: ['https://app.example.com', 'https://admin.example.com'], // 明确指定可信源
credentials: true, // 允许携带 Cookie/认证头
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
✅ 关键点:
- 不要设
origin: '*'!若需携带凭证(如 Cookie),*会导致失败。- 生产环境必须白名单化可信前端域名。
2. 手动设置 CORS 头(通用)
yaml
# 预检请求(OPTIONS)响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Token
Access-Control-Max-Age: 86400 # 预检结果缓存 24 小时
# 实际请求响应
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
3. Nginx 层统一处理 CORS(适合多后端服务)
bash
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Token';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain; charset=utf-8';
return 204;
}
# 代理到后端
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 实际响应也需加 CORS 头
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
}
💡 优势:集中管理,避免每个微服务重复配置。
五、前端配合最佳实践
-
明确请求是否需要凭证:
php// 如需发送 Cookie fetch('/api/user', { credentials: 'include' // 对应后端 Access-Control-Allow-Credentials: true }); -
避免随意添加自定义头 :如非必要,不要加
X-xxx头,减少预检开销。 -
使用相对路径或环境变量管理 API 地址:
ini# .env.development VITE_API_BASE = "http://localhost:8080" # .env.production VITE_API_BASE = "/api" # 配合 Nginx 反向代理
六、终极推荐架构:开发用代理,生产用同源
开发阶段:前端 Dev Server 代理(无跨域)
arduino
// vite.config.js (Vite)
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false
}
}
}
}
→ 前端请求 /api/user 实际转发到 http://localhost:8080/api/user,浏览器认为是同源。
生产阶段:Nginx 反向代理(同源部署)
bash
server {
listen 443 ssl;
server_name app.example.com;
# 前端静态资源
location / {
root /dist;
try_files $uri $uri/ /index.html;
}
# API 代理到后端(同域名,无跨域)
location /api/ {
proxy_pass http://backend-cluster;
}
}
→ 用户访问 https://app.example.com/api/user,前端与 API 同源,彻底规避 CORS。
✅ 这是大型项目最推荐的方式:开发靠代理,生产靠反向代理,全程无跨域烦恼。
结语
跨域不是"bug",而是浏览器安全模型的正常行为。理解其原理,才能从根本上解决。最优路径 = 开发阶段用前端代理 + 生产环境用 Nginx 反向代理 + 后端兜底 CORS 白名单。
记住:永远不要为了"快速解决"而牺牲安全性(如 origin: * + credentials)。真正的工程素养,是在便利与安全之间找到平衡点。