CORS 跨域配置方法(AI 大模型评测平台的前后端分离实践)

前端在 Vercel、后端在自建云主机,两个域名互相调接口,浏览器直接拦截,前端同学第一反应是”后端把跨域放开不就行了”。我当时也这么想过,但放行所有 origin 加允许带 Cookie,等于把后端接口敞开给任意网站调用,安全风险太大。最终平台用 Spring Boot 的 CORS 配置 + 动态白名单 + 凭据策略三件套,把跨域问题解决了,也没留下安全隐患。下面把配置思路、关键响应头和踩过的坑都过一遍。

一、CORS 是什么

先弄清楚问题本身。浏览器同源策略规定,协议 + 域名 + 端口 任意一项不同,请求就不能携带 Cookie 和自定义 Header,这是浏览器层面的安全边界,目的是防止恶意网站冒充用户调用别的网站接口。但前后端分离架构天然跨域,前端调后端就得有一条合规的通道。CORS(Cross-Origin Resource Sharing)就是 W3C 制定的协议,由服务器通过 Access-Control-* 系列响应头声明”哪些 origin 可以访问我”,浏览器看到声明后才放行。

理解这个机制有个关键点:CORS 是服务器声明、浏览器执行的约束,服务端本身并不拦截请求,真正拦在门口的是浏览器。所以后端配置错了,从 curl、Postman 或服务端调用完全看不出来,只有打开浏览器控制台才报错。这个认知帮我们省了不少排查时间——最初定位问题时,团队差点去查服务端日志,实际请求根本没到业务代码。

二、最简配置

Spring Boot 提供了一套开箱即用的 CORS 配置方式,注册一个 WebMvcConfigurer 实现 addCorsMappings 即可。这是大多数人网上搜到的第一版写法:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOriginPatterns("*")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

这段代码能跑通本地联调,但生产环境绝不能直接用——allowedOriginPatterns("*") 加 allowCredentials(true) 组合起来,意味着任何网站都能带上你的 Cookie 调接口,等于把登录态暴露给全世界。我当时用公司安全团队的扫描工具跑了一遍,直接标成高危。所以这版配置只适合开发环境起步,上线前必须换掉。

三、平台的白名单方案

生产环境的做法是把允许的 origin 从通配符换成精确列表,从配置文件读取,改域名不用重新编译。平台最终用的是 CorsConfigurationSource 方案,注册一个 Bean,配置读取 application.yml 里的白名单:

@Configuration
public class CorsConfig {
    @Value("${app.cors.allowed-origins}")
    private List<String> allowedOrigins;

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration cfg = new CorsConfiguration();
        cfg.setAllowedOrigins(allowedOrigins);            // 精确白名单
        cfg.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
        cfg.setAllowedHeaders(List.of(
            "Authorization","Content-Type","token","X-Requested-With"));
        cfg.setExposedHeaders(List.of("Authorization","Content-Disposition"));
        cfg.setAllowCredentials(true);
        cfg.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource src = new UrlBasedCorsConfigurationSource();
        src.registerCorsConfiguration("/**", cfg);
        return src;
    }
}

application.yml:

app:
  cors:
    allowed-origins:
      - https://eval.example.com
      - https://admin.example.com
      - http://localhost:5173   # 本地 Vite 开发

用 allowedOrigins 精确列表之后,allowCredentials(true) 才被允许,Spring 在启动时不会抛异常。这个方案的好处是配置文件化:测试环境、生产环境的白名单分开维护,灰度域名上线时只改配置、不碰代码。白名单一定要精确到子域,别图省事写 *.example.com,否则挂了个不受控的子站等于把跨域放开了。

四、关键响应头解读

配置写完,浏览器实际收到的是哪些响应头,值得逐个弄清楚,否则出问题只能瞎猜。下面是平台实际用到的六组关键头:

响应头 含义 平台设置
Access-Control-Allow-Origin 允许的 origin 回显白名单中的匹配项
Access-Control-Allow-Credentials 是否允许带 Cookie true(前端用 Cookie 存 session)
Access-Control-Allow-Methods 允许的 HTTP 方法 GET/POST/PUT/DELETE/OPTIONS
Access-Control-Allow-Headers 允许的请求头 Authorization/Content-Type/token
Access-Control-Expose-Headers 浏览器可读的响应头 加 Content-Disposition(文件下载文件名)
Access-Control-Max-Age 预检缓存秒数 3600(避免每次 OPTIONS)

这里 Expose-Headers 是最容易被忽略的一个。默认情况下浏览器只暴露少数几个响应头给前端 JS 读取,下载文件时前端要拿 Content-Disposition 里的文件名,不加这个头就拿不到。平台在对接评测报告下载时就在这里卡过一次,前端一直拿不到文件名,加上 Content-Disposition 才通。

五、预检请求处理

浏览器对非简单请求会先发一个 OPTIONS 预检,问服务器”我准备用 POST、带 token 头,你允许吗”。服务器必须正确响应预检,真实请求才会发出。平台用 CorsConfigurationSource 后,Spring 自动处理预检,不需要自己写 OPTIONS 接口。一次完整的预检交互长这样:

