一、基本概念

Spring Security框架看似比较复杂,但说到底,框架中的各种安全功能,基本上也就是一个个Filter(javax.servlet.Filter)组成的所谓“过滤器链”实现的,这些Filter以职责链的设计模式组织起来,环环相扣,不过在刚接触Spring Security框架时不必盯着每个Filter着重去研究,我们首要的目的是学会如何对Spring Security进行配置,很多人,特别是新手,在看过官方文档中配置示例代码(如下所示)之后,在没有足够背景知识的情况下,都会对这个http.build()方法感到莫名的困惑,想要定制开发也不知从何下手,本文主要对整个Spring Security配置过程做一定的剖析,希望可以对学习Spring Sercurity框架的同学所有帮助。

版本说明:下文所贴出的各段源码均源自6.2.3版本,但其实5.7以上的各个版本,跟配置相关的代码基本相同,变动不算太大
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .httpBasic(withDefaults());
    return http.build();
}

概况地说,HttpSecurity的配置过程,主要就是向这个SecurityFilterChian中添加不同功能的Filter对象,为了方便后文理解,首先来看一下其中涉及的几个重要的接口和类(关系如下图)接口

  • SecurityBuilder:顶层接口,定义了抽象的泛型构造器方法——build()
  • SecurityConfigurer:顶层接口,用来定义配置类的通用方法,每个Filter都是由特定的SecurityConfigurer的实现类构建出来并添加到FilterChain中的
  • SecurityFilterChain:顶层接口,即过滤器链,定义了获取List的方法,以及matches,用于判断某个请求是否满足进链的条件
  • HttpSecurityBuilder:继承SecurityBuilder,定义了构建SecurityFilterChain过程中的各种辅助方法,如添加Filter到SecurityFilterChain,获取SecurityConfigurer配置实现类等
  • AbstractSecurityBuilder:顶层的抽象父类,它没有实现build具体的逻辑,实际交由doBuild方法实现,只是用CAS对doBuild过程进行了并发控制
  • AbstractConfiguredSecurityBuilder:继承了AbstractSecurityBuilder,它内部维护了一个SecurityConfigurer的列表,实现了doBuild方法,确立了整个构建的流程
  • HttpSecurity 作为final实现类,它主要面向开发者,我们在开发过程中就是用它提供的一系列的配置入口,方便开发者对SecurityFilterChain中不同的Filter进行定制,包括添加自定义的Filter,关闭某些Filter,或扩展原来Filter的能力等等
0

二、基本流程

接下来,重点分析一下AbstractConfiguredSecurityBuilder类,整个构建过程围绕doBuild方法,主要分为:

  1. 初始化,包括beforeInit和init方法,其中beforeInit是扩展用的钩子方法,默认实现为空
  2. 配置,包括beforeConfigure和configure方法,其中beforeConfigure是扩展用的钩子方法,默认实现为空
  3. 构造,即performBuild方法,具体由HttpSecurity实现,主要是对Filter进行排序,并最终返回DefaultSecurityFilterChain的实例

而在AbstractConfiguredSecurityBuilder中维护了一个Map<Class,List<SecurityConfigure>>对象,用于缓存各种SecurityConfigure的实现类,当调用init和configure时,实际上就会遍历这个Map所有configurer,依次调用对应方法,通常就是在configure方法中,将Filter加入到FilterChain中(下文详述)

protected final O doBuild() throws Exception {
    synchronized (this.configurers) {
       this.buildState = BuildState.INITIALIZING;
       beforeInit();
       init();
       this.buildState = BuildState.CONFIGURING;
       beforeConfigure();
       configure();
       this.buildState = BuildState.BUILDING;
       O result = performBuild();
       this.buildState = BuildState.BUILT;
       return result;
    }
}
 
 private void configure() throws Exception {
    Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
    for (SecurityConfigurer<O, B> configurer : configurers) {
       configurer.configure((B) this);
    }
}

那么,这些SecurityConfigure实例则是如何添加上述的Map中的?可以在HttpSecurityConfiguration找到相关的实现,源码如下

@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
    LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
    AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
          this.objectPostProcessor, passwordEncoder);
    authenticationBuilder.parentAuthenticationManager(authenticationManager());
    authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
    HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
    webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
    // @formatter:off
    http
       .csrf(withDefaults())
       .addFilter(webAsyncManagerIntegrationFilter)
       .exceptionHandling(withDefaults())
       .headers(withDefaults())
       .sessionManagement(withDefaults())
       .securityContext(withDefaults())
       .requestCache(withDefaults())
       .anonymous(withDefaults())
       .servletApi(withDefaults())
       .apply(new DefaultLoginPageConfigurer<>());
    http.logout(withDefaults());
    // @formatter:on
    applyCorsIfAvailable(http);
    applyDefaultConfigurers(http);
    return http;
}

