聊聊跨域问题

跨域到底该谁管?浏览器、代理与 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 之前会问:

  1. 页面来源(Origin)和请求 URL 是否同源?
  2. 不同源的话,响应头里有没有「允许这个来源访问」的声明?

同源要求协议、域名、端口三者完全一致:

页面 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」是正常现象。

允许跨域时,服务端需在响应里带上类似头:

http 复制代码
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 是相对路径还是写死的绝对地址无关。

ts 复制代码
// 页面在 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')

环境变量也一样:

ts 复制代码
// .env: VITE_API_BASE=http://localhost:8080
fetch(`${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 Spring CorsWebFilter 需要(或等价 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:

ts 复制代码
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true,
        changeOrigin: true,
      },
    },
  },
})
ts 复制代码
// 前端统一相对路径
await fetch('/api/v1/auth/health')

生产 --- Nginx:

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/ 也要单独配(Vite ws: 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 团队约定(建议)

  1. 前端请求使用相对路径 /api/.../ws/...,环境变量里不要写死后端 host:port(除非明确走跨域 + CORS 方案)。
  2. 本地:Vite proxy → Gateway 8080
  3. 生产:Nginx 同域名反代 → Gateway。
  4. Gateway CorsConfig:按需启用(API 独立域名或多前端源时再开)。
  5. CORS 只在一层配置,避免 Gateway 与 Nginx 重复。

7. 排查清单

浏览器报 CORS 时,依次确认:

  1. 当前页面 Origin 是什么?(地址栏 + 端口)
  2. 请求完整 URL 是什么?相对路径还是写死的绝对地址?
  3. 中间有没有 Vite / Nginx 代理?代理规则是否覆盖该路径?
  4. 若是跨域直连,Gateway 或 Nginx 是否返回了 CORS 头?(看 Response Headers)
  5. 是否只有浏览器失败、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