写在前面
在SpringSecurity中配置跨域,我相信所有用过SpringSecurity的人应该都知道,因为实在是太简单了。那我为什么还要写这篇文章呢?写这篇文章的目的当然不是去解释如何配置跨域,而是通过分析Spring对跨域支持的源码来感受设计中的优雅。
先声明一下开发环境:SpringBoot:2.2.2
正文
既然说到SpringSecurity配置跨域,那么我们就先简单复习一下如何配置跨域。
配置跨域
我们都知道集成SpringSecurity后配置跨域我们只需要在继承WebSecurityConfigurerAdapter
类,重写configure(HttpSecurity http)
方法,开启cors
并提供一个跨域配置源即可。下面是一个例子:
1 |
|
就如前面所说,只需要开启cors
再提供一个跨域配置源即可。方法很简单,但这里有个坑需要注意一下。
暴露公共接口时跨域的一个坑
如果我们还重写了
configure(WebSecurity web)
方法,使用web.ignoring().antMatchers(ignorePaths)
去暴露一个公共接口’/pub’那么上面的跨域配置对这个接口来说就没用,也就是说这个接口会出现跨域问题。然而我们原本就是为了提供公共接口’/pub’,但现在却有跨域问题,那怎么能行!!!(一般来说这个方法是对静态资源设置直接放行,而不是公共接口!)
那这到底是为什么呢?
因为SpringSecurity配置跨域支持,是通过CorsFilter
过滤器来实现的,我们web.ignoring()
中设置后对应的接口请求就不会经过CorsFilter
来处理,这个接口当然就存在跨域问题了!之所以说这个方法是对静态资源设置直接放行,而不是公共接口也是这个原因,那正确的方法是什么呢?还是configure(HttpSecurity http)
方法:
1 |
|
cors方法
现在我们来看一下cors()
方法,点进这个方法看看,其实很简单,就是应用了一个CorsConfigurer
配置类。如果看过SpringSecurity
自动配置,对形如xxxConfigurer
的类名应该不陌生。
这个Configurer
其实就是在”FilterChain”上添加了一个过滤器,即CorsFilter
我们都知道CorsFilter
的构造方法需要一个CorsConfigurationSource
,在请求到来时,使用CorsProcessor
根据提供的CorsConfiguration
去对请求进行处理(在CorsFilter
中默认是DefaultCorsProcessor
)而CorsConfiguration
是通过CorsConfigurationSource#getCorsConfiguration
方法获得的,所以说怎么获得CorsConfigurationSource
至关重要。
还记得上面在配置CorsConfigurationSource
时,我们直接注册Bean而不是通过configurationSource()
方法指定吗?这种方法为什么是可行的呢?来看一下CorsConfigurer
是如何获得CorsConfigurationSource
并构造CorsFilter
的:
1 | private CorsFilter getCorsFilter(ApplicationContext context) { |
获取CorsConfigurationSource
并构造CorsFilter
的步骤注释里写的很清楚了,正常来说我们配置跨域配置源不管是直接指定也好,还是注册成Bean也好(注意Bean名字的要求),都是可以被获取到的。一般情况下,我们也的确是这样做的(直接提供一个CorsConfigurationSource
)。但为什么最后有MvcCorsFilter.getMvcCorsFilter(context)
这样一个调用?通过这个方法里抛出的异常信息不难猜测到是SpringSecurity为了兼容SpringMVC中配置跨域的方式。
还记得不使用SpringSecurity时如何在SpringMVC中配置支持跨域吗?
两种方式:
- 将
@CrossOrigin
注解标注在支持跨域的接口上 - 重写
WebMvcConfigurer#addCorsMappings
方法进行全局配置
看到这里你可能会猜测:是不是HandlerMappingIntrospector
实现了CorsConfigurationSource
,并且是根据上面两种方式的配置来返回跨域配置的呢?
事实上,的确是这样的。为了便于理解后面给出的代码,先来看看CorsFilter
类和CorsConfigurationSource
接口:
CorsConfigurationSource
1 | public interface CorsConfigurationSource { |
跨域配置源,实现类要实现getCorsConfiguration
方法返回一个CorsConfiguration
跨域配置,其中包含允许那些域、请求方法、请求头,是否允许携带凭证,缓存时间是多久,允许携带的头属性等信息。
CorsConfigurationSource
有五个实现类:
- CorsInterceptor
- HandlerMappingIntrospector
- PreFlightHandler
- ResourceHttpRequestHandler
- UrlBasedCorsConfigurationSource
CorsFilter
1 | public class CorsFilter extends OncePerRequestFilter { |
DefaultCorsProcessor#processRequest
中根据请求是否跨域,是否是预检请求以及CorsConfiguration
等信息来对请求进行处理和在响应头中写入一些信息。具体的源码就不分析了,还是比较好理解的。前提是需要对CORS有一定的了解,可以看下HTTP访问控制(CORS)这篇文章。
HandlerMappingIntrospector
HandlerMappingIntrospector
比较特别,不要认为这是个拦截器,”Introspector”翻译成中文是内省者的意思。
这个类在初始化后会调用afterPropertiesSet
方法,将容器中所有的HandlerMapping
添加到该类的handlerMappings
这个List
中。
来看一下官方对于这个类的解释:
1 | Helper class to get information from the HandlerMapping that would serve a specific request. |
这个类是一个帮助类,用于从
HandlerMapping
中获取请求的特定信息,提供了两个方法。第一个方法用于获取一个MatchableHandlerMapping
来检查请求匹配条件,第二个方法用于获取适用于这个请求的CorsConfiguration
跨域配置。
我们重点关注第二个方法:
1 | public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { |
这个CorsConfigurationSource
实现类根据请求从HandlerMapping
中获取获取HandlerExecutionChain
执行链,再依次从执行链的拦截器和处理器中获取CorsConfigurationSource
,如果获取到了再调用其HandlerMappingIntrospector#getCorsConfiguration
方法返回跨域配置。具体来说就是那两个if判断。
所以这么说来的话,HandlerMappingIntrospector
虽然实现了CorsConfigurationSource
但其本质有点像一个委托类?它检查请求对应的执行链上的拦截器和处理器有没有实现CorsConfigurationSource
,如果有,再委托给这个CorsConfigurationSource
来获取CorsConfiguration
。所以说如果我们在一个Controller
的接口上标注了@CrossOrigin
注解,那么对应的,在拦截器中获取不到CorsConfiguration
,就会从这个Handler上获取到CorsConfiguration
,也就是将@CrossOrigin
注解中提供的信息封装成了CorsConfiguration
。那为什么还会先检查执行链中的拦截器呢?
因为SpringMVC中还有第二种方法配置跨域支持,也就是上面提到的重写WebMvcConfigurer#addCorsMappings
方法进行全局配置。那为什么重写这个方法添加跨域配置最后会注册成拦截器呢?(一个实现了CorsConfigurationSource
的拦截器)
这就要说到SpringBoot在WebMvc的自动配置、WebMvcConfigurer
和HandlerMapping
了。
如果你有仔细看过SpringBoot在SpringMVC的自动配置方面的源码,你一定知道WebMvcConfigurationSupport
这个最主要的配置类在注册HandlerMapping
的时候会从一个CorsRegisty
中获取跨域配置:(这里以RequestMappingHandlerMapping
为例)
1 |
|
addCorsMappings()
方法是个空方法,并且只有DelegatingWebMvcConfiguration
类重写了这个方法。实际上WebMvcConfigurationSupport
这个类中用@Bean
这个可传递的注解标注了很多方法但该类上并没有标注@Configuration
,那么为什么还会起到配置类的作用呢?其实真正的配置类是DelegatingWebMvcConfiguration
。
DelegatingWebMvcConfiguration
在DelegatingWebMvcConfiguration
这个类上有个@Configuration
注解,并且继承自WebMvcConfigurationSupport
,实际上它就是个委托类。
可以说这个类才是真正的配置类,去看看DelegatingWebMvcConfiguration
这个类,相信你一定会发现什么!!!
DelegatingWebMvcConfiguration
中有WebMvcConfigurerComposite
这么一个对象,并且将容器中所有WebMvcConfigurer
注入进来:
1 | false) (proxyBeanMethods = |
如果你去看了一下这个类的源码,你就会发现WebMvcConfigurer
中有的方法这个类都有,并且这个委托类仅仅是将请求委托给configurers
,来看看重写的addCorsMappings
方法:
1 |
|
调用WebMvcConfigurerComposite#addCorsMappings
,显而易见WebMvcConfigurerComposite
是个复合的WebMvcConfigurer
,他也实现了WebMvcConfigurer
并且内部维护了一个List<WebMvcConfigurer> delegates
列表,实现的所有方法会依次调用列表中WebMvcConfigurer
对应的方法。(并且你还能发现WebMvcConfigurer
中的方法都是作为回调方法并且大部分是返回void的)
说到这里,不得不说一个题外话。如果看Spring源码比较多的话,就会发现Spring中类的命名都有规律可循并且某些后缀都是有特定意义的,比如
xxxComposite
、xxxConfigurer
、Delegatingxxx
、xxxDelegator
等等,这样我们看到这个类名就立马能猜到它的作用。
我们平时对WebMvc进行一些配置都是实现WebMvcConfigurer
类,重写其中的方法。下面是一个例子:
1 |
|
说到这,也就是相当于WebMvcConfigurationSupport#getCorsConfigurations
方法会回调容器中所有WebMvcConfigurer
实现类的addCorsMappings()
方法,向CorsRegistry
中添加跨域映射,然后再取出CorsConfiguration
返回:
1 | protected final Map<String, CorsConfiguration> getCorsConfigurations() { |
最后反应到AbstractHandlerMapping
中的就是使用CorsConfiguration
注册一个CorsInterceptor
拦截器,这个拦截器是AbstractHandlerMapping
中的一个内部类,继承自HandlerInterceptorAdapter
,并且实现了CorsConfigurationSource
。
看到这里,如果没有了解过
HandlerMapping
,可能会一头雾水,可以看看我的这篇文章源码角度分析Spring容器启动阶段注册Controller处理器的流程,虽然不是讲HandlerMapping
,但是相信在看完后,会对HandlerMapping
有一个理解。
CorsInterceptor
1 | private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource { |
这个类只重写了拦截器的preHandle
方法,其他方法都是空方法。而且你能发现这个preHandle
方法中的内容和CorsFilter#doFilterInternal
方法基本是一模一样的,都是根据CorsConfiguration
使用跨域处理器处理请求。
看到这里,现在应该知道关于HandlerMappingIntrospector
的猜测是没错的,并且知道了HandlerMappingIntrospector
是如何与SpringMVC两种支持跨域的配置方式联系起来的,这里再次总结一下:
- 首先获取请求对应的执行链上的拦截器,判断拦截器有没有实现
CorsConfigurationSource
(CorsInterceptor
类),如果有则调用getCorsConfiguration
获取CorsConfiguration
后返回 - 如果拦截器上获取失败,则判断处理器有没有实现
CorsConfigurationSource
(PreFlightHandler
类),如果有则调用getCorsConfiguration
获取CorsConfiguration
后返回
从而实现了兼容SpringMVC中两种配置跨域的方式。
这其中最关键的几点就在于CorsConfigurer
获取CorsConfigurationSource
并且构造CorsFilter
的步骤、HandlerMappingIntrospector
获取CorsConfiguration
的步骤,还有Spring回调WebMvcConfigurer
对HandlerMapping
进行设置跨域配置等信息的步骤
其中还涉及到了SpringMVC中HandlerMapping
、HandlerExecutionChain
、Handler
、Interceptor
等相关知识。
根据这次的分析,能得到几个结论:
- SpringMVC支持跨域两种方式一个是基于处理器实现,另一个是基于拦截器实现。
- SpringSecurity跨域是基于过滤器,并且兼容了SpringMVC的两种配置(使用
HandlerMappingIntrospector
“桥接”)。 - SpringSecurity中的
CorsConfigurer
使用HandlerMappingIntrospector
来兼容SpringMVC跨域两种方式。 HandlerMappingIntrospector
获取CorsConfiguration
时的优先级是先拦截器,再处理器。- SpringBoot注册
HandlerMapping
或者说通过WebMvcAutoConfiguration
自动配置来对WebMvc必要的组件进行装配和注入。 WebMvcConfigurer
是DelegatingWebMvcConfiguration
类驱动WebMvcConfigurerComposite
来进行回调的。
并且经过这次的源码阅读,也是足足感受到Spring设计上的优雅。
写在最后
文章写的有点乱,并且有点跳跃。仅仅是跟着文章来看可能不大能看懂,最好在电脑上根据源码来阅读。这篇文章也仅仅是作为我个人在一次踩坑后好奇心大法,阅读源码后的一段总结以及感悟吧,自己能看懂并且以后还能看懂也就满意了。如果这篇文章有幸被你刷到并且你能够看懂我想表达的那我自然是更高兴。其实这个博客存在的理由也仅仅是为了记录自己学习过程中的感悟和总结,便于自己以后回顾,毕竟我比较健忘。所以需要记录下有必要的,并且在个人看来,这篇文章干货还是足足的,所以说更加有必要记录。其实在写文章之初我也不想写这么一篇文章,因为实在是太难写明白了,并且由于涉及到的东西比较分散很难进行组织,也可能我表达能力差的原因吧,但最终还是花了一下午加一晚上,在不断修改下产出了这么一篇很长很长很长的文章,可能是写过的字数最多的文章了吧😥。文章中可能有错别字也可能有错误的内容,如果你发现文章有什么错误的地方或者没表述清楚的内容,欢迎在评论中交流。