Spring Security学习笔记
Spring Web Security是Java web开发领域的一个认证(Authentication)/授权(Authorisation)框架,基于Servlet技术,更确切的说是基于Servlet的Filter技术。因此,在学习Spring Web Security之前,有必要先对Servlet Filter的工作机制做个介绍。
注:Spring Security本身并不只是针对web的,但本文讲的主要是其在web开发中的使用,因此下文提到Spring Security时主要指Spring Security在web开发中那一部分。
基本原理
Servlet技术是Java web开发的底层使能性技术,也就是说Java世界的很多web开发框架都是建立在Servlet基础之上的,比如Structs和Spring MVC,前者的ActionServlet和后者的DispatcherServlet都只是标标准准的Servlet而已,并无什么特别之处。Servlet可以看成是处理web请求的基本单元,而Filter则是围绕着Servlet,用于在web请求被处理之前或者之后对web请求(Request)和应答(Response)修改,其工作机制如下图:
Servlet的Filter机制这里有三点比较重要:
- Filter即可以作用于Servlet之前、又可以作用于Servlet之后(Spring Security只用到了前者)。
- Filter在Request到达Servlet之前,可以直接将Response返回,此功能用于诸如在未登录的情况下直接向用户展示登录页面这样的功能。
- 多个Filter起作用时有先后顺序。
有了上面几点,我们自己都可以实现一套认证授权机制出来。事实上,在Spring Security没出来之前,很多开发者的确是基于Filter机制自己实现的一套安全框架。以下是一个Filter的例子:
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//1. 实现Filter逻辑,比如记录访问日志,认证,授权等
//2. 调用下一个Filter;
// 或者在没有下一个Filter的情况下,调用请求终点站——Servlet
chain.doFilter(request,response);
//3. 实现Filter逻辑,到这里Servlet处理已经完成,
// 此处主要是对处理后的Response进行处理,比如记录本次请求消耗的时间等
}
@Override
public void destroy() {
}
}
上文中提到,Filter是“围绕”着Servlet的,这里的“围绕”即是:在调用chain.doFilter(request,response);
之前和之后都可以执行Filter逻辑,只是一个在Request处理之前,一个在Request处理之后,Spring Security作为一个认证框架,只会作用在Request处理之前,这样才能起到对Servlet的保护作用。
要使Spring Security生效,从可行性上来说,我们需要有一个Spring Security的Filter能够被Servlet容器(比如Tomcat、Jetty等)感知到,这个Filter便是DelegatingFilterProxy,该Filter并不受Spring IoC容器的管理,在Servlet容器眼中,DelegatingFilterProxy只是一个Filter而已,跟其他的Servlet Filter没什么却别。
虽然DelegatingFilterProxy本身不在IoC容器中,它却能够访问到IoC容器中的其他对象,这些对象才是真正完成Spring Security逻辑的对象。这些对象中的部分对象本身也实现了javax.servlet.Filter
接口,但是他们并不能被Servlet容器感知到,比如UsernamePasswordAuthenticationFilter。
DelegatingFilterProxy只是起代理作用,其本身也不是Spring Security的一部分,而是Spring Web中的一个基础设施类。它将真正的逻辑代理给其他的被Spring IoC容器管理的对象(即Spring中的bean)。当它把逻辑代理给Spring Security bean时,便引入了Spring Security。我们完全可以使DelegatingFilterProxy将逻辑代理给自己编写的Filter bean时,这样的Filter做什么事情都可以,不见得是和Security相关的,也就是说DelegatingFilterProxy其实是一个很通用的代理类。
那么,问题来了?为什么不直接将Spring Security的Filter对象或者自己编写的Filter对象直接对Servlet容器可见呢?是因为我们想让这些Filter对象能够享受到Spring IoC容器所带来的好处。
那么,问题又来了?既然DelegatingFilterProxy是个通用的代理Filter,它是如何知道到底需要代理给哪个bean的呢?答案是:我们可以为DelegatingFilterProxy配置一个targetBeanName
字段,运行时DelegatingFilterProxy会去IoC容器中名字为该字段值的bean,并将逻辑代理给这个bean。比如,在使用Spring Security时,我们经常会在web.xml文件中做如下配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
DelegatingFilterProxy其实继承了Spring中的GenericFilterBean,GenericFilterBean(本身实现了javax.servlet.Filter
接口)可以获取到自身的名字——也就是上面代码中的springSecurityFilterChain,DelegatingFilterProxy会将该名字赋值给targetBeanName字段,然后从IoC容器中找到名字为targetBeanName值(此时也即springSecurityFilterChain)的bean,并将逻辑代理给该bean。
也就是说,这个bean的名字必须为springSecurityFilterChain。开发者通常遇到Spring Security启动时抛出异常的情况,很多时候其实就是因为所配置的DelegatingFilterProxy名字不为springSecurityFilterChain而导致的。
在Spring Security中,名字为springSecurityFilterChain的bean为一个FilterChainProxy类型的对象,该对象其实也只是起代理作用,但是跟DelegatingFilterProxy不同的是,它并不是一个Spring通用的代理对象,而是Spring Security自己的代理对象。FilterChainProxy维护了多个SecurityFilterChain,每一个SecurityFilterChain只会对特定的Request起作用(比如特定的URL),FilterChainProxy在接收到Request时,会从SecurityFilterChain列表中选出能够处理该Request的那个SecurityFilterChain对象,然后将逻辑代理给该对象。这样做的用处在于我们可以为不同的Request配置不同的认证方式,或者有些Request不用经过认证的(比如静态资源)可以配置一个空的SecurityFilterChain,即该SecurityFilterChain里面没有任何过滤逻辑。
这个SecurityFilterChain其实也不是最终完成Spring Security认证逻辑的对象,而是维护了多个Filter bean,这些Filter bean才是真正处理认证逻辑的对象。对于这些Filter bean来说,有的Filter用于处理用户名+密码登录、有些用于生成登录页面、有些用于维持用户登录状态、有的用于鉴权,根据具单一职责原则各司其职。
通常情况下,有些Filter bean在Spring Security中是必须的,Spring Security会自动为我们创建并配置这些bean,比如用于鉴权的FilterSecurityInterceptor,另外,我们也可以将自己的Filter bean将入Filter bean列表中,比如完成基于token的认证机制。另外,Filter bean的先后顺序是重要的,你总不至于在用户都还没登录就去做鉴权吧?
综上,Spring Security的处理流程图如下:
Spring Security处理流程从上图可以看到,不同的URL请求可能分配给不同的SecurityFilterChain,不同的SecurityFilterChain又可以包含不同的Filter列表,从而采用不同的认证方式。举个例子,如果哪天我们需要开发一个系统既能提供无状态的REST接口,又能提供传统Spring MVC的页面,那么可以为前者和后者分别创建各自的SecurityFilterChain,前者的SecurityFilterChain包含了处理Token认证的Filter Bean,后者的SecurityFilterChain包含基于Cookie/Session的Form表单登录的Filter Bean。Spring Security已经为我们提供了一套足够强大的认证设施,可以满足我们大部分需求。但是,Spring Security默认提供的认证设施主要针对传统的基于Cookie和Session的的Web应用,对于基于Token的认证方式需要我们自己开发Filter Bean。
关键Filter
ChannelProcessingFilter
用于将HTTP请求重定向到HTTPS页面,比如登录页面需要用户输入密码,这种带有敏感信息的页面通常需要通过HTTPS保护,如果用户访问的是HTTP,那么ChannelProcessingFilter可以自动重定向到HTTPS的登录页面。
SecurityContextPersistenceFilter
保存用户的登录状态,作用为:用户登录之后,以后的访问就不需要再登录了。默认情况下登录状态保存在HTTP Session里面。这对于传统的Web应用来说是比较常见的,但是对于某些要求无状态的应用来说,便不合适了。
UsernamePasswordAuthenticationFilter
用于处理基于Form登录的认证,认证成功重定向到指定页面,认证失败向用户重新返回登录界面并提示错误。UsernamePasswordAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,AbstractAuthenticationProcessingFilter可以配置一个AuthenticationSuccessHandler和一个AuthenticationFailureHandler,认证成功之后将调用AuthenticationSuccessHandler,比如像UsernamePasswordAuthenticationFilter一样重定向到某个页面,也可以根据自定义向用户返回一个JWT的Token;认证失败(比如用户名或密码不正确)后将调用AuthenticationFailureHandler,比如像UsernamePasswordAuthenticationFilter一样重新返回登录页面,也可以根据自定义向用户返回一个401状态码。
AbstractAuthenticationProcessingFilter并不完成认证逻辑,而是将其交给AuthenticationManager,AuthenticationManager进而代理给AuthenticationProvider,AuthenticationProvider验证用户提供的凭证是否正确(比如从数据库加载用户的密码然后与用户提供的密码对比,或者与LDAP服务器通信验证用户名和密码)。
ExceptionTranslationFilter和FilterSecurityInterceptor
这两个Filter通常是结合在一起用的,前者负责处理后者所抛出的异常并做相应的处理,后者主要用于鉴权。ExceptionTranslationFilter在处理异常时,如果异常为AuthenticationException类型,表示用户认证都失败了(比如还没有经过认证),此时将调用AuthenticationEntryPoint开启认证过程,比如向用户展示登录页面;如果异常为AccessDeniedException,表示用户可能已经登录但是没有足够的权限,此时将调用AccessDeniedHandler,比如向用户展示“你没有权限”的通知页面。
更多关于Spring Security架构方面的知识,请参考这篇Spring官网文章。