15 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-05-18 14:30 |
企业项目跨域问题解决方案
概述
跨域(Cross-Origin Resource Sharing, CORS)是前后端分离架构中最常见的网络限制问题。本文从浏览器同源策略出发,系统讲解跨域产生的原理、常见场景及完整解决方案,涵盖服务端配置、Nginx 反向代理和客户端降级方案,并附带 Go / Spring Boot / Node.js / Nginx 的实际代码示例。
[!question] 思考:为什么浏览器要阻止跨域请求?
根源在于浏览器的「同源策略」(Same-Origin Policy)——它是一种安全机制,防止恶意网站读取你当前页面的 Cookie、LocalStorage 和 API 响应。但这一保护也带来了开发的困扰:我们需要在保持安全的前提下,让不同域的服务之间正常通信。
什么是跨域
同源的判定
两个 URL 只要 协议(protocol)、域名(host)、端口(port) 三者完全一致,就称为同源。任意一项不同,即为跨域。
| 当前 URL | 目标 URL | 是否跨域 | 原因 |
|---|---|---|---|
https://api.example.com |
https://app.example.com |
✅ 跨域 | 域名不同 |
http://localhost:3000 |
http://localhost:8080 |
✅ 跨域 | 端口不同 |
https://example.com |
http://example.com |
✅ 跨域 | 协议不同 |
https://example.com/api |
https://example.com/data |
❌ 同域 | 仅路径不同(不算跨域) |
跨域的本质
跨域不是请求发不出去,而是 浏览器拒绝接收响应。实际过程如下:
flowchart TD
A["前端发起 AJAX / Fetch 请求"] --> B{"是否为跨域请求?"}
B -- '否' --> C["正常发送, 浏览器接收响应"]
B -- '是' --> D{"是否预检请求?"}
D -- '简单请求' --> E["直接发送请求"]
D -- '复杂请求' --> F["先发 OPTIONS 预检"]
F --> G{"服务端返回 200 + 正确 CORS 头?"}
G -- '是' --> E
G -- '否' --> H["浏览器拦截预检, 不发送真实请求"]
E --> I{"服务端是否携带 CORS 响应头?"}
I -- '是' --> C
I -- '否' --> J["浏览器拒绝响应, 控制台报错"]
关键结论:服务器必须设置正确的 CORS 响应头,浏览器才会放行响应给 JavaScript。
常见跨域场景
开发环境(本地 vs 后端)
flowchart LR
DevBrowser["浏览器<br/>localhost:5173"] --> Backend["Go API :8080<br/>(请求 /api/users)"]
这是开发阶段最常见的跨域场景——前端 dev server 跑在 localhost:5173,后端服务在 :8080,端口不同即触发跨域。
生产环境(多子域名)
flowchart LR
App["app.company.com"] --> Api["api.company.com<br/>(需配 CORS)"]
主站和 API 属于不同子域名,同样需要处理跨域。
嵌入场景(iframe / CDN)
- 页面加载第三方 CDN 上的静态资源(字体、JS、CSS)→ 通常没问题(GET 请求天然不受限)
- 页面内 iframe 向自身域名发 XHR 请求 → 受跨域限制
- 调用第三方 API(如支付回调、地图 SDK)→ 取决于对方是否支持 CORS
解决方案
方案一:服务端配置 CORS(推荐首选)
适用场景:自有服务端,可直接修改响应头。
Go (Gin 框架)
使用官方 gin-contrib/cors 中间件:
import "github.com/gin-contrib/cors"
router := gin.Default()
config := cors.DefaultConfig()
config.AllowOrigins = []string{"https://app.example.com"} // 明确允许的源
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
config.AllowCredentials = true // 携带 Cookie 时必须设为 true
config.MaxAge = 12 * time.Hour // 预检请求缓存时长
router.Use(cors.New(config))
AllowOrigins 白名单是安全的第一道防线——它告诉浏览器"只有这些域名可以读取我的接口数据"。AllowCredentials 用于允许携带 Cookie(如登录态),但配合白名单使用,绝不可与通配符 * 同时出现。MaxAge 设置预检请求在浏览器的缓存时长,减少不必要的 OPTIONS 请求。
[!tip] 生产环境安全要点
- 不要用
AllowOrigins = ["*"]配合AllowCredentials = true,这会引发 panic 或无效配置。- 应显式列出允许的前端域名白名单,避免恶意站点劫持接口。
- 如需支持多个前端域名,可从环境变量或配置中心动态读取。
Java (Spring Boot)
Spring Boot 提供了多种 CORS 配置方式:
全局配置(实现 WebMvcConfigurer):
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 生效的路径
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600); // 预检缓存 1 小时
}
}
注解级别(细粒度控制单个 Controller):
@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
@RestController
@RequestMapping("/api/admin")
public class AdminController { ... }
Node.js (Express)
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
此配置传入 Express 的中间件链中,会对所有路由生效。如需限定路径范围,可改为 app.use('/api', cors(config)) 。与 Go / Java 方案相比,Node.js 方案最轻量——无需额外注解或配置类,只需在启动时注册一次即可。
方案二:Nginx 反向代理(零侵入)
适用场景:无法修改后端代码、多语言混合后端统一治理、隐藏后端地址提升安全。
核心思路:让前端访问 Nginx 的 /api 路径,Nginx 把请求转发到后端,同时注入 CORS 响应头。
server {
listen 443 ssl;
server_name app.example.com;
# --- 前端静态资源 ---
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# --- Location 1: OPTIONS 预检请求(直接返回,不通后端)---
location ~ ^/api/.*$ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin';
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';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
# --- Location 2: 真实请求(反向代理 + CORS 头)---
location /api/ {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
proxy_pass http://backend_server:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
[!warning] Nginx 注意事项
if ($request_method = 'OPTIONS')中的if在 Nginx 中有陷阱,但它在这里是安全的用法(只操作 header)。always参数确保即使错误响应(4xx / 5xx)也带上 CORS 头。return 204直接返回空体,避免 OPTIONS 请求打到后端增加负载。
Nginx 方案的架构图
flowchart TD
Browser["浏览器 app.example.com"] --> Nginx["Nginx 443<br/>(GET /api/users)"]
Nginx --> Backend["后端 :8080<br/>(proxy_pass)"]
Backend --> JSONResponse["JSON 响应"]
Nginx --> BrowserReply["返回含 CORS 头的<br/>JSON 响应"]
BrowserReply --> Browser
对比之下,不加 Nginx 时的直连模式:
flowchart TD
Browser["浏览器<br/>app.example.com<br/><b>❌ 跨域 + 无 CORS 头</b>"] --> Backend["后端 api.example.com:8080"]
此时浏览器直接访问后端接口,因域名和端口均不同触发跨域限制,且服务端未配置 CORS 头,浏览器拦截响应。
方案三:开发期 Vite/Webpack Proxy(开发专用)
适用场景:本地开发时绕过跨域,上线后由 Nginx 处理。
Vite 配置 (vite.config.ts)
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true, // 改写 Host 头
secure: false, // 自签证书时可关闭校验
},
},
},
});
这样前端发 fetch('/api/users') 时,Vite dev server 会转发到 http://localhost:8080/api/users,因为是同源请求,不存在跨域。
Webpack (vue.config.js / webpack.config.js)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};
changeOrigin: true 会将被代理请求的 Host 头改为后端地址,这对于某些依赖 Host 做鉴权的后端服务是必要的。Webpack proxy 同样只在本地开发生效,生产环境仍需 Nginx 或 CORS 中间件配合。
[!info] Vite/Webpack Proxy 与正式方案的区别
开发 Proxy Nginx / 服务端 CORS 用途 仅限本地开发 生产环境 需要后端改吗 不需要 不需要(Nginx)/ 需要(服务端) HTTPS 支持 需额外配置 原生支持 部署复杂度 无 低
方案四:JSONP / 服务端重定向(历史方案,了解即可)
JSONP
利用 <script> 标签不受同源策略限制的特性,通过回调函数接收数据。仅支持 GET,且存在 XSS 风险,已属历史方案。
原理:后端返回 callbackName(data) 这样的 JavaScript 代码,前端动态插入 <script> 标签触发执行。
<!-- 前端 -->
<script>
function jsonpCallback(data) {
console.log(data); // 拿到后端返回的数据
}
</script>
<script src="https://api.example.com/data?callback=jsonpCallback"></script>
// Go 后端示例
func JSONPHandler(w http.ResponseWriter, r *http.Request) {
callback := r.URL.Query().Get("callback")
data := `{"name": "John", "age": 30}`
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprintf(w, "%s(%s)", callback, data)
}
[!note] JSONP 的局限性
- 只支持 GET——POST/PUT/DELETE 等无法实现
- 无错误处理——脚本加载失败时只能通过
onerror粗略捕获- 安全风险——任何提供 JSONP 的接口都可能被恶意站点调用,需额外做 Referer 校验
- 已被 CORS 全面取代——现代浏览器全部支持 CORS,新项目不应考虑此方案
服务端重定向
后端自己调后端,前端只请求同源接口。适合微服务间调用,但不解决浏览器侧跨域。
进阶话题
带凭据的请求(Cookie / Authorization Header)
当 withCredentials: true(fetch)或 xhr.withCredentials = true(XMLHttpRequest)时:
- 服务端
Access-Control-Allow-Origin不能为*,必须是具体域名 - 服务端需设置
Access-Control-Allow-Credentials: true - 浏览器会在请求中带上 Cookie
// 前端 fetch 带 Cookie
fetch('https://api.example.com/user/profile', {
method: 'GET',
credentials: 'include', // 关键:带上 Cookie
headers: { 'Authorization': 'Bearer xxx' },
});
credentials: 'include' 是让浏览器发送 Cookie 的关键。如果只设 credentials 而不配服务端 Access-Control-Allow-Credentials: true,或者后端 Allow-Origin 仍为 *,请求同样会被拦截——这是一个"双方都满足才能通过"的条件。
预检请求的优化
复杂请求(自定义头、PUT/DELETE 等)会先发送 OPTIONS 预检。可通过以下方式减少开销:
Max-Age 缓存的影响
flowchart LR
A["Max-Age = 0<br/><b>每次请求都发 OPTIONS</b>"] --> B["⚠️ 高延迟,<br/>服务器压力大"]
C["Max-Age = 86400<br/><b>24小时内复用预检结果</b>"] --> D["✅ 首慢后续快,<br/>负载低"]
设置方式
服务端或 Nginx 中设置响应头即可:
Access-Control-Max-Age: 86400— 缓存 24 小时,超过需重新预检- 一般建议设为 1 天到 7 天,具体取决于接口变更频率
RESTful 动词与预检的关系
| 方法 | 是否可能预检 | 原因 |
|---|---|---|
| GET | ❌ 不会 | 简单请求 |
| POST (application/x-www-form-urlencoded / multipart/form-data / text/plain) | ❌ 可能不会 | Content-Type 在白名单内 |
| POST (application/json) | ✅ 会预检 | Content-Type 不在简单请求白名单 |
| PUT / DELETE / PATCH | ✅ 会预检 | 不在简单请求方法的白名单 |
常见问题排查清单
[!check] 跨域问题排查流程
- 确认跨域类型 — 打开浏览器 DevTools → Network,看请求状态码是
200还是cors error- 检查请求头 — Request Headers 里是否有
Origin,值是否正确- 检查响应头 — Response Headers 中是否有
Access-Control-Allow-Origin,值和前端域名匹配吗- 区分简单请求和预检 — 有无单独的
OPTIONS请求发出?OPTIONS 返回什么?- credentials 场景 — 是否同时满足了
具体 Allow-Origin+Allow-Credentials: true- 多级代理链路 — 经过网关 / WAF / CDN 时,确认每一层都不删除 CORS 头
方案选型速查表
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 前后端同公司,后端可改 | 服务端 CORS 中间件 | 最直接,语义清晰 |
| 多语言后端 / 不便改代码 | Nginx 反代注入 CORS 头 | 统一治理,零侵入 |
| 本地开发 | Vite/Webpack Proxy | 开发体验最佳 |
| 生产环境隐藏后端 IP | Nginx 反代 | 安全 + 跨域双收益 |
| 纯静态页调用第三方公开 API | 让第三方配好 CORS | 自身无能为力 |
关联笔记
- hhs/GIN/3-middleware/cors-registration-scope/cors-preflight — Gin 框架下预检请求的详细机制
- hhs/DEV/Nginx/Nginx — Nginx 配置速查,含反向代理、负载均衡等
- hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击 — 同源策略是防御 XSS/CSRF 的基础
- 03-服务端鉴权认证方案 — Cookie / Session 鉴权与 CORS 凭据的配合
- hhs/MS/02-服务治理/01-API网关 — 微服务架构中通过 API 网关统一处理跨域