SpringSecurity是如何玩弄过滤器链的
- 引言
- SpringSecurity的自动配置流程
- SpringSecurity的过滤器链是个什么样子
- FilterChainProxy
- VirtualFilterChain
本文适合在对SpringSecurity有基本认识,并且会基础使用的,想要进阶研究源码的小伙伴。
引言
SpringSecurity核心有两大功能: 认证和鉴权。其中认证和鉴权中的一部分都是通过SpringSecurity提供的过滤器链完成的,因此,过滤器链是SpringSecurity的核心,那么SpringSecurity是如何玩弄过滤器的呢?
这个问题,我们需要从引入SpringSecurity依赖开始讲起。
环境依赖:
代码语言:javascript复制 <parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
SpringSecurity的自动配置流程
SecurityAutoConfiguration是SpringSecurity自动配置的起点,
代码语言:javascript复制package org.springframework.boot.autoconfigure.security.servlet;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
//虽然不是核心,但是事件机制解耦是一种非常的好的设计思路
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
PS: 通过事件发布与订阅机制进行解耦是一种非常好的手段。
在自动配置类的核心在于通过@Import注解,导入到容器中的三个配置类:
代码语言:javascript复制@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
- SpringBootWebSecurityConfiguration负责在容器中不存在WebSecurityConfigurerAdapter的前提下,向容器中注入一个默认的WebSecurityConfigurerAdapter
(核心)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {
//默认实现
@Configuration(proxyBeanMethods = false)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
}
}
- SecurityDataConfiguration提供的SecurityEvaluationContextExtension,能够通过SPEL为经过身份验证的用户提供数据查询
(非核心,先了解)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SecurityEvaluationContextExtension.class)
public class SecurityDataConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
- WebSecurityEnablerConfiguration配置类中添加了@EnableWebSecurity注解,该注解是核心
(核心)
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
}
- EnableWebSecurity通过@Import注解又直接导入了三个配置类,并且通过@EnableGlobalAuthentication又间接导入了一个配置类,这些配置类都是核心
(核心)
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}
- OAuth2ImportSelector判断当前环境是否是OAuth2,如果是则导入相关配置
(非核心)
- SpringWebMvcImportSelector如果当前环境是Spring mvc环境,则引入相关配置
(非核心)
- WebSecurityConfiguration配置WebSecurity
(核心)
- EnableGlobalAuthentication用来开启全局配置
(与本文核心关联不大,但是对理解SpringSecurity全局配置非常重要)
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
//AuthenticationConfiguration就是负责SpringSecurity中的全局配置的
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}
SpringSecurity的过滤器链是个什么样子
在继续往下分析之前,我们有必要先来用语言加图画的形式,先将SpringSecurity整体过滤器链架构设计展现出来。
Spring MVC体系下的Filter,其实还是servlet下的Filter玩法,我们通过在web.xml中声明好相关的filter配置,或者在servlet 3.0时代后,通过@Filter注解标注对应的实现类;
但是,对于Spring来说,这些原生的Filter并不受其控制,如果我们的Filter在实现期间需要某些服务的支持,尤其是当前Spring MVC应用的WebApplicationContext中的某些服务的支持,我们不得不采用某种过度耦合的绑定机制或者查找方式来获取这些服务的支持。
为了能够让Filter的实现更加无拘无束,尽情享用依赖注入所带来的乐趣,Spring MVC引入DelegatingFilterProxy以改变Filter的现状。
顾名思义,DelegatingFilterProxy的作用是作为一个Filter的Proxy对象,当真正需要执行拦截操作的时候,它将把具体的工作委派给它所对应的一个Filter委派对象。
在物理结构上,DelegatingFilterProxy位于web.xml中承担Filter的原始使命,而它所委派的那个Filter对象,也就是做实际工作的那个家伙,却可以置身于WebApplicationContext中,充分享受Spring的IoC容器所提供的各项服 务。
DelegatingFilterProxy更多细节实现,请翻阅本文
那么,上面所讲的这些和SpringSecurity有和关联呢?
我们知道,SpringSecurity的核心就是它的过滤器链,并且过滤器链中这些Filter实现,或多或少都需要ApplicationContext的支持,那么我们就需要利用上面所讲的DelegatingFilterProxy,将原生过滤器链中拦截到的请求,借助于DelegatingFilterProxy之手,转发给SpringSecurity自己的过滤器链中,当SpringSecurity自己的过滤器链执行完毕后,再回到原生过滤器链中继续执行。
DelegatingFilterProxy会将拦截到的请求,转发个内部从IOC中获取到的Filter实现,在我们导入了SpringSecurity依赖后,会通过AbstractSecurityWebApplicationInitializer类,借助于ServletContext,往对应的DispathcerServlet中添加一个名为springSecurityFilterChain的DelegatingFilterProxy实现类。
代码语言:javascript复制public abstract class AbstractSecurityWebApplicationInitializer
implements WebApplicationInitializer {
...
public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";
...
private void insertSpringSecurityFilterChain(ServletContext servletContext) {
//要插入到Dispatchservlet中的DelegatingFilterProxy对应的filterName
String filterName = DEFAULT_FILTER_NAME;
DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(
filterName);
String contextAttribute = getWebApplicationContextAttribute();
if (contextAttribute != null) {
springSecurityFilterChain.setContextAttribute(contextAttribute);
}
//插入过滤器
registerFilter(servletContext, true, filterName, springSecurityFilterChain);
}
...
private void registerFilter(ServletContext servletContext,
boolean insertBeforeOtherFilters, String filterName, Filter filter) {
Dynamic registration = servletContext.addFilter(filterName, filter);
...
}
我们知道DelegatingFilterProxy会根据自身的FilterName,去IOC中寻找同名的Filter实现,作为将要被代理的Filter, 而往容器中注入名为springSecurityFilterChain的任务就由WebSecurityConfiguration完成了:
代码语言:javascript复制 //注入bean的名字为springSecurityFilterChain
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
而这里由WebSecurityConfiguration注入到容器中的Filter实现,就是SpringSecurity所提供的过滤器链实现,由此,就将原生过滤器链与SpringSecurity提供的过滤器链串接在了一起,形成了下面这幅图的样子:
FilterChainProxy
从上面我们可以知道,webSecurity.build()最终会构建出SpringSecurity相关的过滤器链集合注入到容器中,那么上面图中所画的FilterChainProxy又是怎么肥事呢?
webSecurity的performBuild方法中,最终完成了过滤器链中的构建:
代码语言:javascript复制 @Override
protected Filter performBuild() throws Exception {
..
int chainSize = ignoredRequests.size() securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
//我们可以继承WebSecurityConfigurerAdapter重写configure(WebSecurity web)方法
//然后通过web.ignoring()方法来设置哪些请求是无须被SpringSecurity提供的过滤器链集合拦截的
for (RequestMatcher ignoredRequest : ignoredRequests) {
//对于每一个被不被SpringSecurity过滤器链集合拦截的请求,都会包装为一个单独的过滤链加入
//SpringSecurity过滤器链集合中
//这里只添加了RequesuMathcer,而没有给当前过滤器链中添加任何过滤器,即filter.size==0
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
//一个SecurityBuilder负责构建出一个过滤器链,然后构建出的过滤链会加入过滤器链集合中去
//一般只会存在一个
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
//SpringSecurity提供的过滤器链代理对象
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
...
Filter result = filterChainProxy;
..
return result;
}
代码语言:javascript复制SecurityBuilder和SecurityConfigure对于新手来说会特别难理解,建议可以阅读深入浅出Spring Security第四章,本文篇幅有限,不展开讲述,就默认各位都已经掌握上述知识了。
public class FilterChainProxy extends GenericFilterBean {
private List<SecurityFilterChain> filterChains;
...
FilterChainProxy最重要的一点在于其内部维护的filterChains集合,从这里可以看出SpringSecurity可以同时存在多条过滤器链集合,那么,DelegatingFilterProxy拦截到请求转发给FilterChainProxy后,FilterChainProxy又该如何决定把请求转发给内部的哪一条过滤器链进行处理呢?。
代码语言:javascript复制 public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
//从过滤器链集合中挑选出一个适合当前请求的,然后将该过滤器链内部所有Filters拿到并返回
List<Filter> filters = getFilters(fwRequest);
//如果当前请求关联的过滤器集合为空的话,那么说明当前请求无需被拦截
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
(filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
//跳回到原生的过滤器链继续执行
chain.doFilter(fwRequest, fwResponse);
return;
}
//构建一条虚拟过滤器链,来执行当前请求关联的过滤器集合,执行完后,还是跳回到原生的过滤器链继续执行
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
代码语言:javascript复制 private List<Filter> getFilters(HttpServletRequest request) {
//遍历过滤器链集合,调用每个过滤器链中提供的ReuqestMathcer进行请求匹配,判断是否拦截当前请求
//第一个匹配上,就直接返回
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
联系上文,对于通过web.ignore配置的忽略请求而言,每个请求都会包装为一个DefaultSecurityFilterChain率先加入FilterChainProxy的过滤链集合中去,但是对应的DefaultSecurityFilterChain中却没有任何一个Filter,这样在被忽略的请求被FilterChainProxy拦截时,发现得到的filters.size==0,就知道当前请求需要被放行了。
VirtualFilterChain
VirtualFilterChain负责将需要应用到当前请求上的filters集合再包装为一个VirtualFilterChain,然后挨个执行每个Filter:
代码语言:javascript复制 private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain;
private final List<Filter> additionalFilters;
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0;
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
//该跳回到原生的过滤器链中了
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
" reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition ;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
" at position " currentPosition " of " size
" in additional filter chain; firing Filter: '"
nextFilter.getClass().getSimpleName() "'");
}
nextFilter.doFilter(request, response, this);
}
}
}