1. Spring Security 6.x 一文快速搞懂配置原理-Spring专区论坛-技术-SpringForAll社区

1. Spring Security 6.x 一文快速搞懂配置原理

一、基本概念
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的能力等等
  • 2bc838ae3120240420204301

二、基本流程
接下来,重点分析一下AbstractConfiguredSecurityBuilder类,整个构建过程围绕doBuild方法,主要分为:
  1. 初始化,包括beforeInit和init方法,其中beforeInit是扩展用的钩子方法,默认实现为空
  2. 配置,包括beforeConfigure和configure方法,其中beforeConfigure是扩展用的钩子方法,默认实现为空
  3. 构造,即performBuild方法,具体由HttpSecurity实现,主要是对Filter进行排序,并最终返回DefaultSecurityFilterChain的实例
而在AbstractConfiguredSecurityBuilder中维护了一个Map>对象,用于缓存各种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
SessionManagementFilterConcurrentSessionFilterDisableEncodeUrlFilter(默认生效)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
Saml2LogoutRequestFilterSaml2LogoutResponseFilterSaml2RelyingPartyInitiatedLogoutFilter
用于实现SAML 2.0认证协议登出逻辑
18
oauth2Login
OAuth2AuthorizationRequestRedirectFilter
用于接入OAuth2.0认证协议登录逻辑,例如Github登录
19
oidcLogout
OidcBackChannelLogoutFilter
用于实现OIDC认证协议登出逻辑
20
oauth2Client
OAuth2AuthorizationRequestRedirectFilterOAuth2AuthorizationCodeGrantFilter
用于实现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
至此整个过滤器链的构建就完成了。

f3ccdd27d220240420204240

 
请登录后发表评论

    没有回复内容