Springboot加了拦截器后出现的跨域问题解析
1.背景
起初解决Springboot跨域问题的方法是直接在Controller上添加@CrossOrigin
注解,实现了前后端分离中的跨域请求。但随着业务代码的编写,做了token会话保持的检验,添加了拦截器后,再次出现了跨域问题。很纳闷,讲理说后台已经允许了跨域请求,之前的测试也证明了这一点,那为什么又突然出现了跨域拦截问题呢?
2.分析及解决方案
在拦截器中作了判断,如果请求头中没有携带token,则是非法请求,直接返回404码。然后由于有对获取的请求头中的token做打印,所以出现问题的时候,控制台的token值为null,打开浏览器的开发者工具查看请求头也发现没有携带成功。之前在拦截器没完全实现业务逻辑的时候做了对header是否成功获取的校验(也就是控制台打印看有值没),当时是成功获取到了的,所以都不用考虑前台问题,问题出在后台,准确的说是拦截器。
那么为什么浏览器不能成功发送token呢?根据线索在更详细的查看了CROS的介绍后发现,原来CROS复杂请求时会首先发送一个OPTIONS请求做嗅探,来测试服务器是否支持本次请求,请求成功后才会发送真实的请求;而OPTIONS请求不会携带数据,导致这个请求被拦截了,直接返回了状态码,响应头中没携带解决跨域问题的头部信息,出现了跨域问题。
那么,为什么会发生提前结束的这种情况呢?难道自定义的拦截器优先于@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配置做相应的处理。
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()方法,令其跳过预检请求的拦截。
拦截器依赖于web框架,在SpringBoot中就是依赖于SpringMVC框架。在实现上,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在service或者一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑的操作。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。
3.结语
以上,希望对您有所帮助~