可以看到在构造过程中(那段很长链式配置),已经帮我们添加了若干SecurityConfgurer实例,因此我们在使用配置SecurityFilterChain时,仅需要很少的配置就可以得到一个完整的具备基本功能的SecurityFilterChain,当然我们也可以利用这些配置项做很多定制开发。事实上,HttpSecurity大约提供了24个Filter相关的Configurer配置方法,其中11个Filter是默认加载的,整理成表格:

序号
方法
对应Filter
作用
1
headers
HeaderWriterFilter(默认加载)
用于添加Security HTTP header到response中,例如X-Frame-Options
2
cors
CorsFilter(通常在Spring MVC环境时默认加载)
用于支持跨域请求
3
sessionManagement
SessionManagementFilter
ConcurrentSessionFilter
DisableEncodeUrlFilter(默认生效)
ForceEagerSessionCreationFilter
用于管理session,如保存session,并发控制等操作
4
jee
J2eePreAuthenticatedProcessingFilter
用于支持Java EE 容器预认证
5
x509
X509AuthenticationFilter
用于支持X.509证书预认证,通常是指在浏览器中使用HTTPS协议
6
rememberMe
RememberMeAuthenticationFilter
用于在登录时记录用户登录信息,以保持登录态
7
authorizeHttpRequests
AuthorizationFilter
用于实现授权访问的逻辑
8
requestCache
RequestCacheAwareFilter(默认加载)
用于实现用户登录之后,跳转回登录之前请求的地址
9
exceptionHanding
ExceptionTranslationFilter(默认加载)
用于配置异常处理
10
securityContext
SecurityContextHolderFilter(默认加载)
SecurityContextPersistenceFilter(旧版本,现不推荐使用)
用于加载用户的登录态信息,并保存在SecurityContextHolder中
11
servletApi
SecurityContextHolderAwareRequestFilter(默认加载)
用于将HttpServletRequest包装为Servlet3SecurityContextHolderAwareRequestWrapper,方便其他Filter使用
12
csrf
CsrfFilter(默认加载)
用于防范跨站请求伪造(CSRF)攻击
13
logout
LogoutFilter(默认加载)
用于实现登出注销逻辑
14
anonymous
AnonymousAuthenticationFilter(默认加载)
用于实现匿名登录逻辑
15
formLogin
UsernamePasswordAuthenticationFilter
用于实现用户名密码登录逻辑
16
saml2Login
Saml2WebSsoAuthenticationFilter
用于实现SAML 2.0认证协议登录逻辑
17
saml2Logout
Saml2LogoutRequestFilter
Saml2LogoutResponseFilter
Saml2RelyingPartyInitiatedLogoutFilter
用于实现SAML 2.0认证协议登出逻辑
18
oauth2Login
OAuth2AuthorizationRequestRedirectFilter
用于接入OAuth2.0认证协议登录逻辑,例如Github登录
19
oidcLogout
OidcBackChannelLogoutFilter
用于实现OIDC认证协议登出逻辑
20
oauth2Client
OAuth2AuthorizationRequestRedirectFilter
OAuth2AuthorizationCodeGrantFilter
用于实现OAuth2.0客户端逻辑,例如授权码模式等
21
oauth2ResourceServer
BearerTokenAuthenticationFilter
用于实现OAuth2.0服务端逻辑
22
requiresChannel
ChannelProcessingFilter
用于判断哪些资源需要被保护
23
httpBasic
BasicAuthenticationFilter
用于实现HTTP基础认证的逻辑
24
passwordManagement
RequestMatcherRedirectFilter("/change-password")
用于实现在需要修改密码时,跳转页面的逻辑
25
HttpSecurityConfiguraion直接添加
WebAsyncManagerIntegrationFilter(默认加载)
用于在异步线程中,支持通过SecurityContextHolder获取认证对象Authentication

如果我们不对HttpSecurity做任何改动的话,默认得到的SecurityFilterChain是如下这样的,先了解大概,后续还针对部分重要的filter做深入分析。

org.springframework.security.web.session.DisableEncodeUrlFilter
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextHolderFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.web.filter.CorsFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.access.ExceptionTranslationFilter

三、SecurityConfigurer举例

这里还是以Spring Security官方文档中配置的示例代码为例,配置代码只需几行,比较优雅,这种设计是值得学习的,尽量让复杂的配置逻辑封装起来,让开发者在使用时,只需要关注业务逻辑即可

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .httpBasic(withDefaults());
    return http.build();
}

Spring Security提供了两种方式进行配置,一种就是示例代码中,即利用lambda表达式实现配置逻辑,这是5.5版本引入的,在这之前是使用无参的方法获取配置对象 ,然后进行链式的配置,如上述示例代码可以改写为

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.
            authorizeHttpRequests().anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
    return http.build();

}

以authorizeHttpRequests为例看一下源码实现,可以看到两种方式大同小异,只是使用了Customer函数式接口进行了封装

