CORS 跨域是网关的责任,不是业务服务的负担

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

这是一个非常经典、且在微服务架构中极易被误解的问题。

你的直觉是对的:不需要在每个业务服务(如 user-serviceorder-service)中重复配置跨域。


✅ 终极答案:

🚫 不需要在每个业务模块中配置跨域!

只需要在 API Gateway(网关)中统一配置跨域即可!

你已经在网关层做了跨域配置,这就是最正确、最推荐、最工业级的做法
在每个业务服务中再配一次,是重复劳动 + 架构反模式,会导致:

  • 配置不一致
  • 维护成本飙升
  • 调试困难
  • 安全风险增加

📌 一、为什么“只在网关配置跨域”是唯一正确做法?

🔍 微服务架构中的请求路径

[前端浏览器]
     ↓ (CORS 请求:GET /user/me)
[API Gateway] ←───────✅【在这里处理 CORS】
     ↓ (转发为内部 HTTP 请求,无 CORS)
[user-service]
     ↓ (返回 JSON)
[API Gateway] ←───────✅【网关将响应头原样返回给前端】
     ↓ (带 Access-Control-* 头)
[前端浏览器] ←───────✅【浏览器收到响应,CORS 检查通过】

✅ 关键原理:CORS 是浏览器行为,不是后端协议

  • CORS(跨域资源共享)是浏览器的安全机制,不是 HTTP 协议的一部分。
  • 浏览器在发起跨域请求前,会先发送一个 OPTIONS 预检请求。
  • 只有网关直接暴露给公网,所以:
    • 只有网关接收来自浏览器的原始请求(含 Origin)
    • 只有网关需要添加 Access-Control-Allow-Origin: https://shop.urbane.io
    • 业务服务(如 user-service)收到的是网关转发的内部请求,Origin 已被移除或伪造,根本不会触发 CORS 检查!

💡 所以:业务服务根本不需要关心 CORS!


❌ 二、如果在业务服务中也配置跨域,会发生什么?

假设你在 user-service 中也写了:

@Configuration
public class WebConfig {
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("*"));
        // ... 其他配置
        return new CorsWebFilter(config);
    }
}

⚠️ 后果如下:

问题说明
1. 响应头冲突网关和业务服务都加了 Access-Control-* 头 → 浏览器收到两个相同头,可能拒绝
2. 性能浪费每个请求经过多个 Filter,多一次处理开销
3. 配置不一致网关允许 https://shop.urbane.io,但服务允许 * → 安全策略混乱
4. 调试困难浏览器报错 “Multiple CORS headers”,你不知道是哪个服务搞的鬼
5. 安全漏洞如果某个服务误配 allowedOrigins("*"),等于开放所有网站访问你的私有接口

✅ 实测结果(Chrome DevTools):

Access-Control-Allow-Origin: https://shop.urbane.io   ← 来自网关
Access-Control-Allow-Origin: *                       ← 来自 user-service ❌
→ 浏览器报错:The 'Access-Control-Allow-Origin' header contains multiple values...

✅ 三、正确的架构设计:“CORS 只由网关负责”

✅ 推荐架构图(清晰明了)

[浏览器]
   │
   │ 发起跨域请求:GET /user/me
   │ 带 Origin: https://shop.urbane.io
   ▼
[API Gateway] ←───────────── ✅【唯一处理 CORS 的地方】
   │
   │ 移除 Origin,转为内部请求
   │ 添加 X-Forwarded-For、X-User-ID 等 Header
   ▼
[user-service] ←───────────── ❌【完全不知道跨域,也不该知道】
   │
   │ 返回纯 JSON 数据
   ▼
[API Gateway] ←───────────── ✅【接收响应,原样加上 CORS 头返回给浏览器】
   │
   │ 添加:
   │   Access-Control-Allow-Origin: https://shop.urbane.io
   │   Access-Control-Allow-Credentials: true
   │   Access-Control-Allow-Headers: Authorization, Content-Type
   ▼
[浏览器] ←────────────────── ✅【收到响应,CORS 检查通过,允许访问数据】

核心思想

  • 网关是“对外门户”
  • 业务服务是“内部组件”
  • CORS 是边界控制,不是内部逻辑

✅ 四、如何在网关中正确配置跨域?(完整示例)

✅ 在 urbane-commerce-gatewayapplication.yml 中:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 对所有路径生效
            allowedOrigins: "https://shop.urbane.io" # ✅ 生产环境限定域名
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true # ✅ 允许携带 Cookie(JWT)
            maxAge: 3600 # 预检缓存 1 小时

