一、基本概念
Spring Security框架看似比较复杂,但说到底,框架中的各种安全功能,基本上也就是一个个Filter(javax.servlet.Filter)组成的所谓“过滤器链”实现的,这些Filter以职责链的设计模式组织起来,环环相扣,不过在刚接触Spring Security框架时不必盯着每个Filter着重去研究,我们首要的目的是学会如何对Spring Security进行配置,很多人,特别是新手,在看过官方文档中配置示例代码(如下所示)之后,在没有足够背景知识的情况下,都会对这个http.build()方法感到莫名的困惑,想要定制开发也不知从何下手,本文主要对整个Spring Security配置过程做一定的剖析,希望可以对学习Spring Sercurity框架的同学所有帮助。
版本说明:下文所贴出的各段源码均源自6.2.3版本,但其实5.7以上的各个版本,跟配置相关的代码基本相同,变动不算太大
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的能力等等
二、基本流程
接下来,重点分析一下AbstractConfiguredSecurityBuilder类,整个构建过程围绕doBuild方法,主要分为:
- 初始化,包括beforeInit和init方法,其中beforeInit是扩展用的钩子方法,默认实现为空
- 配置,包括beforeConfigure和configure方法,其中beforeConfigure是扩展用的钩子方法,默认实现为空
- 构造,即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找到相关的实现,源码如下
HTTPSECURITY_BEAN_NAME) ("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官方文档中配置的示例代码为例,配置代码只需几行,比较优雅,这种设计是值得学习的,尽量让复杂的配置逻辑封装起来,让开发者在使用时,只需要关注业务逻辑即可
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版本引入的,在这之前是使用无参的方法获取配置对象 ,然后进行链式的配置,如上述示例代码可以改写为
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http. authorizeHttpRequests().anyRequest().authenticated() .and().formLogin() .and().httpBasic(); return http.build(); }
以authorizeHttpRequests为例看一下源码实现,可以看到两种方式大同小异,只是使用了Customer函数式接口进行了封装
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用于封装认证失败后执行的操作等等
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的配置过程,分析它们有哪些配置项,这些配置点主要能提供什么样能力的等,从而打开思路,快速实现不同的定制需求。
四、总结
最后做一个简单的总结,如图所示:
- 从HttpSercurityConfiguration定义HttpSecurity的Bean对象开始,便向HttpSecurity中添加了若干SecurityConfigurer对象,另外我们可以在自定义的配置类中对其进行一些定制调整
- 然后当调用HttpSecurity#Build()方法时,就会将取得所有SecurityConfigurer进行遍历,依次调用对应的init和configurer方法,而在configurer方法中,创建出各种功能的Filter实例,并添加到List列表中
- 最后通过performBuild方法,将List进行排序,并创建出DefaultFilterChian至此整个过滤器链的构建就完成了。