前言
只要是权限校验框架,就离不开两个步骤:认证(Authentication)和授权(Authorization)。认证即“用户登录”,授权即“允许用户访问目标URI”。这篇博客介绍了Spring Security 5.0.6版本认证与授权这两个过程的执行链条,未涉及到更高级的功能(如单点登录、RememberMe)。
官方文档(Security5.0.6):https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/reference/htmlsingle/
认证
Spring Security的认证工作主要由UsernamePasswordAuthenticationFilter完成。这个Filter是默认配置的,每个请求都会被它拦截。认证成功后,会生成经过认证的用户信息对象Authentication。无论认证成功或失败,都不会继续走后面的Filter。
步骤简述:
- 请求进入
UsernamePasswordAuthenticationFilter.doFilter()方法 - 在doFilter()方法中,先判断请求URI是否为
POST loginProcessingUrl,是则继续,否则跳过认证,直接进入Security过滤器链中下一个Filter。loginProcessingUrl表示登录提交的URI,可自定义,默认值为”/login” - 判断为“是”,doFilter()调用
AuthenticationManager.authenticate()。默认调用到AuthenticationManager的实现类ProviderManager - 在ProviderManager.authenticate()中,调用
AuthenticationProvider.authenticate()方法执行真正的认证逻辑。默认调用到AuthenticationProvider的实现类DaoAuthenticationProvider - 在AuthenticationProvider.authenticate()中,根据请求中的用户名参数”username”,调用
UserDetailsService.loadUserByUsername(),该方法返回UserDetails对象,表示数据源中存储的用户信息 - AuthenticationProvider获得UserDetails对象后,校验此UserDetails对象,校验依据是UserDetails接口的几个boolean方法,如isEnabled()。说明一下,因为此UserDetails对象是用请求中的用户名查找出的,所以校验此对象状态也就是对请求中的用户名进行认证
 - 若在“第5步”中