✅ 或者使用 Java 配置(更灵活):

package io.urbane.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

/**
 * 网关全局跨域配置
 * 功能:
 *   - 允许前端域名访问所有 API
 *   - 支持携带凭证(Cookie / Authorization)
 *   - 缓存预检结果,减少 OPTIONS 请求
 *
 * 注意:
 *   - 不要使用 "*" 作为 allowedOrigins,生产环境必须指定具体域名
 *   - allowCredentials=true 时,allowedOrigins 不能为 "*"
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("https://shop.urbane.io")); // ✅ 生产环境
        config.setAllowedMethods(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true); // 必须开启才能携带 Authorization
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

测试方式
在浏览器打开开发者工具 → Network → 查看 /user/me 请求:

  • Response Headers 中是否包含:
    Access-Control-Allow-Origin: https://shop.urbane.io
    Access-Control-Allow-Credentials: true
    

✅ 五、如果你坚持要在业务服务中配置(不推荐),怎么办?

如果你因为历史原因、团队习惯、或者用了旧版 Spring Boot(<2.7),非要保留业务服务的跨域配置,请务必:

✅ 强制关闭业务服务的跨域拦截器

// 在每个业务服务中,禁用自动配置
@SpringBootApplication(exclude = {CorsAutoConfiguration.class})
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

✅ 或者,在 application.yml 中关闭:

spring:
  mvc:
    cors:
      enabled: false

✅ 目的:确保只有网关输出 CORS 头,其他服务完全不参与


✅ 六、企业级最佳实践总结(可打印贴墙上)

原则说明
CORS 只应在网关配置所有外部请求入口都在网关,跨域控制必须集中管理
业务服务不应感知 CORS业务服务是内部服务,收到的是网关转发的请求,非浏览器直连
禁止在业务服务中写 @CrossOrigin 注解这是错误示范,会导致头冲突
生产环境不允许 allowedOrigins("*")必须明确指定前端域名,防 XSS 和 CSRF
必须开启 allowCredentials: true否则前端无法携带 Authorization: Bearer xxx
使用 maxAge: 3600 缓存预检减少 OPTIONS 请求,提升性能
前端只调用网关地址如:https://api.urbane.io/user/me,不要直连 http://user-service:8082

✅ 七、常见误区澄清

误区正解
❌ “我怕网关挂了,业务服务要独立可用”网关是入口,不是可选项;业务服务永远不应该直接暴露给公网
❌ “我的服务是独立部署的,应该自己管 CORS”那就别用微服务!单体应用才这么干
❌ “我用 Feign 调用其他服务,也需要 CORS”❌ 错!Feign 是服务间调用,走内网,没有浏览器,不触发 CORS
❌ “我在 Nginx 做了跨域,就不需要网关配置了”✅ 可以,但建议统一用 Spring Cloud Gateway,便于日志、监控、限流一体化
❌ “我加了 @CrossOrigin 就万事大吉”❌ 你这是在“污染”内部服务,未来一定会出问题

✅ 八、最终建议:立即行动清单

步骤操作
✅ 1删除所有业务服务(user-service、order-service 等)中的 CorsWebFilter@CrossOriginWebMvcConfigurer 跨域配置
✅ 2确保网关中已正确配置 globalcorsCorsWebFilter(见上文)
✅ 3重启网关,测试前端能否正常访问 /user/me/order/list
✅ 4在浏览器开发者工具 Network 标签页,检查响应头是否包含 Access-Control-Allow-Origin
✅ 5通知前端团队:以后所有请求只访问 https://api.yourcompany.com,不要直连任何 service:port
✅ 6把本节内容加入团队《微服务架构规范文档》,作为强制条款

✅ 结语:一句话记住黄金法则

“CORS 是网关的责任,不是业务服务的负担。”

让网关做网关的事 —— 认证、路由、跨域、限流。
让业务服务做业务的事 —— 查询用户、创建订单、扣库存。
分而治之,才是微服务的灵魂。


📦 Bonus:我为你准备了完整配置包

如果你希望我为你提供:

  • 完整的 gateway 跨域配置文件(YAML + Java 版)
  • 前端调用示例(Axios + Vue/React)
  • Postman 测试用例(模拟跨域请求)
  • Docker Compose 示例(网关 + 用户服务 + 前端)
  • GitLab CI 检查脚本(禁止业务服务包含 CORS 配置)

👉 请回复:
“请给我完整的网关跨域配置模板包!”

我会立刻发送你一份开箱即用的企业级跨域解决方案包,包含所有代码、注释和测试用例,你只需复制粘贴,即可彻底解决这个问题 💪

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值