OPTIONS /api/eval/submit
Origin: https://eval.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: token,Content-Type

→ 200 OK
Access-Control-Allow-Origin: https://eval.example.com
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Headers: token,Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600

注意 Access-Control-Allow-Headers 必须包含前端实际要带的头,漏一个,预检就直接失败,前端报”Request header field token is not allowed”——这是平台线上遇到过的真实报错。另外 Max-Age: 3600 让浏览器缓存预检结果一小时,避免每次请求都先发一次 OPTIONS,减少一次往返延迟。

六、Cookie 跨域的关键点

平台用 Cookie 存 session,跨域时带 Cookie 有三件事必须同时满足:前端 withCredentials: true、后端 Allow-Credentials: true、后端 origin 不能是通配符。缺任何一环,Cookie 都不会带上,用户会莫名其妙”每请求一次就掉登录”。前端 axios 配置:

import axios from 'axios';
const http = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  withCredentials: true,        // 关键!
  timeout: 30000,
});

后端必须显式指定 origin(不能用 *),且 Allow-Credentials: true。这就是为什么不能用 allowedOriginPatterns("*")。调试这个问题的经验:在浏览器 Network 面板看请求的 Request Headers 里有没有 Cookie,没有就说明 withCredentials 没生效,或预检没通过。

七、与鉴权拦截器的配合

平台后来把 session 从 Cookie 换成了自定义 token Header 存储,因为前后端分离场景下 Header 比 Cookie 更好控制、也更适合移动端复用。切换时有个连锁动作:CORS 的 allowedHeaders 必须把 token 加进去,否则预检直接拦掉:

cfg.setAllowedHeaders(List.of("Authorization","Content-Type","token","X-Requested-With"));

OPTIONS 预检通过后,真实请求会带 token 头,鉴权 AOP 才能正常解析。这里埋过一个坑:加了 token 头但预检缓存还挂着旧配置,浏览器一直复用旧的 OPTIONS 结果,改了配置不生效,把 Max-Age 临时设成 0 才能强制重新预检。

八、踩过的坑

跨域配置看着简单,实际部署时踩过的坑一个比一个隐蔽,都列在这里。

  • allowedOrigins("*") + allowCredentials(true) 直接报错:Spring Boot 6+ 启动就抛 IllegalArgumentException,必须用 allowedOriginPatterns 或者列白名单。
  • Nginx 反代后 CORS 头重复:Nginx 已加 add_header Access-Control-Allow-Origin 又经过 Spring Boot,浏览器看到两个 Allow-Origin 直接报错。要么只在 Spring 配,要么在 Nginx 配 proxy_pass_header 不透传。
  • 本地 localhost 跨域:开发时 http://localhost:5173 与 http://127.0.0.1:5173 是不同 origin,必须都加白名单。
  • CSRF 误伤:Spring Security 默认开 CSRF,前后端分离架构会拦截 POST,要么关掉 CSRF 要么只用 token 鉴权(不依赖 Cookie),平台选后者。
  • 网关层 CORS 与应用层 CORS 重复:SluGateway/Kong 已加 CORS 时,应用层要写 setAllowedOriginPatterns("*") 兜底,否则会被网关拦截。

Nginx 头重复这个坑排查了很久,当时浏览器控制台报 “The ‘Access-Control-Allow-Origin’ header contains multiple values”,一度以为是 Spring 配错,后来抓包才发现 Nginx 和 Spring 各加了一次。约定只有一层加 CORS 后问题就消失了。

九、生产环境的安全实践

配置稳定后,还要从安全角度再过一遍。平台总结出五条生产规范,每条都是真实踩坑或扫描告警逼出来的:

  1. 白名单精确到子域:不要写 *.example.com,明确写 https://app.example.com。
  2. 限制允许的 Header:不要 *,只列业务需要的。
  3. 限制暴露的 Header:避免把内部 Header 暴露给前端。
  4. Max-Age 不要设太大:1 小时合适,开发期可以设 0 方便调试。
  5. 监控异常 origin:日志里记下所有 Origin 头,发现异常来源立即封禁。

其中监控异常 origin 这条收益明显:上线后日志里出现过几次陌生域名尝试调用,我们靠 Origin 日志快速定位并封了来源 IP。CORS 配置本身只是”声明允许谁”,真正的安全边界还要靠鉴权兜底,两层都做才算完整。到这里,从配置到预检、从 Cookie 到安全实践的跨域方案就完整了。

常见问题(FAQ)

Q1:用了 Nginx 反代还需要 CORS 配置吗?

如果 Nginx 和后端同源(都在 eval.example.com 下),不需要;前后端不同源必须配,可以放 Nginx 也可以放 Spring Boot,二选一不要重复。

Q2:手机 App 调用需要 CORS 吗?

不需要,CORS 是浏览器的限制。原生 App / Postman / curl 都没有这层。

Q3:上线后改了前端域名怎么办?

改 application.yml 里的 app.cors.allowed-origins 重启即可。建议把这部分配置放 Nacos/Apollo 配置中心,改完无需重启。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部