1.背景

起初解决Springboot跨域问题的方法是直接在Controller上添加@CrossOrigin注解,实现了前后端分离中的跨域请求。但随着业务代码的编写,做了token会话保持的检验,添加了拦截器后,再次出现了跨域问题。很纳闷,讲理说后台已经允许了跨域请求,之前的测试也证明了这一点,那为什么又突然出现了跨域拦截问题呢?
在这里插入图片描述

2.分析及解决方案

在登录拦截器中作了校验,对于需要登录后才能访问的接口,如果请求头中没有携带token,则是非法请求,直接返回404码。然后由于一直有对拦截到的请求中的请求头中的token做打印,所以出现问题的时候,控制台打印的token值为null,打开浏览器的开发者工具查看请求头也发现没有携带成功。而在没有添加拦截器之前上述问题都是不存在的,都是正常的,所以都不用考虑是前端问题,问题肯定出在后端,准确的说是拦截器。

那么为什么浏览器不能成功发送token呢?根据线索在更详细的查看了CROS的介绍后发现,原来CROS复杂请求时会首先发送一个OPTIONS请求做嗅探,来测试服务器是否支持本次请求,请求成功后才会发送真实的请求;而OPTIONS请求不会携带任何数据,导致这个请求不符合我们拦截器的校验规则被拦截了,直接返回了状态码,响应头中也没携带解决跨域需要的头部信息,进而出现了跨域问题。所以在浏览器调试工具中会发现该次请求没有携带token,后端控制台打印token也为null。

但是,就算这样,为什么会发生在添加跨域相关头部信息前就提前结束请求的这种情况呢?难道自定义的拦截器优先于@CrossOrigin注解执行?

通过查阅资料,解析@CrossOrigin注解的源码得知,如果Controller在类上标了@CrossOrigin或在方法上标了@CrossOrigin注解,则Spring 在记录mapper映射时会记录对应跨域请求映射,将结果返回到AbstractHandlerMethodMapping,当一个跨域请求过来时,Spring在获取handler时会判断这个请求是否是一个跨域请求,如果是,则会返回一个可以处理跨域的handler。总结起来@CrossOrigin的原理相当于和Handler进行强绑定。

于是现在的问题又到了:Handler和拦截器的执行顺序?

DispatchServlet.doDispatch()方法是SpringMVC的核心入口方法,经过分析发现所有的拦截器的preHandle()方法的执行都在实际handler的方法(比如某个API对应的业务方法)之前,其中任意拦截器返回false都会跳过后续所有处理过程。而SpringMVC对预检请求的处理则在PreFlightHandler.handleRequest()中处理,在整个处理链条中出于后置位。由于预检请求中不带数据,因此先被权限拦截器拦截了。

所以每次获取不到token的请求都是OPTIONS请求,那么解决的方法就很明了了:把所有的OPTIONS请求统统放行

//拦截器取到请求先进行判断,如果是OPTIONS请求,则放行
if("OPTIONS".equals(httpServletRequest.getMethod().toUpperCase())) {
    System.out.println("Method:OPTIONS");
	return true;
 }

测试一下,发现打印有OPTIONS的提示,说明确实存在OPTIONS请求,并且成功解决了问题。

补充1:另一种解决方法:

不使用@CrosOrigin注解解决跨域问题,使用过滤器:示例使用CorsFilter,也就是一个封装了解决跨域问题的filter而已。

由于CorsFilter是定义在Web容器中的过滤器(实现了javax.servlet.Filter),因此其执行顺序先于Servlet,而SpringMVC的入口是DispatchServlet,因此该Filter会先于SpringMVC的所有拦截器执行。分析代码可知,CorsFilter可以对获取的单个请求对应的Cors配置做相应的处理。这样当请求到达拦截器前,做跨域嗅探的OPTIONS请求已经得到答案返回了,经过测试并不会到达拦截器。

package com.example.pahms.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class GlobalCorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        config.setAllowCredentials(true);
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.addExposedHeader("token");
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);
        return new CorsFilter(configSource);
    }

}

补充2:还有一种方法,仅提供思路,没做实现。

利用AOP环绕增强所有自定义拦截器的preHandle()方法,令其跳过预检请求(嗅探)的拦截,相当于把上述拦截器中放行OPTIONS的操作放到AOP的切入函数中,实现全局放行。

拦截器依赖于web框架,在SpringBoot中就是依赖于SpringMVC框架。在实现上,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在service或者一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑的操作。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。

3.结语

以上,希望对您有所帮助~

Logo

前往低代码交流专区

更多推荐