Spring Security与Spring Boot集成原理
Spring Security依赖的是过滤器机制,首先是web容器例如tomcat作为独立的产品,本身有自己的一套过滤器机制用来处理请求,那么如何将tomcat接收到的请求转入到Spring Security的处理逻辑呢?spring充分采用了tomcat的拓展机制提供了tomcat过滤器的一个拓展,也就是中间加了一个适配器。这样当请求过来之后,请求经过web容器的过滤器的时候就会进入spring的过滤器流程,从而进入security的过滤器流程。
Tomcat的拓展机制
根据servlet规范:
从Servlet3.0开始,在ServletContext中添加了方法支持在使用编程的方式来自定义servlet,filter和url pattern。有两种方式来定义这些:
contexInitialized
method of aServletContextListener
implementation;- the
onStartup
method of aServletContainerInitializer
implementation;
我们来看通过外部ServletContainerInitializer实现的加载方式,官方定义:在服务启动的时候会通过java的spi机制去查找ServletContainerInitializer类,并且每个应用在启动的时候会实例化ServletContainerInitializer。我们需要将实现放到META-INF/services/javax.servlet.ServletContainerInitializer文件中。
For each application, an instance of the ServletContainerInitializer is created by the container at application startup time.
ServletContainerInitializer
会在服务启动的时候并且所有的servlet监听器调用之前被调用。
在文档中还描述了一个重要的注解就是:HandlesType,这个注解的作用是:在ServletContainerInitializer
的 onStartup
方法会被调用的时候,会传入一个 Set
集合,该集合包含的类要么是扩展/实现了初始化器所感兴趣(通过 @HandlesTypes
注解指定)的类,要么是带有通过 @HandlesTypes
注解指定的任何类作为注解的类。
The
onStartup
method of theServletContainerInitializer
is called with aSet
of Classes that either extend / implement the classes that the initializer expressed interest in or if it is annotated with any of the classes specified via the@HandlesTypes
annotation.
在了解了上面的前提之后,我们来看Spring进行的拓展,查看spring-web中文件META-INF/services/javax.servlet.ServletContainerInitializer会发现里面有一个实现类:org.springframework.web.SpringServletContainerInitializer
,而这也正是一切的开始。
SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {@Overridepublic void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)throws ServletException {......}
}
我们首先注意到在上面的实现中,@HandlesTypes
注解指定的是WebApplicationInitializer
类,因此在onStartup
方法的参数webAppInitializerClasses中我们就能获取到所有该接口的实现类。之后便是通过反射进行实例化和执行所有WebApplicationInitializer
实现类的onStartup
方法。
查看WebApplicationInitializer
接口的实现类可以看到:
由于引了包的缘故,这里看到了web、webmvc和security下面的实现。这里我们看到了熟悉的ContextLoader,当还在使用web.xml集成配置spring的时候,我们配置过<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>,在这里使用AbstractContextLoaderInitializer
以编程的方式实现了同样的效果。同样的道理还有AbstractDispatcherServletInitializer
,也使用编程的方式替换掉了我们的web.xml配置。
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
那Spring Security的实现呢,我们还看到了AbstractSecurityWebApplicationInitializer
这个实现类?
AbstractSecurityWebApplicationInitializer
tomcat容器在执行的时候会执行过滤器链FilterChain。同时为拓展filter提供了标准,我们可以通过实现Filter
接口来注册我们自己的Filter到FilterChain中,但是tomcat容器不会感知到spring容器中的Filter实例。
因此spring提供了一个Filter的实现:DelegatingFilterProxy
,从而将spring中的过滤器的逻辑加入到tomcat容器中。
我们查看Spring Security的官网,对这个架构图不会陌生:
为了和spring的集成,Spring Security利用spring提供的过滤器的拓展点,提供了一个Spring Security的统一的入口:FilterChainProxy
,在集成之后执行过滤器的时候会通过:tomcat Filter->spring filter->spring security filter调用链路进行调用。
onStartup
我们来看一下AbstractSecurityWebApplicationInitializer
的onStartup
方法的实现逻辑:
insertSpringSecurityFilterChain
方法的实现:
在这里创建了一个DelegatingFilterProxy
对象,并且传递的filterName是DEFAULT_FILTER_NAME,这个常量是“springSecurityFilterChain”。这里需要注意的是这个传递的是filterName是一个bean名称。
在第一次接收到请求的时候,在doFilter方法中会使用单例模式从spring容器中获取bean名称的实例,生成一个唯一的Filter
对象。通过调试最后得到的是:FilterChainProxy
对象。
从这里我们可以知道,如果我们拓展一套自己的过滤器逻辑,则可以把DelegatingFilterProxy
当作入口,filterName参数使用我们自己的filter bean名称就可以了。
总结
在servlet3.0之后,我们可以将之前web.xml中的配置内容通过编程式的方式来实现。Spring充分利用了这一拓展,提供了ServletContainerInitializer接口的实现类SpringServletContainerInitializer来在启动的时候初始化所有的WebApplicationInitializer。
Spring Security使用了这个拓展方式,将请求通过tomcat Filter->spring filter->spring security filter这样的链路,转入到自己的处理逻辑当中。
对于我们自定义的过滤器也可以通过DelegatingFilterProxy
作为接入点,将请求引入进来。
参考
- servlet-spec-6.1