目录

  • 前言
  • 准备工作
  • 配置 CORS 允许跨域
  • CORS 允许跨域原理
  • 为什么跨域配置无效?
  • CORS 配置失效解决方案
  • 解决方案一(推荐)
  • 解决方案二
  • 前/后端源码


前言

浏览器有跨域限制,非同源策略(协议、主机名或端口不同)被视为跨域请求。

解决方案目前主流分两种:

1.后端:配置 CORS,允许跨域资源共享。
2.前端:配置反向代理,让浏览器误认为是同源请求,自然不会有跨域请求限制的问题。

前后端方式任选一种都能解决跨域问题,选择哪一种,取决于真正想解决问题的人,别前后端互相推事儿。

本文通过配置 CORS 来解决跨域,也就是从后端开发的角度解决问题,并从原理分析为什么你配置的 CORS 无法解决跨域问题。

准备工作

使用前后端分离开源项目 youlai-boot + vue3-element-admin 做跨域请求测试 。

其中 vue3-element-admin 默认通过 vite + proxy 前端反向代理解决跨域,如果想关闭代理仅需修改 baseURL 为接口地址(http://localhost:8989),其他不用动,如下:

// request.ts
const service = axios.create({
  //baseURL: import.meta.env.VITE_APP_BASE_API,  // 前端反向代理解决跨域的配置
  baseURL: "http://localhost:8989", // 后端通过配置CORS解决跨域的配置, http://localhost:8989 是后端接口地址
  timeout: 50000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' }
});

配置 CORS 允许跨域

一般情况在项目添加以下配置即可解决浏览器跨域限制。

/**
 * CORS 资源共享配置
 *
 * @author haoxr
 * @date 2022/10/24
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //1.允许任何来源
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        //2.允许任何请求头
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        //3.允许任何方法
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        //4.允许凭证
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
}

CORS 允许跨域原理

CorsFilter 读取 CorsConfig 配置通过 DefaultCorsProcessor 给 response 响应头添加 Access-Control-Allow-* 以允许跨域请求能够被成功处理。

响应头参数

作用

Access-Control-Allow-Origin

允许访问的源地址

Access-Control-Allow-Methods

允许访问的请求方法

Access-Control-Allow-Headers

允许访问的请求头

Access-Control-Allow-Credentials

是否允许发送 Cookie 等身份凭证

Access-Control-Max-Age

缓存预检请求的时间

核心是 DefaultCorsProcessor# handleInternal 方法

spring boot跨域不生效 springboot跨域配置无效_spring boot跨域不生效

spring boot跨域不生效 springboot跨域配置无效_java_02

为什么跨域配置无效?

但。。。有的项目按照如上配置允许跨域请求成功了,但有些项目却不生效?

其实就是一个结论:有中断响应的过滤器在 CorsFilter 之前执行了,也就无法执行到 CorsFilter,自然 CorsConfiguration 中的配置形同虚设。

常见的场景:项目中使用了 Spring Security 安全框架导致 CORS 跨域配置失效。

接下来就 Spring Security 导致 CORS 配置失效展开分析。

在 ApplicationFilterChain#internalDoFilter 添加断点,然后通过改造后(移除反向代理)的 vue3-element-admin 发出跨域请求。

spring boot跨域不生效 springboot跨域配置无效_跨域_03

可以看出 SpringSecurityFilterChain 是先于 CorsFilter 执行的(重点), 如果是跨域请求浏览器会在正式请求前发出一次预检请求(OPTIONS),判断服务器是否允许跨域。

跨域请求没到达 CorsFilter 过滤器就先被 Spring Security 的过滤器给拦截了,要知道预检 OPTIONS 请求是不带 token 的,所以响应 401 未认证的错误。预检请求失败导致后面的请求响应会被浏览器拦截。

spring boot跨域不生效 springboot跨域配置无效_java_04

CORS 配置失效解决方案

根据配置失效原理分析,有两个解决方案:

  • 解决方案一: 配置 CorsFilter 优先于 SpringSecurityFilter 执行;
  • 解决方案二: 放行预检 OPTIONS 请求 + 基础 CORS 配置。

解决方案一(推荐)

配置 CorsFilter 优先于 SpringSecurityFilter 执行

Spring Security 过滤器是通过 SecurityFilterAutoConfiguration 的 DelegatingFilterProxyRegistrationBean 注册到 servletContext上下文,其中过滤器的顺序属性 Order 读取的 是 SecurityProperties 的默认配置也就是 -100;

SpringBoot 可以通过 FilterRegistrationBean 来对 Filter 自定义注册(排序), 设置 Order 小于 SpringSecurity 的 -100 即可。完整配置如下:

/**
 * CORS资源共享配置
 *
 * @author haoxr
 * @date 2023/4/17
 */
@Configuration
public class CorsConfig {

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //1.允许任何来源
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        //2.允许任何请求头
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        //3.允许任何方法
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        //4.允许凭证
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        CorsFilter corsFilter = new CorsFilter(source);

        FilterRegistrationBean<CorsFilter> filterRegistrationBean=new FilterRegistrationBean<>(corsFilter);
        filterRegistrationBean.setOrder(-101);  // 小于 SpringSecurity Filter的 Order(-100) 即可

        return filterRegistrationBean;
    }
}

可以看到不同源的跨域请求能够成功响应。

spring boot跨域不生效 springboot跨域配置无效_跨域_05

解决方案二

放行预检 OPTIONS 请求 + 基础 CORS 配置

SecurityConfig 放行 OPTIONS 预检请求配置 SecurityConfig 配置源码

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http 
            	...
                // 走 Spring Security 过滤器链的放行配置
                .requestMatchers(HttpMethod.OPTIONS,"/**").permitAll() // 放行预检请求
                .anyRequest().authenticated();

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 不走过滤器链的放行配置
        return (web) -> web.ignoring()
                .requestMatchers(HttpMethod.OPTIONS,"/**") // 放行预检请求
         
    }

基础的跨域共享配置

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //1.允许任何来源
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        //2.允许任何请求头
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        //3.允许任何方法
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        //4.允许凭证
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
    
}

另外有自定义过滤器 (例如:VerifyCodeFilter)通过 response.getWriter().print() 响应给浏览器也是不走后面的 CorsFilter 过滤器,所以需要设置响应头

// ResponseUtils# writeErrMsg
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("Access-Control-Allow-Origin","*");
response.getWriter().print(JSONUtil.toJsonStr(Result.failed(resultCode)));