UserDetailsService.loadUserByUsername()没有找到UserDetails,或在“第6步”中校验UserDetails对象不通过,则说明请求中的用户名有误。这两个方法会抛出AuthenticationException子类,说明认证失败。这个异常向上传递最终由UsernamePasswordAuthenticationFilter捕获处理。UsernamePasswordAuthenticationFilter捕获异常后,调用AuthenticationFailureHandler.onAuthenticationFailure()进行后续处理,默认调用到实现类SimpleUrlAuthenticationFailureHandler,作用是返回403状态码,或者在设置了failureUrl时跳转到此URL。之后不会再经过其他Filter - 若认证成功,则AuthenticationProvider生成通过认证的
Authentication对象(含用户名、密码、权限等),并返回到AuthenticationManager.authenticate(),再向上传递到UsernamePasswordAuthenticationFilter.doFilter() - doFilter()获得
Authentication对象后,调用SecurityContextHolder.getContext().setAuthentication()将Authentication对象赋予给当前的SecurityContext - doFilter()调用
AuthenticationSuccessHandler.onAuthenticationSuccess()进行后续处理,默认调用到实现类SavedRequestAwareAuthenticationSuccessHandler,作用是从request header或配置中取targetUrl并跳转。之后不会再经过其他Filter 
认证成功流程:
认证失败流程:
UserDetailsService和UserDetails
在上述“第5步”中,提到了UserDetailsService.loadUserByUsername()方法。该方法源码;
1  | UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException;  | 
该方法的作用就是查找数据源中的用户信息,AuthenticationProvider.authenticate()就是以此为依据对当前请求进行认证。根据UserDetailsService的已有实现类推测,数据源可以是JDBC数据库、内存。
该方法的返回对象UserDetails源码:
1  | public interface UserDetails extends Serializable {  | 
它的4个boolean方法,就是DaoAuthenticationProvider对请求进行认证的依据。Spring默认使用的实现类是User。
getAuthorities()方法返回一个Collection<GrantedAuthority>对象,GrantedAuthority是个接口,一般使用它的实现类SimpleGrantedAuthority。
1  | public interface GrantedAuthority extends Serializable {  | 
自定义实现思路
- 自定义
AuthenticationProvider.authenticate(),实现自己的认证逻辑,如校验密码是否正确 - 自定义
UserDetailsService.loadUserByUsername(),实现自己的查找原用户信息的方式,如从数据库查找 - 自定义
UserDetails,这是原用户信息对象,也是认证成功后被保存在SecurityContext的对象,可以自定义需要的属性,如用户所在组、岗位等 - 自定义会进入认证逻辑的
POST loginProcessingUrl,如果你的登录表单action不是”/login”就需要设置 - 自定义登录页地址
 - 自定义认证成功和失败后的处理,
AuthenticationSuccessHandler和AuthenticationFailureHandler 
以上自定义可以实现基本的认证需求,更高级的如RememberMe、单点登录等这里没有列出,需要查阅其他资料。
自定义实现示例
配置示例(只写出与认证有关的部分):
1  | 
  | 
自定义AuthenticationProvider示例:
1  | 
  | 
授权
在认证流程中,只有当请求URI是设置的登录提交URI loginProcessingUrl时,才会进入UsernamePasswordAuthenticationFilter的认证逻辑,且不论认证结果如何都会直接返回,不会进入授权流程。是其他URI时,就会跳过认证,直接进入授权流程。
Spring Security的授权工作主要由FilterSecurityInterceptor完成,它也是个Filter实现类。它是Security过滤器链中位置倒数第二的Filter,若进入了它的授权逻辑,授权成功则走出Security过滤器链,进入下一步;授权失败则请求结束,返回响应体或跳转页面。
- 请求进入
FilterSecurityInterceptor拦截器,调用其父类方法super.beforeInvocation(fi),继续调用FilterInvocationSecurityMetadataSource.getAttributes()获取被拦截URI所需的权限字符串集合。Security内置了一些权限字符串,如“permitAll” - 若
FilterInvocationSecurityMetadataSource.getAttributes()方法返回NULL,则FilterSecurityInterceptor执行完毕,请求走出Security过滤器链。因此可以认为返回NULL的URI是无需权限访问的 - 被拦截URI的权限不为NULL,则FilterSecurityInterceptor通过
SecurityContext.getAuthentication()取得当前用户的权限信息,再调用授权管理器AccessDecisionManager.decide()方法决定是否允许用户访问此URI。这个方法由AccessDecisionManager子类实现,Spring已实现的decide策略有AffirmativeBased一票肯定,UnanimousBased一票否定,ConsensusBased少数服从多数。默认使用AffirmativeBased - 若用户无权访问,AccessDecisionManager.decide()抛出
AccessDeniedException异常,这个异常会被ExceptionTranslationFilter处理,这个后面再讲 - AccessDecisionManager.decide()顺利执行完毕,表示用户有权访问,即授权成功,请求走出Security过滤器链
 
若未登录用户直接访问URI,AccessDecisionManager会怎么判断?
这个要看URI的权限,如果URI的权限是“permitAll”,则未登录用户也可以访问。具体看AffirmativeBased类的源码。
另外,请求走出Security过滤器链后,不一定就直接进入Controller,因为走完Security过滤器链之后可能还有过滤器。这部分内容见“过滤器链”一节。
授权成功流程:
授权失败流程:
AccessDecisionManager和FilterInvocationSecurityMetadataSource
接口源码:
1  | public interface AccessDecisionManager {  | 
decide()方法参数介绍:
authentication是从SecurityContext中拿到的当前用户信息,里面包含用户拥有的权限object代表被拦截URIconfigAttributes是FilterInvocationSecurityMetadataSource.getAttributes()返回的,代表被拦截URI所需权限
FilterInvocationSecurityMetadataSource.getAttributes()源码:
1  | Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;  | 
getAttributes()方法返回一个Collection<ConfigAttribute>对象,ConfigAttribute是个接口,一般使用它的实现类SecurityConfig。
1  | public interface ConfigAttribute extends Serializable {  | 
这个Collection<ConfigAttribute>是不是和认证流程中提到的Collection<GrantedAuthority>很像?它们都是权限字符串的集合,区别在于前者是被拦截URI的权限集合,后者是用户拥有的权限集合。
ExceptionTranslationFilter
ExceptionTranslationFilter在Security过滤器链中放在FilterSecurityInterceptor前一位,但它可以捕获过滤器链中,甚至过滤器链之后的,任一个Filter抛出的异常,这其中就包括FilterSecurityInterceptor抛出的异常。这是因为,它在doFilter方法中,把chain.doFilter(request, response);整个try-catch了。
虽然它捕获了过滤器链中所有类型的异常,但它只处理两种异常:AuthenticationException和AccessDeniedException,其它的异常它会继续抛出。
若异常为AuthenticationException,它会调用AuthenticationEntryPoint.commence()处理。一般的做法是重定向到登录页。若异常为AccessDeniedException,它分两种情况,若用户未登录,它调用AuthenticationEntryPoint.commence()处理;若用户已登录但无权限,它调用AccessDeniedHandler.handle()处理。
值得一提的是,在认证过程中抛出的AuthenticationException由UsernamePasswordAuthenticationFilter自己处理后返回,不会进入ExceptionTranslationFilter。即只有授权时抛出的异常会由它处理。
自定义实现思路
- 自定义
AccessDecisionManager.decide(),实现自己的授权逻辑,比如直接判断用户权限是否包含此URI,而不是用Security提供的投票方式 - 自定义
FilterInvocationSecurityMetadataSource.getAttributes(),实现自己的查找URI所需权限的方式,如从数据库查找 - 自定义ExceptionTranslationFilter处理异常的方法,
AuthenticationEntryPoint.commence()和AccessDeniedHandler.handle() 
以上自定义可以实现基本的授权需求,更高级的需要查阅其他资料。
自定义实现示例
配置示例(只写出与授权有关的部分):
1  | 
  | 
自定义AccessDecisionManager示例:
1  | public class CustomAccessDecisionManager implements AccessDecisionManager {  | 
过滤器链
在Security配置类WebSecurityConfigurerAdapter的初始化方法中,会给HttpSecurity设置一些过滤器。这个动作在执行我们重写的configure()方法之前。
1  | protected final HttpSecurity getHttp() throws Exception {  | 
上面展示的部分源码,作用主要是注册了10个Filter,数字代表它们在过滤器链中的顺序。这些过滤器的顺序是在FilterComparator类中定义:
1  | FilterComparator() {  | 
那在我们什么配置都没有的情况下,UsernamePasswordAuthenticationFilter是在哪里注册并起作用的?
在默认的WebSecurityConfigurerAdapter.configure()方法:
1  | protected void configure(HttpSecurity http) throws Exception {  | 
一行formLogin(),工作包括初始化UsernamePasswordAuthenticationFilter,并设置其成员变量requiresAuthenticationRequestMatcher=POST /login,这个成员变量就代表登录提交URI loginProcessingUrl,即要进入认证流程的URI。
注意:Spring Security的这一整套过滤器,是以一个过滤器的身份被添加到ApplicationFilterChain中。ApplicationFilterChain就是Tomcat整理的过滤器链。这“一个过滤器”名叫“SpringSecurityFilterChain”。若我们自定义Filter没有添加到Security过滤器链中,这个Filter会排在Security过滤器链之后,即执行完Security所有过滤器后才会执行到这个Filter。
像下面这个自定义Filter就是没有加入到Security过滤器链中,只是加入了ApplicationFilterChain:
1  | 
  | 
如何把自定义Filter添加到Security过滤器链中?下面说明。
添加自定义过滤器
想在Security的过滤器链中的某个位置加入自定义过滤器,可以使用HttpSecurity的3种方法:
- addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
 - addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)
 - addFilterAt(Filter filter, Class<? extends Filter> atFilter)
 
这3个方法的前一个参数就是自定义Filter对象,后一个表示自定义Filter要放在哪个默认Filter的前面,或者后面,或者就放在默认Filter所在位置。
下面这个例子表示把自定义Filter放在UsernamePasswordAuthenticationFilter的前一个位置:
1  | http.addFilterBefore(new BeforeLoginFilter(), UsernamePasswordAuthenticationFilter.class)  | 
有一个问题,若使用:
1  | http.addFilterAt(new AtLoginFilter(), UsernamePasswordAuthenticationFilter.class)  | 
AtLoginFilter是在UsernamePasswordAuthenticationFilter的前面还是后面,还是把UsernamePasswordAuthenticationFilter覆盖?
答案是addFilterAt()不会覆盖UsernamePasswordAuthenticationFilter,且在它之前。具体原因是,HttpSecurity会用一个List<Filter>保存系统中所有自定义和默认的Filter,自定义过滤器是先于大部分默认过滤器被加入到List中的。在放入的同时,自定义Filter会被FilterComparator安排一个序号,这个序号等于UsernamePasswordAuthenticationFilter的序号。Spring在初始化过滤器链时,调用HttpSecurity.performBuild()方法,方法中使用FilterComparator对List中元素进行排序,序号一致的维持原序。因此使用addFilterAt()时,自定义Filter会在默认Filter之前。
permitAll()
我在参考官方文档配置Spring Security时,看到两个permitAll()方法:
1  | http  | 
第一个permitAll是ExpressionUrlAuthorizationConfigurer.AuthorizedUrl.permitAll(),第二个是FormLoginConfigurer<HttpSecurity>.permitAll()。它们的作用都是,把这些指定的URI的权限字符串设置为“permitAll”,表示允许任何用户访问这些指定的URI,即这些URI无需权限访问,即使用户未登录。
第一个指定URI是自己添加的,第二个指定的URI是认证过程中会用到的loginPage、loginProcessingUrl、failureUrl这三个URI,它们有默认值。
无论使用哪个permitAll,这些URI最后会被添加到这里:
1  | public abstract class AbstractConfigAttributeRequestMatcherRegistry<C> extends  | 
这些URI保存后,在创建ExpressionBasedFilterInvocationSecurityMetadataSource时会被取出。这个类是Spring在授权时默认使用的FilterInvocationSecurityMetadataSource实现类。
被取出时的部分源码:
1  | public abstract class AbstractConfigAttributeRequestMatcherRegistry<C> extends  | 
取出后被封装为LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>类型,保存了原本在List中的顺序,赋值给ExpressionBasedFilterInvocationSecurityMetadataSource的成员变量requestMap。
在Spring默认配置的授权流程中,有请求访问这些URI时,只要不是会进入认证过程的POST loginProcessingUrl,就会进入FilterSecurityInterceptor。FilterSecurityInterceptor调用ExpressionBasedFilterInvocationSecurityMetadataSource.getAttribute()方法,得到这些URI的权限字符串是“permitAll”,再通过AccessDecisionManager.decide()决定此URI允许访问(默认调用AffirmativeBased)。因此无论用户是否登录、是否有权限,都可访问此URI。
由于这些permitAll()方法是在Spring默认实现的AccessDecisionManager.decide()中起作用,若我们自定义的AccessDecisionManager.decide()不会使用“permitAll”权限字符串,则可以不用设置。
WebSecurity
WebSecurity一般用来设置不想被Security拦截的静态资源URI。如下示例:
1  | 
  | 
这些URI会跳过Security的过滤器链,但不会跳过在Security过滤器链之后的Filter。
Security保存登录用户信息原理
在“授权”过程中我们提到,FilterSecurityInterceptor 通过 SecurityContext.getAuthentication() 取得当前用户的权限信息来做访问权限校验。那么在用户登录后的每次请求中,SecurityContext 如何准确获取该用户的认证信息?
首先说明, 在一次请求线程中,SecurityContext 整个对象都是是通过SecurityContextHolder来存取的。在默认配置下,SecurityContextHolder 用ThreadLocal<SecurityContext> contextHolder存储 SecurityContext 对象,使得该对象在一次请求中都有效且无线程安全问题。这个存储方式可配置修改,也可自定义,具体见 SecurityContextHolder 类源码。
那么请求结束后,线程销毁,SecurityContextHolder 如何在用户下一次请求时还能取得该对象?答案在 Security 过滤器 SecurityContextPersistenceFilter 中,这个过滤器在过滤器链中排第二。
SecurityContextPersistenceFilter 部分源码:
1  | public class SecurityContextPersistenceFilter extends GenericFilterBean {  | 
从这个过滤器中可以看到,Security在每次请求过后,把用户的 SecurityContext 对象保存在 HttpSession 中。下一次用户请求中会带着Cookie:JSESSIONID=xxxx过来,Security 就能找到这个用户的 SecurityContext 对象了。
如果用户禁用了Cookie,要怎么实现这个功能呢?
用户禁用了cookie也不妨碍我们使用session,关键是要拿到JSESSIONID。网上有教程介绍禁用cookie继续使用session的方法,一种方法就是把JSESSIONID放在每个URL之后。