SpringBoot 自定义Filter 提前返回 CORS 错误 处理前后端分离跨域配置无效问题解析

前言

浏览器有跨域限制,非同源策略 (协议、主机名或端口不同) 被视为跨域请求,解决跨域有跨域资源共享(CORS)、反向代理和 JSONP的方式。本篇通过 SpringBoot 的资源共享配置 (CORS) 来解决前后端分离项目的跨域,以及从原理上去解决跨域配置不生效的问题。

准备工作

https://gitee.com/youlaiorg/vue3-element-admin) 默认通过 vite + proxy 前端反向代理解决跨域,如果想关闭方向代理只需修改 baseURL 即可:

复制代码
// 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 方法

CORS 配置失效原理分析

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

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

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

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

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

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

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

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;
    }
}

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

解决方案二

放行预检 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,"/**") // 放行预检请求
         
    }

或者

复制代码
		  //放行 options 的请求
        if ("OPTIONS".equals(request.getMethod())) {
            filterChain.doFilter(request, servletResponse);
        }

基础的跨域共享配置

复制代码
@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)));
相关推荐
8***23551 分钟前
【wiki知识库】07.用户管理后端SpringBoot部分
java
bcbnb2 分钟前
iOS 性能测试的工程化方法,构建从底层诊断到真机监控的多工具测试体系
后端
开心就好20255 分钟前
iOS 上架 TestFlight 的真实流程复盘 从构建、上传到审核的团队协作方式
后端
小周在成长14 分钟前
Java 泛型支持的类型
后端
aiopencode14 分钟前
Charles 抓不到包怎么办?HTTPS 抓包失败、TCP 数据流异常与底层补抓方案全解析
后端
阿蔹17 分钟前
JavaWeb-Selenium 配置以及Selenim classnotfound问题解决
java·软件测试·python·selenium·测试工具·自动化
稚辉君.MCA_P8_Java19 分钟前
Gemini永久会员 C++返回最长有效子串长度
开发语言·数据结构·c++·后端·算法
Penge66637 分钟前
Redis-bgsave浅析
redis·后端
阿白的白日梦43 分钟前
Windows下c/c++编译器MinGW-w64下载和安装
c语言·后端
小毅&Nora1 小时前
【后端】【C++】从裸指针到 C++20 协程:现代 C++ 内存与并发编程的双重革命
java·c++20