跨域到底该谁管?浏览器、代理与 Gateway CORS
适用:WeekFlow 前后端分离开发(Vite + Gateway + Nginx)
读者:前后端开发、运维
1. 从一个报错说起
前端跑在http://localhost:5173,Gateway 在http://localhost:8080,控制台出现:
Access to fetch at 'http://localhost:8080/api/v1/auth/health' from origin 'http://localhost:5173' has been blocked by CORS policy后端说:Gateway 加个CorsConfig。
前端说:Vite 配个 proxy 不就行了?
两个人都没错,说的不是同一件事。把机制理顺,本地能跑、上线不踩坑、排查能快。
2. 跨域是浏览器的规矩
CORS(Cross-Origin Resource Sharing)不是 Spring、不是 Nginx 发明的业务规则,是浏览器对页面里 JavaScript 的安全限制。
浏览器在把响应交给 JS 之前会问:
- 页面来源(Origin)和请求 URL 是否同源?
- 不同源的话,响应头里有没有「允许这个来源访问」的声明?
同源要求协议、域名、端口三者完全一致:
| 页面 Origin | 请求 URL | 是否跨域 |
|---|---|---|
http://localhost:5173 | http://localhost:8080/api/... | 是(端口不同) |
http://localhost:5173 | http://127.0.0.1:8080/api/... | 是(域名不同) |
https://weekflow.com | https://weekflow.com/api/... | 否(同源) |
https://app.weekflow.com | https://api.weekflow.com/... | 是(子域不同) |
Postman、curl、Feign、Gateway 转发到下游——都不走这套。所以「Postman 能通、浏览器报 CORS」是正常现象。
允许跨域时,服务端需在响应里带上类似头:
Access-Control-Allow-Origin: http://localhost:5173 Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Allow-Credentials: true没有这些头,浏览器会拦截,JS 读不到 body。Network 里可能已是 200,但fetch仍会进catch。
结论:限制来自浏览器;「允不允许跨」必须由某层 HTTP 服务通过响应头声明。
3. 前端写死后端地址,一定跨域吗?必须要 CorsConfig 吗?
3.1 写死地址 ≠ 一定跨域
跨域看的是页面 Origin 和请求 URL 是否同源,跟 URL 是相对路径还是写死的绝对地址无关。
// 页面在 http://localhost:5173// 情况 A:写死 8080 → 跨域(5173 ≠ 8080)fetch('http://localhost:8080/api/v1/auth/health')// 情况 B:相对路径 + Vite 代理 → 浏览器认为请求发往 5173,同源fetch('/api/v1/auth/health')// 情况 C:写死地址,但页面本身就在 8080 上(不常见)// 例如静态资源也由 Gateway 或 Nginx 同端口提供 → 不跨域fetch('http://localhost:8080/api/v1/auth/health')环境变量也一样:
// .env: VITE_API_BASE=http://localhost:8080fetch(`${import.meta.env.VITE_API_BASE}/api/v1/auth/health`)只要浏览器当前页面的 Origin 是5173,请求目标是8080,就是跨域。
写死只是更容易把 host:port 写错成另一个源;用相对路径/api/...则天然跟页面同源。
3.2 跨域了,是否必须要 CorsConfig?
不必须。CorsConfig只是「让 Gateway 在响应里加 CORS 头」这一种做法。
跨域时浏览器要能正常读响应,至少需要下面之一:
| 方案 | 做法 | 是否需要 Gateway CorsConfig |
|---|---|---|
| 同源代理(推荐) | Vite / Nginx 反代,前端用相对路径 | 不需要 |
| Gateway CORS | SpringCorsWebFilter | 需要(或等价 Java/YAML 配置) |
| Nginx 层 CORS | add_header Access-Control-* | 不需要 Gateway 配,Nginx 配 |
| 不处理 | 直连跨域 URL,无任何 CORS 头 | 浏览器拦截 JS 读响应 |
WeekFlow 若采用开发 Vite 代理 + 生产 Nginx 同源反代,前后端约定用/api/...,则Gateway 的CorsConfig可以没有,不影响正常用户访问。
CorsConfig适合这些场景再开:
- API 独立域名(如
api.weekflow.com,页面在app.weekflow.com) - 多个前端域名共用一个 API
- 本地有人习惯直连
http://localhost:8080调试,且前端仍从 5173 发起请求 - 第三方浏览器页面调用开放 API
CORS 头只需在一层加(Gateway 或 Nginx 二选一),多层重复可能出 duplicate header,浏览器照样报错。
3.3 跨域但没 CORS 时,后台有没有收到请求?
有可能收到了。CORS 拦的是浏览器是否允许 JS 读响应,不是网络层能不能连上。
典型现象:Network 显示 200,控制台报 CORS,fetch拿不到数据。
所以「接口在 Postman 里是好的」不能证明浏览器场景没问题。
4. 两条解决思路
思路一:同源代理(WeekFlow 推荐)
不让浏览器跨域,把转发放在服务器侧。
本地 — Vite:
// vite.config.tsexportdefaultdefineConfig({server:{proxy:{'/api':{target:'http://localhost:8080',changeOrigin:true,},'/ws':{target:'ws://localhost:8080',ws:true,changeOrigin:true,},},},})// 前端统一相对路径awaitfetch('/api/v1/auth/health')生产 — Nginx:
server { listen 443 ssl http2; server_name weekflow.com; location / { root /var/www/weekflow; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://weekflow-gateway:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /ws/ { proxy_pass http://weekflow-gateway:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } }页面与 API 在浏览器里都是https://weekflow.com,不触发 CORS。
思路二:显式 CORS
浏览器直连 API 域名,由 Gateway(或 Nginx)返回Access-Control-Allow-*。
WeekFlow Gateway 当前实现:weekflow-gateway模块com.weekflow.gateway.config.CorsConfig,允许http://localhost:5173。
若团队确定永远走同源代理,该类可删除或注释,并在团队规范里写清楚。
5. 容易踩坑的点
OPTIONS 预检
带Authorization、非简单 Content-Type 时,浏览器会先发 OPTIONS。直连跨域 API 时 Gateway 必须正确处理;走同源代理则通常无感。
WebSocket
REST 代理了,/ws/也要单独配(Vitews: true、Nginx Upgrade 头),否则表现为 HTTP 正常、长连接失败。
Credentials
Cookie 方案下Allow-Origin不能为*,须指定具体域名且Allow-Credentials: true。Bearer Token 一般简单些。
开发与生产策略不一致
开发用 proxy、预发却改成https://api.staging.xxx.com直连,联调阶段会突然冒出 CORS。团队应统一:各环境前端 baseURL 怎么配、是否允许写死后端 host。
微服务间调用
Gateway → auth-service、OpenFeign 等不涉及 CORS,仅影响浏览器里的 JS。
6. WeekFlow 团队约定(建议)
- 前端请求使用相对路径
/api/...、/ws/...,环境变量里不要写死后端host:port(除非明确走跨域 + CORS 方案)。 - 本地:Vite proxy → Gateway
8080。 - 生产:Nginx 同域名反代 → Gateway。
- Gateway
CorsConfig:按需启用(API 独立域名或多前端源时再开)。 - CORS 只在一层配置,避免 Gateway 与 Nginx 重复。
7. 排查清单
浏览器报 CORS 时,依次确认:
- 当前页面 Origin 是什么?(地址栏 + 端口)
- 请求完整 URL 是什么?相对路径还是写死的绝对地址?
- 中间有没有 Vite / Nginx 代理?代理规则是否覆盖该路径?
- 若是跨域直连,Gateway 或 Nginx 是否返回了 CORS 头?(看 Response Headers)
- 是否只有浏览器失败、Postman 正常?(是则基本可断定 CORS 问题)
8. 相关代码与文档
| 项 | 位置 |
|---|---|
| Gateway CORS(可选) | weekflow-gateway/.../config/CorsConfig.java |
| Gateway 路由 | weekflow-gateway/src/main/resources/application.yml |
| 开发计划 Step 1.3.4 | docs/architecture/03-开发计划.md |
9. 一句话总结
- 写死后端地址:只有页面 Origin 与目标 URL 不同源时才跨域;写死
8080而页面在5173就会跨域。 - CorsConfig:不是必选项,是「跨域直连 API」时的一种解法;同源代理方案下可以不要。
- 团队真正要对齐的是:前端 URL 怎么写、代理在哪一层、什么场景才开 CORS。