spring security最常见的应用场景还是基于http请求和servlet API的web应用,接下来的几个章节我们将重点探讨下spring security是如何在web应用中实现认证和访问控制的。通过前面章节的讲述,我们已经了解到,在web应用中spring是通过各种各样的filter来做认证和鉴权,本小节就主要讨论下spring中过滤器的几个主要组件。
1.DelegatingFilterProxy
为了使Filter起作用,我们必须明确的在web.xml中声明,或者通过servlet3.0+中追加的方法ServletContext.addFilter方法追加到servlet中,否则这个filter是会被servlet容器忽略的。在spring security中,我们的filter也是一个spring bean,也需要用到依赖注入功能来装配,而我们知道,正常情况下servlet context的创建是在application context之前的,如果我们直接把我们的filter注册到servlet中,filter中依赖注入的对象将获取不到,为了解决这个问题,spring引入了DelegatingFilterProxy类,他提供了一servlet context和application context之间的一种链接。
在web.xml中使用DelegatingFilterProxy,通常采用下面的配置代码段
<filter> <filter-name>myFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>myFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
相应的java config代码段如下:
Filter filter = new DelegatingFilterProxy("myFilter"); servletContext.addFilter("myFilter", filter) .addMappingForUrlPatterns( EnumSet.of(DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST), false, "/*");
可以看到,我们实际上配置的Filter是DelegatingFilterProxy,并不是我们实际实现了Filter接口的类,当有请求满足DelegatingFilterProxy的UriPatterns时,DelegatingFilterProxy类会首先判断实际的Filter类是否已经配置过,如果没有将根据配置的bean名称从spring context中获取对应的bean,然后将处理委托给我们真实的Filter类。这样我们真实的Filter类就可以利用spring bean生命周期方法以及灵活的依赖注入配置,需要注意的是我们的Filter类注入到spring context中的bean名称必须和传入到DelegatingFilterProxy中的名字一致。DelegatingFilterProxy中处理请求的代码段如下:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Lazily initialize the delegate if necessary. Filter delegateToUse = this.delegate; if (delegateToUse == null) { synchronized (this.delegateMonitor) { if (this.delegate == null) { WebApplicationContext wac = findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: " + "no ContextLoaderListener or DispatcherServlet registered?"); } this.delegate = initDelegate(wac); } delegateToUse = this.delegate; } } // Let the delegate perform the actual doFilter operation. invokeDelegate(delegateToUse, request, response, filterChain); } ...... protected void invokeDelegate( Filter delegate, ServletRequest request, ServletResponse response,FilterChain filterChain) throws ServletException, IOException { delegate.doFilter(request, response, filterChain); }
2.FilterChainProxy
在spring security我们通过DelegatingFilterProxy注册到servlet context中并不是一个个单独的filter,而是FilterChainProxy,一个FilterChainProxy就是一系列过滤器链的代理,里面包含很多个的过滤器链(过滤器链在spring中的统一接口是SecurityFilterChain,每一个SecurityFilterChain中都包含一个排好序的过滤器列表),当FilterChainProxy接受到DelegatingFilterProxy委托过来的请求时,就从这些过滤器链中查找到第一个满足当前请求的过滤器链,并获取这个过滤器链中的过滤器列表,然后依次执行获取到的过滤器列表。虽然理论上我们是能够通过DelegatingFilterProxy将我们需要的filter一个个都配置到web.xml或者通过servletContext.addFilter方法追加到servlet中,但是随着我们filter的追加这种方式会变的难以维护,并且这样一来filter的执行顺序将严格依赖我们追加的顺序,缺少灵活性。下面是一个例子
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy"> <constructor-arg> <list> <sec:filter-chain pattern="/restful/**" filters=" securityContextPersistenceFilterWithASCFalse, basicAuthenticationFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> <sec:filter-chain pattern="/**" filters=" securityContextPersistenceFilterWithASCTrue, formLoginFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> </list> </constructor-arg> </bean>
在FilterChainProxy中的多个过滤器链中,我们是用第一个匹配当前请求的过滤器链对请求进行filter操作,所以越具体的URI应配置的越靠前
3.下面我们来看看在spring security中是如何把我们需要的filter注册到servlet中的
3.1首先是WebSecurity类的performBuild方法,
这个方法会把我们追加到ignoredRequests(通过ignoring()方法配置)和配置好的securityFilterChainBuilders(实际就是HttpSecurity)分别配置成SecurityFilterChain,代码片段如下:
@Override protected Filter performBuild() throws Exception { ... int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>( chainSize); for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); ...
3.2配置好的FilterChainProxy返回后会在WebSecurityConfiguration中被注册到spring context,bean名称为springSecurityFilterChain
/** * Creates the Spring Security Filter Chain * @return * @throws Exception */ @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(); }
如果我们有继承WebSecurityConfigurerAdapter的配置类,就会用我们自己的。
AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME的具体值就是springSecurityFilterChain
public abstract class AbstractSecurityWebApplicationInitializer implements WebApplicationInitializer { ... public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain"; ...
3.3 通过DelegatingFilterProxy把名称为springSecurityFilterChain的bean注册到servlet中,在spring boot工程中这个是通过SecurityFilterAutoConfiguration这个自动注册类来实现的
private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME; @Bean @ConditionalOnBean(name = DEFAULT_FILTER_NAME) public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration( SecurityProperties securityProperties) { DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean( DEFAULT_FILTER_NAME); registration.setOrder(securityProperties.getFilterOrder()); registration.setDispatcherTypes(getDispatcherTypes(securityProperties)); return registration; }
可以看到,当我们的context中存在名称为springSecurityFilterChain的bean后,spring boot将自动为我们注册一个DelegatingFilterProxyRegistrationBean,并将springSecurityFilterChain传递进去,DelegatingFilterProxyRegistrationBean这个bean的继承关系为
DelegatingFilterProxyRegistrationBean ->AbstractFilterRegistrationBean->RegistrationBean->ServletContextInitializer,所以在servlet启动时会出发onStartup方法,在这个方法内部,具体实现在AbstractFilterRegistrationBean类中
@Override public void onStartup(ServletContext servletContext) throws ServletException { Filter filter = getFilter(); Assert.notNull(filter, "Filter must not be null"); String name = getOrDeduceName(filter); if (!isEnabled()) { this.logger.info("Filter " + name + " was not registered (disabled)"); return; } FilterRegistration.Dynamic added = servletContext.addFilter(name, filter); if (added == null) { this.logger.info("Filter " + name + " was not registered " + "(possibly already registered?)"); return; } configure(added); }
可以看到,这里会获取一个filter并注册到servletContext中,具体的getFilter方法在DelegatingFilterProxyRegistrationBean中
@Override public Filter getFilter() { return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) { @Override protected void initFilterBean() throws ServletException { // Don't initialize filter bean on init() } }; }
很明显,是用了一个DelegatingFilterProxy将我们传入的bean名称进行了包装。至此spring security中用到的filter就被以DelegatingFilterProxy的形式注册到了servlet。
具体我们想用的filter如 UsernamePasswordAuthenticationFilter、SecurityContextPersistenceFilter等是如何追加到HttpSecurity中,从而在3.1步骤中被加入到SecurityFilterChain中的,我们在具体讨论每一个filter时再继续讲解
4.绕过filter
在实际应用开发中,对于某些资源的访问,我们可能不想使用spring security进行保护,例如一些静态的js、html等,下面分别给出用xml和java config两种配置形式实现这个功能的例子
xml配置中,我们可以将对应URI的filter设置成none,如下
<http pattern="/static/**" security="none"/> <http pattern="/resources/**" security="none"/>
对应的java config
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/resources/**", "/static/**"); }
通过3.1小节,我们知道通过web.ignoring()配置的过滤规则,会被配置成DefaultSecurityFilterChain追加到过滤器链的列表的最前面,并且在配置DefaultSecurityFilterChain时只传入了RequestMatcher,没有传入任何filter,DefaultSecurityFilterChain构造函数如下,
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) { this(requestMatcher, Arrays.asList(filters)); }
表示对于满足条件的uri不适用任何过滤器来处理,和xml中security="none"效果一样。
另外需要注意的一点是,采用这种配置后,任何满足条件的URI将不被spring security的过滤器处理,此时SecurityContext中将不包含任何用户信息。