--- tags: [CORS, 跨域, 安全, HTTP, Web开发, 前后端分离] 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` | ❌ 同域 | 仅路径不同(不算跨域) | ### 跨域的本质 跨域不是请求发不出去,而是 **浏览器拒绝接收响应**。实际过程如下: ```mermaid 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 后端) ```mermaid flowchart LR DevBrowser["浏览器
localhost:5173"] --> Backend["Go API :8080
(请求 /api/users)"] ``` 这是开发阶段最常见的跨域场景——前端 dev server 跑在 `localhost:5173`,后端服务在 `:8080`,端口不同即触发跨域。 ### 生产环境(多子域名) ```mermaid flowchart LR App["app.company.com"] --> Api["api.company.com
(需配 CORS)"] ``` 主站和 API 属于不同子域名,同样需要处理跨域。 ### 嵌入场景(iframe / CDN) - 页面加载第三方 CDN 上的静态资源(字体、JS、CSS)→ 通常没问题(GET 请求天然不受限) - 页面内 iframe 向自身域名发 XHR 请求 → 受跨域限制 - 调用第三方 API(如支付回调、地图 SDK)→ 取决于对方是否支持 CORS ## 解决方案 ### 方案一:服务端配置 CORS(推荐首选) **适用场景**:自有服务端,可直接修改响应头。 #### Go (Gin 框架) 使用官方 `gin-contrib/cors` 中间件: ```go 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):** ```java @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):** ```java @CrossOrigin(origins = "https://app.example.com", maxAge = 3600) @RestController @RequestMapping("/api/admin") public class AdminController { ... } ``` #### Node.js (Express) ```js 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 响应头。 ```nginx 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 方案的架构图 ```mermaid flowchart TD Browser["浏览器 app.example.com"] --> Nginx["Nginx 443
(GET /api/users)"] Nginx --> Backend["后端 :8080
(proxy_pass)"] Backend --> JSONResponse["JSON 响应"] Nginx --> BrowserReply["返回含 CORS 头的
JSON 响应"] BrowserReply --> Browser ``` 对比之下,不加 Nginx 时的直连模式: ```mermaid flowchart TD Browser["浏览器
app.example.com
❌ 跨域 + 无 CORS 头"] --> Backend["后端 api.example.com:8080"] ``` 此时浏览器直接访问后端接口,因域名和端口均不同触发跨域限制,且服务端未配置 CORS 头,浏览器拦截响应。 ### 方案三:开发期 Vite/Webpack Proxy(开发专用) **适用场景**:本地开发时绕过跨域,上线后由 Nginx 处理。 #### Vite 配置 (`vite.config.ts`) ```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`) ```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 利用 ` ``` ```go // 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)时: 1. 服务端 `Access-Control-Allow-Origin` **不能**为 `*`,必须是具体域名 2. 服务端需设置 `Access-Control-Allow-Credentials: true` 3. 浏览器会在请求中带上 Cookie ```ts // 前端 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 缓存的影响 ```mermaid flowchart LR A["Max-Age = 0
每次请求都发 OPTIONS"] --> B["⚠️ 高延迟,
服务器压力大"] C["Max-Age = 86400
24小时内复用预检结果"] --> D["✅ 首慢后续快,
负载低"] ``` #### 设置方式 服务端或 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] 跨域问题排查流程 > > 1. **确认跨域类型** — 打开浏览器 DevTools → Network,看请求状态码是 `200` 还是 `cors error` > 2. **检查请求头** — Request Headers 里是否有 `Origin`,值是否正确 > 3. **检查响应头** — Response Headers 中是否有 `Access-Control-Allow-Origin`,值和前端域名匹配吗 > 4. **区分简单请求和预检** — 有无单独的 `OPTIONS` 请求发出?OPTIONS 返回什么? > 5. **credentials 场景** — 是否同时满足了 `具体 Allow-Origin` + `Allow-Credentials: true` > 6. **多级代理链路** — 经过网关 / 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 网关统一处理跨域