前言
看了网上各种关于Spring Security原理解析的文章,大部分都是一上来就贴源码的,我个人觉得一来就贴源码是非常不好的行为,一篇好的教程,讲了什么和怎么讲是一样重要的,相信很多人都有这种经历,想简单明了的了解下这个框架的原理、作用、使用方法而搜索相关文章,结果全是大段大段的源码,我相信没几个人愿意看下去,本文主旨在于用不贴代码的方式给不了解Spring Security的读者快速理解Spring Security的工作流程,本篇并不讲Spring Security怎么使用,相信读者在了解了基本流程以后再学习怎么使用会有事半功倍的效果。本文的参考资料主要来源于Spring Security的官方文档。
必要知识
相信读到这个文章的人已经有了Spring和Servlet的相关知识,不过由于很多程序员已经面向Spring开发了太久,或者一开始就是用Spring来开发,可能已经忘记或者不了解Servlet的相关知识,作为接下来知识点的必要基础,在这复习一下Servlet的相关知识。对Servlet很了解的话可以跳过。
Servlet
简单来说Servlet就是一组接口,我们编写的类实现这组接口,当Tomcat、Jboss等实现了Servlet规范的服务器程序在运行的时候,会根据URL来加载相应的我们编写的类。早期没有Spring等框架的时候,开发Java服务器程序全是靠手写一个个实现了Servlet的类,然后在WEB-INF文件夹下的web.xml文件里将这些将这些Servlet类和对应的URL进行映射,然后将这些Servlet类和web.xml一起打成一个war包放到Tomcat等容器的webapp目录下,这样容器在运行的时候监听到有请求发过来后,就能根据事先写好的映射关系,通过反射的方式加载我们写的Servlet类然后执行里面的业务逻辑。
Filter
Filter和Servlet一样也是一组接口,实现这组接口的类也像Servlet一样,在web.xml中配置好和URL的映射关系后被打包进war包,容器会根据URL来加载对应的Filter,Filter会在Servlet之前执行,于是我们可以在这里写一些过滤和校验的代码,在请求进入Servlet之前对请求进行校验和处理。Filter的处理流程如下图所示,多个Filter构成了一条过滤链,只有通过了所有Filter请求才能进入Servlet,而Spring Security正是利用了Filter来完成各种校验功能。
正文
Spring Security的基本原理
DelegatingFilterProxy
从必要知识里我们知道了Filter的工作原理,在Spring中使用自定义的Filter有个问题那就是Filter必须在Servlet容器启动前就注册好,但是Spring使用ContextLoaderListener来加载Spring Bean,这就像先有鸡还是先有蛋的问题,于是Spring设计了一个Filter的代理DelegatingFilterProxy,在Servlet容器中注册这个代理,将过滤的工作拦截下来转发给Spring容器中的过滤器。
FilterChainProxy和SecurityFilterChain
在实际代码中Spring并没有在DelegatingFilterProxy中直接实例化和调用自定义的Filter,而是构建了一个过滤链FilterChainProxy,FilterChainProxy内部又调用了SecurityFilterChain,我们自定义的Filter会被加到SecurityFilterChain上,这一点和Tomcat的设计很像,这样做的好处是确定了一个起点,当你要排除Spring Security的bug时,在FilterChainProxy中添加调试点是一个很好的起点。
这样设计还有个好处就是在Servlet容器中,仅根据URL调用过滤器。 但是,FilterChainProxy可以利用RequestMatcher接口,根据HttpServletRequest中的任何内容确定调用,比原生的Servlet更灵活。
还有一点是FilterChainProxy可以构建多条SecurityFilterChain, 你的应用程序可以为不同的情况提供完全独立的配置,如下图所示。
不过在使用SecurityFilterChain中要注意,FilterChainProxy使用第一个匹配上的SecurityFilterChain。例如如果请求的URL是/ api / messages /,则它将首先与SecurityFilterChain0的/ api / 模式匹配,因此即使SecurityFilterChainn也能匹配上,也会仅调用SecurityFilterChain0。 如果请求的URL是/ messages /,则在SecurityFilterChain0的/ api / 模式下将不匹配。
Security Filters
Security Filters即是我们用于定义各种过滤规则的过滤器,通过SecurityFilterChain的API插入到FilterChainProxy中。过滤器的顺序很重要。各过滤器按先后顺序依次执行,以下是各过滤器的作用顺序:
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
处理异常
Spring在FilterChainProxy中自动插入了一个名为ExceptionTranslationFilter的Filter,这个Filter用来处理登录和认证异常,将AccessDeniedException和AuthenticationException转换为HTTP响应。
具体的处理流程为:
1.ExceptionTranslationFilter调用FilterChain.doFilter(request,response)来调用应用程序的其余部分。
2.如果用户未通过身份验证或它是AuthenticationException,则启动身份验证。此时会清除SecurityContextHolder,HttpServletRequest保存在RequestCache中。等用户成功进行身份验证后,将使用RequestCache重播原始请求。
3.如果它是AccessDeniedException,则拒绝访问。 调用AccessDeniedHandler来处理被拒绝的访问。
如果应用程序未引发AccessDeniedException或AuthenticationException,则ExceptionTranslationFilter不执行任何操作。
ExceptionTranslationFilter的伪代码如下所示:
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException e) {
if (!authenticated || e instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
认证方式
Spring Security是一个非常复杂且庞大的框架,涵盖了几乎所有的认证方式,包括一些已经不推荐使用的方式,想要在一篇文章里囊括所有的认证方式不太可能,即是可能,读者也很难坚持读下去,所以接下来只挑选了最常用的基于用户名和密码的认证方式来说明Spring Security的认证流程。
Spring Security提供了以下内置机制,用于从HttpServletRequest中读取用户名和密码:
- Form Login 使用表单登录方式
- Basic Authentication 使用Http Basic方式
- Digest Authentication 使用摘要身份验证方式
需要注意的是不应该在现代应用程序中使用摘要式身份验证,因为它必须以纯文本,加密或MD5格式存储密码,所有这些存储格式都被认为是不安全的,所以在这里并不写怎么通过摘要验证。
表单认证
首先我们来看如何通过表单验证,在此之前需要了解如何将用户重定向到登录表单的过程:
1.首先,用户向未经授权的资源/ private发出未经身份验证的请求。
2.Spring Security的FilterSecurityInterceptor表示拒绝访问,通过抛出AccessDeniedException拒绝了未经身份验证的请求。
3.由于未对用户进行身份验证,因此ExceptionTranslationFilter会启动“开始身份验证”,并使用配置的AuthenticationEntryPoint将重定向发送到登录页面。 在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的实例。
4.浏览器将请求将其重定向到的登录页面。
5.登录页面。
提交用户名和密码后,UsernamePasswordAuthenticationFilter会对用户名和密码进行身份验证,UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,因此此图看起来应该非常相似。
1.当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。
2.接下来将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证。 AuthenticationManager的详细信息取决于用户信息的存储方式。
3.如果身份验证失败,则认证失败,清除SecurityContextHolder,
RememberMeServices.loginFail被调用。如果RememberMe未配置,则为空,AuthenticationFailureHandler被调用。
4.如果身份验证成功,则认证成功,SessionAuthenticationStrategy被通知有新的登录,在SecurityContextHolder上设置该认证信息,RememberMeServices.loginFail被调用。如果RememberMe未配置,则为空,ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent,AuthenticationSuccessHandler被调用。通常这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到ExceptionTranslationFilter保存的请求。
默认情况下,Spring Security表单登录处于启用状态。 但是,一旦提供了任何基于servlet的配置,就必须显式提供基于表单的登录。
Http Basic认证
让我们看一下HTTP基本身份验证在Spring Security中如何工作。首先,我们看到WWW-Authenticate标头被发送回未经身份验证的客户端。
1.首先,用户向未经授权的资源/ private发出未经身份验证的请求。
2.Spring Security的FilterSecurityInterceptor表示认证失败,通过抛出AccessDeniedException拒绝了未经身份验证的请求。
3.由于用户未通过身份验证,因此ExceptionTranslationFilter会启动“开始身份验证”。配置的AuthenticationEntryPoint是BasicAuthenticationEntryPoint的实例,该实例发送WWW-Authenticate标头。RequestCache通常是一个NullRequestCache,它不保存请求,因为客户端能够重播它最初请求的请求。
当客户端收到WWW-Authenticate标头时,它知道应该使用用户名和密码重试。 以下是正在处理的用户名和密码的流程。
1当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。
2接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证。 AuthenticationManager的详细信息取决于用户信息的存储方式。
3如果身份验证失败,则认证失败,清除SecurityContextHolder,RememberMeServices.loginFail被调用。如果RememberMe未配置,则为空,调用AuthenticationEntryPoint触发WWW-Authenticate重新发送。
4如果身份验证成功,则认证成功,在SecurityContextHolder上设置认证身份信息,RememberMeServices.loginSuccess被调用。如果RememberMe未配置,则为空,BasicAuthenticationFilter调用FilterChain.doFilter(request,response)继续进行其余的应用程序逻辑。
Spring Security的HTTP基本身份验证支持默认为启用。 但是,一旦提供了任何基于servlet的配置,就必须显式提供HTTP Basic。
储存机制
读取到的用户身份信息可以使用以下几种方式进行储存:
Simple Storage with In-Memory Authentication
Relational Databases with JDBC Authentication
Custom data stores with UserDetailsService
LDAP storage with LDAP Authentication
In-Memory Authentication
Spring Security的InMemoryUserDetailsManager实现了UserDetailsService,以支持对在内存中检索到的基于用户名/密码的身份验证。 InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。 当配置为接受用户名/密码进行身份验证时,Spring Security使用基于UserDetails的身份验证。
JDBC Authentication
Spring Security的JdbcDaoImpl实现了UserDetailsService,以提供对使用JDBC检索的基于用户名/密码的身份验证的支持。 JdbcUserDetailsManager扩展了JdbcDaoImpl以通过UserDetailsManager接口提供对UserDetails的管理。 当配置为接受用户名/密码进行身份验证时,Spring Security使用基于UserDetails的身份验证。
UserDetails
UserDetails由UserDetailsService返回。 DaoAuthenticationProvider验证UserDetails,然后返回身份验证,该身份验证的主体是已配置的UserDetailsService返回的UserDetails。
UserDetailsService
DaoAuthenticationProvider使用UserDetailsService检索用户名,密码和其他用于使用用户名和密码进行身份验证的属性。Spring Security提供UserDetailsService的内存中和JDBC实现。
密码编码
Spring Security的servlet支持通过与PasswordEncoder集成来安全地存储密码。可以通过公开一个PasswordEncoder Bean来定制Spring Security使用的PasswordEncoder实现。
(一口气写了这么多,需要休息一阵了,未完待续)