@Deprecated(since = "6.1", forRemoval = true)
public AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizeHttpRequests()
       throws Exception {
    ApplicationContext context = getContext();
    return getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry();
}

public HttpSecurity authorizeHttpRequests(
       Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer)
       throws Exception {
    ApplicationContext context = getContext();
    authorizeHttpRequestsCustomizer
       .customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry());
    return HttpSecurity.this;
}

其中getOrApply方法,用于获取到Configurer的具体实例(上文中提到过在AbstractConfiguredSecurityBuilder中维护了一个Configurers的Map,这些Configurer实例便是从这个Map获取的)

不过第二种写法,根据源码的注释,应该会在Spring Security 7版本里面移除,所以还是要适应这种Customizer参数的配置方法。

上面authorizeHttpRequests方法返回的是AuthorizeHttpRequestsConfigurer类中的AuthorizationManagerRequestMatcherRegistry对象,可以先看一下AuthorizeHttpRequestsConfigurer中的configure方法

public void configure(H http) {
    AuthorizationManager<HttpServletRequest> authorizationManager = this.registry.createAuthorizationManager();
    AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager);
    authorizationFilter.setAuthorizationEventPublisher(this.publisher);
    authorizationFilter.setShouldFilterAllDispatcherTypes(this.registry.shouldFilterAllDispatcherTypes);
    authorizationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
    http.addFilter(postProcess(authorizationFilter));
}

这里创建了一个AuthorizationFilter,并添加到HttpSecurity的List中,而AuthorizationManagerRequestMatcherRegistry则又是一个构造器模式实现的配置类,主要功能就是配置一些权限拦截的具体逻辑,如哪些地址需要什么角色访问等,这里就不展开了。

再看一下formLogin的例子

public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
    formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
    return HttpSecurity.this;
}

formLogin方法实际上创建FormLoginConfigurer的示例,该类主要用于创建UsernamePasswordAuthenticationFilter,即默认的用户名密码认证的过滤器。

其中的configure方法由父类AbstractAuthenticationFilterConfigurer实现,源码如下,虽然这个方法有点长,但基本是围绕配置UsernamePasswordAuthenticationFilter实例而展开(this.authFilter就是UsernamePasswordAuthenticationFilter的实例,它在FormLoginConfigurer的构造函数中创建出来),主要就是创建用户认证所用到的一些基本组件,例如AuthenticationManager用于封装不同的用户认证方式(如用户名密码),AuthenticationSuccessHandler用于封装认证成功后执行的操作,AuthenticationFailureHandler用于封装认证失败后执行的操作等等

@Override
public void configure(B http) throws Exception {
    PortMapper portMapper = http.getSharedObject(PortMapper.class);
    if (portMapper != null) {
       this.authenticationEntryPoint.setPortMapper(portMapper);
    }
    RequestCache requestCache = http.getSharedObject(RequestCache.class);
    if (requestCache != null) {
       this.defaultSuccessHandler.setRequestCache(requestCache);
    }
    this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    this.authFilter.setAuthenticationSuccessHandler(this.successHandler);
    this.authFilter.setAuthenticationFailureHandler(this.failureHandler);
    if (this.authenticationDetailsSource != null) {
       this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
    }
    SessionAuthenticationStrategy sessionAuthenticationStrategy = http
       .getSharedObject(SessionAuthenticationStrategy.class);
    if (sessionAuthenticationStrategy != null) {
       this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
    }
    RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
    if (rememberMeServices != null) {
       this.authFilter.setRememberMeServices(rememberMeServices);
    }
    SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
    if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
       SecurityContextRepository securityContextRepository = securityContextConfigurer
          .getSecurityContextRepository();
       this.authFilter.setSecurityContextRepository(securityContextRepository);
    }
    this.authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
    F filter = postProcess(this.authFilter);
    http.addFilter(filter);
}

通过上面两个源码示例,可以看到配置Filter的过程其实并不复杂,当我们在研究Spring Security不同过滤器功能时,可以参考源码中configure的配置过程,分析它们有哪些配置项,这些配置点主要能提供什么样能力的等,从而打开思路,快速实现不同的定制需求。

四、总结

最后做一个简单的总结,如图所示:

  1. 从HttpSercurityConfiguration定义HttpSecurity的Bean对象开始,便向HttpSecurity中添加了若干SecurityConfigurer对象,另外我们可以在自定义的配置类中对其进行一些定制调整
  2. 然后当调用HttpSecurity#Build()方法时,就会将取得所有SecurityConfigurer进行遍历,依次调用对应的init和configurer方法,而在configurer方法中,创建出各种功能的Filter实例,并添加到List列表中
  3. 最后通过performBuild方法,将List进行排序,并创建出DefaultSecurityFilterChian至此整个过滤器链的构建就完成了。