接上一篇,我们已经熟悉了Spring Security的基本使用,并且实操了几个范例。
本文我们将在这几个范例的基础上进行调试,并深入研究其在源码层面上的实现原理。只有知其然,知其所以然,才能在Spring Security的使用上更加得心应手。
本文主要参考了官方文档9-11章节,在文档基础上进行丰富、拓展和提炼。
Spring Security初始化
SecurityAutoConfiguration
按照Spring Boot Starter的套路,一定有一个Configuration提供一些默认的Bean注入。而在Spring Security中,SecurityAutoConfiguration承担着该角色:
1 | java复制代码@Configuration(proxyBeanMethods = false) |
重点关注导入了WebSecurityEnablerConfiguration.class :
1 | java复制代码/** |
从注释可以看出,WebSecurityEnablerConfiguration的意义是注入一个名为”springSecurityFilterChain”的bean。但如果用户已经指定了同名的bean,则这里就不注入。
再来关注下注解 @EnableWebSecurity
1 | java复制代码/** |
注释里讲得很清楚,这个注解@EnableWebSecurity是为了向实现WebSecurityConfigurer或者是继承 WebSecurityConfigurerAdapter的实例暴露Spring Security的配置API入口。API入口分为三类:
1 | java复制代码public void configure(WebSecurity web) throws Exception {//...} |
所以用户只要通过实现WebSecurityConfigurer接口(或者继承WebSecurityConfigurerAdapter)覆盖configure方法中的若干个,即可实现自定义登录安全逻辑。正如我们在上一篇文章所写的第一个样例:
1 | java复制代码public class GeneralSecurityConfiguration extends WebSecurityConfigurerAdapter { |
EnableWebSecurity
我们主要关注EnableWebSecurity注解注入了WebSecurityConfiguration.class以及引入了另外一个注解@EnableGlobalAuthentication。我们逐个来看看都做了哪些操作。
先来看看WebSecurityConfiguration的核心逻辑:
1 | java复制代码public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { |
以上删减了大部分代码,只关心重要的两件事情:
- 第一件事,在方法setFilterChainProxySecurityConfigurer上注入用户自定义的WebSecurityConfigurers,并新建了一个WebSecurity,并将用户的configurers放进了WebSecurity中。这个步骤是通过@Autowire注入的,要比内部的@Bean的优先级更高。
- 第二件事,调用WebSecurity的build方法,生成并注入了一个名为”springSecurityFilterChain”的bean。这个bean是Spring Security的核心逻辑,后文会详细分析;
可以从build方法debug进去看看WebSecurity注入用户自定义配置的过程,这个不在本文的讨论范围之内,有时间再写一篇有关文章。
DelegatingFilterProxy
Spring Security会自动注册一个DelegatingFilterProxy到Servlet的过滤链中:
1 | java复制代码@Configuration(proxyBeanMethods = false) |
DelegatingFilterProxyRegistrationBean,顾名思义,就是为了注册DelegatingFilterProxy而存在的。getFilter 方法用来获取需要注册过滤器,方法里新建了一个DelegatingFilterProxy对象,入参包含了this.targetBeanName,也就是”springSecurityFilterChain” :
1 | java复制代码@Override |
注册完毕后的过滤链是这样子的:
DelegatingFilterProxy 实现了Filter接口,并且也是一个代理类。所以它会将doFilter方法的调用透传给内部的被代理对象 Filter delegate 。此外,还能实现被代理对象的懒加载:
1 | java复制代码@Override |
FilterChainProxy && Securityfilterchain
DelegatingFilterProxy内部的被代理对象delegate其实是一个FilterChainProxy。流程图可以更新为:
但为什么delegate不是SecurityFilterChain?那是因为WebSecurity在doBuild中,又给它包了一层代理:
1 | java复制代码@Override |
值得注意的是FilterChainProxy也是一个特殊的Filter,而且可以看出它是支持对多个securityFilterChain进行代理(详见下一节)!!!FilterChainProxy#doFilterInternal里会按顺序找到第一个满足条件的securityFilterChain,并构建一个VirtualFilterChain:
1 | java复制代码private void doFilterInternal(ServletRequest request, ServletResponse response, |
而在VirtualFilterChain内部会按顺序触发该securityFilterChain内部的所有过滤器:
1 | java复制代码private static class VirtualFilterChain implements FilterChain { |
所以,流程图更新为:
为什么使用FilterChainProxy?主要是有三点好处:
- 使用它作为过滤链的起点,可以方便排查故障的时候,将它作为debug入口。
- 使用它做一些公共操作。比如清空线程内的SecurityContext,避免内存泄漏;又比如使用HttpFirewall过滤部分类型的攻击请求。
- 使用它可以支持多个securityFilterChain,不同的securityFilterChain匹配不同的URL。这样可以提供更多的灵活性。如下图所示:
Security Filters
SecurityFilterChain 内部会包含多个Security Filters。这些 Security Filters都是用户通过configure注册,并通过WebSecurity的build过程生成的。
常见的一些Filter顺序如下:
- ChannelProcessingFilter
- ConcurrentSessionFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- CsrfFilter
- LogoutFilter
- OAuth2AuthorizationRequestRedirectFilter
- Saml2WebSsoAuthenticationRequestFilter
- X509AuthenticationFilter
- AbstractPreAuthenticatedProcessingFilter
- CasAuthenticationFilter
- OAuth2LoginAuthenticationFilter
- Saml2WebSsoAuthenticationFilter
- UsernamePasswordAuthenticationFilter
- ConcurrentSessionFilter
- OpenIDAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- DigestAuthenticationFilter
- BearerTokenAuthenticationFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- OAuth2AuthorizationCodeGrantFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- SwitchUserFilter
具体会用到哪些Filter,需要结合业务分析。本文只分析其中最关键的几个Filter(已加粗显示)。
例如在表单登录例子里,用到的filter如下图:
ExceptionTranslationFilter
ExceptionTranslationFilter是非常重要的过滤器之一,它主要功能是负责异常拦截,而且核心是对AccessDeniedException 和 AuthenticationException 捕获处理。
比如,所有第一次访问 xxxx.com/index.html 的未登录用户,都应该被重定向到登录页 xxxx.com/login.html 进行鉴定。
这个功能点实现就需要依赖ExceptionTranslationFilter对AuthenticationException 的处理。我们来看源码:
1 | java复制代码private void handleSpringSecurityException(HttpServletRequest request, |
这段代码的逻辑非常清晰,注释已经清楚这里不再赘言。而sendStartAuthentication方法再展开看看是这样的:
1 | java复制代码protected void sendStartAuthentication(HttpServletRequest request, |
用一张图总结以上流程:
因此,用户可以自定义自己的异常跳转页面,例如:
1 | java复制代码// 自定义异常跳转页面 |
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter 是专门用来处理登录请求的过滤器。默认情况下,它只处理POST /login请求:
1 | java复制代码public UsernamePasswordAuthenticationFilter() { |
请求过滤是在父类实现的:
1 | java复制代码public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) |
UsernamePasswordAuthenticationFilter 实现了父类的身份鉴定逻辑抽象方法:
1 | java复制代码public Authentication attemptAuthentication(HttpServletRequest request, |
可见,UsernamePasswordAuthenticationFilter只是个框架,真正的鉴定逻辑都是在AuthenticationManager中。
再回到父类看看鉴定成功后的处理逻辑,因为这逻辑为后文埋了伏笔:
1 | java复制代码protected void successfulAuthentication(HttpServletRequest request, |
注意这里在上下文保存了鉴定结果。
RememberMeAuthenticationFilter
上边我们着重强调上下文空间会保存用户的鉴定信息,但是当用户关闭浏览器之后一段时间,session会自动超时销毁。那么上下文空间保存的鉴定信息自然丢失,此时用户就需要重新登录鉴定。
所以,常见一些安全风险不大的网页,会提供一个”记住我”的按钮。只要用户在一定时间内(比如24h)重新打开该网站,都为用户自动登录。RememberMeAuthenticationFilter的存在正是为了实现这个功能。
既然RememberMeAuthenticationFilter是个Filter,那么按照惯例,我们直接来看其doFilter方法:
1 | java复制代码public class RememberMeAuthenticationFilter extends GenericFilterBean implements |
从注释可以看出,doFilter里也是非常骨架的代码。核心的逻辑需要看看AuthenticationManager和RemeberMeService,这部分内容放在后文。请继续往下。
鉴定
SecurityContextHolder && SecurityContext
SecurityContextHolder是Spring Security的核心模型,用来保存通过鉴定的用户信息。
默认情况下,用户信息保存在线程空间内(ThreadLocal)。可以通过以下方法保存和取出用户信息:
1 | java复制代码// 取出鉴定信息 |
Authentication
Authentication内部包含是三部分:
- principal : 保存用户信息,当通过用户和密码进行鉴定的时候,通常是一个UserDetails实例;
- credentials :保存密码,通常当用户通过鉴定后,该字段会被清空以免密码泄露;
- authorities :保存用户权限,通常是角色或者范围等较高抽象等级的权限,GrantedAuthority的实现类型;
Authentication有两个应用情景:
- 表示还没被鉴定的用户的信息。它作为AuthenticationManager的入参,这时候isAuthenticated()方法返回false;
- 表示已经被鉴定的用户信息。可以通过SecurityContext获取到当前用户的鉴定信息。
GrantedAuthority
GrantedAuthority是抽象程度较高的权限,一般是角色或者范围等级别。比方说可以是 ROLE_ADMINISTRATOR 或者 ROLE_HR_SUPERVISOR。这种级别的权限后续可以应用到URL授权或者方法访问授权上。但不建议用于更细粒度的权限控制。
AuthenticationManager && ProviderManager
AuthenticationManager是Spring Security的身份鉴定入口。通常的流程是Spring Security的Filter调用AuthenticationManager的API进行身份鉴定后获得一个用户鉴定信息(Authentication),然后将该 Authentication保存在线程空间(SecurityContextHolder)中。
而ProviderManager是最常用的一个AuthenticationManager实现。它将委托若干AuthenticationProvider进行身份鉴定。每一个AuthenticationProvider都有机会对该用户信息鉴定,如果鉴定不通过则交给下一个Provider。如果所有的Provider都不支持该Authentication的鉴定,则会抛出一个ProviderNotFoundException。
实际中每一个AuthenticationProvider只处理特定类型的authentication。例如,UsernamePasswordAuthenticationToken只能够鉴定用户密码类型的信息:
1 | java复制代码public boolean supports(Class<?> authentication) { |
这样子的设计使得我们可以通过定制不同的AuthenticationProvider来支持多种鉴定方式。
ProviderManager还支持指定一个可选的父AuthenticationManager,以应对所有的Provider都不支持该Authentication的鉴定的情景。父AuthenticationManager通常也是一个ProviderManager。
代码中也清晰体现了以上逻辑:
1 | java复制代码public Authentication authenticate(Authentication authentication) |
另外多个ProviderManager共享同一个父AuthenticationManager也是可行的,常常用于多个 SecurityFilterChain 对象 拥有部分公共的鉴定逻辑的情景。
AuthenticationProvider
如上所言,多个AuthenticationProvider会被注入到ProviderManager。但是单个AuthenticationProvider只能处理特定类型的鉴定信息。比如,DaoAuthenticationProvider支持包含用户名密码的鉴定信息,而JwtAuthenticationProvider 支持包含JWT token的鉴定信息。
AuthenticationEntryPoint
上文我们已经多次看见AuthenticationEntryPoint,一般它的作用是当发现来自未鉴定用户请求的时候进行重定向到登录页。
AbstractAuthenticationProcessingFilter
前文在介绍UsernamePasswordAuthenticationFilter的时候,其实已经介绍过AbstractAuthenticationProcessingFilter。它其实是用来鉴定用户身份信息的一个抽象Filter。用户可以继承该抽象基类,实现自定义的身份鉴定Filter,比如:UsernamePasswordAuthenticationFilter。通过分析该抽象基类源码,可以得到以下执行流程图:
总结流程如下:
- 1.当用户提交身份鉴定信息,AbstractAuthenticationProcessingFilter会从HttpServletRequest提供的信息构造一个Authentication。这个步骤是在 attemptAuthentication 抽象方法中完成的,也就是由AbstractAuthenticationProcessingFilter的实现类来实现。例如,UsernamePasswordAuthenticationFilter 构造了一个包含用户名和密码的UsernamePasswordAuthenticationToken作为身份鉴定信息;
- 2.接下来这个Authentication会被传递到AuthenticationManager进行身份鉴定;
- 3.如果鉴定失败:
- 清空 SecurityContextHolder;
- 调用RememberMeServices.loginFail;如果没有注册RememberMeServices,则忽略;
- 调用AuthenticationFailureHandler;
- 4.如果鉴定成功:
- 通知SessionAuthenticationStrategy有一个新的登录请求;
- 保存Authentication到SecurityContextHolder。稍后SecurityContextPersistenceFilter会保存SecurityContext到HttpSession中;
- 调用RememberMeServices.loginSuccess;如果没有注册RememberMeServices,则忽略;
- ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。
从代码里也能一看端倪:
1 | java复制代码public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) |
UserDetails
UserDetails与Authentication的内部结构非常相似,读者需要注意区分。上文提到Authentication的两处应用场景,而UserDetails则只有唯一的应用场景,它是由 UserDetailsService返回的数据结构,携带着用户信息,包括用户名、密码(一般是加密后)、权限角色等。
UserDetailsService
先来看看接口定义:
1 | java复制代码/** |
从注释可以看出,这个接口主要是提供给DaoAuthenticationProvider加载用户信息的,返回数据结构正是上文的UserDetails。当然这个接口还应用在了AbstractRememberMeServices等地方。
用户可以自定义自己UserDetailsService:
1 | java复制代码@Bean |
PasswordEncoder
PasswordEncoder是用来对密码进行加解密的接口。Spring Security提供了非常多实现,可以看看 PasswordEncoderFactories类。
特别注意的是,DelegatingPasswordEncoder还支持根据密文前缀来动态选择加解密算法。
DaoAuthenticationProvider
如果用户没有指定AuthenticationProvider的话,Spring Security默认会以DaoAuthenticationProvider作为兜底。
DaoAuthenticationProvider使用UserDetailsService以及UserDetailsService来鉴定用户的身份信息。流程图大概如下:
- 1.Authentication Filter (比如UsernamePasswordAuthenticationFilter)构造一个
包含用户名和密码的UsernamePasswordAuthenticationToken,并传递给AuthenticationManager(通常是ProviderManager实现); - 2.ProviderManager会使用AuthenticationProvider列表去鉴定用户身份,列表里就可以有DaoAuthenticationProvider;
- 3.DaoAuthenticationProvider 通过 UserDetailsService 加载用户信息 UserDetails;
- 4.DaoAuthenticationProvider使用 PasswordEncoder 验证传入的用户身份信息和UserDetails是否吻合;
- 5.当身份鉴定成功,会返回一个包含UserDetails和Authorities的UsernamePasswordAuthenticationToken实例。最后这个token会在Authentication Filter里被保存到SecurityContextHolder中。
Basic Authentication
Spring Securtiy原生支持Basic HTTP Authentication。
这种认证方式不常用,而且分析思路和上文一致,因此,可以参考下官方文档10.10.2. Basic Authentication
Digest Authentication
同上,不常用而且分析思路一致,详见官方文档 10.10.3. Digest Authentication
LDAP Authentication
LDAP的接入例子我们在上一篇《Spring Security 学习之使用篇》已经学习过使用方式,现在回顾下:
1 | java复制代码@Profile("ldap") |
与之前不一样的是,这里LDAP需要自定义一个 LdapAuthenticationProvider ,用来取代兜底的DaoAuthenticationProvider。
这给我们一个启发,可以通过自定义AuthenticationProvider,实现更加丰富多样的身份鉴定逻辑。
RememberServices
RememberServices用来实现”记住我”功能,原理是从用户请求的Cookie中解析token信息,然后和本地缓存的token(或者根据用户密码生成的token)作比较,比较成功则为用户自动登录。
1 | java复制代码@Override |
Spring Security提供了两个常用实现:PersistentTokenBasedRememberMeServices 和 TokenBasedRememberMeServices。前者支持将cookieToken持久化,而后者则是从UserDetailsService获取用户信息并生成cookieToken。
总结
本文详细介绍了Spring Security实现原理。
首先,谈到Spring Security对Sevlet Filter的应用,并在Filter的基础上拓展出DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain以及Security Filters。我们在实际生产中遇到这种责任链模式的时候,也可以参考这个丰富且灵活的案例。
然后,我们又深入了解了Spring Security的鉴定模块原理,掌握了各个内部组件的实现细节。
通过深入学习原理,我们可以大大提高对Spring Security的掌握程度,真正做到融会贯通、得心应手。
参考资料
- 官方文档9-11章节:非常清晰的文档;
- 杜瓦尔的博客:我的个人博客地址,可以去看看更多内容;
本文转载自: 掘金