在现代 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)。真正的工程素养,是在便利与安全之间找到平衡点。