Spring Security 认证与授权流程

前言

只要是权限校验框架,就离不开两个步骤:认证(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。

步骤简述:

  1. 请求进入UsernamePasswordAuthenticationFilter.doFilter()方法
  2. 在doFilter()方法中,先判断请求URI是否为POST loginProcessingUrl,是则继续,否则跳过认证,直接进入Security过滤器链中下一个Filter。loginProcessingUrl表示登录提交的URI,可自定义,默认值为”/login”
  3. 判断为“是”,doFilter()调用AuthenticationManager.authenticate()。默认调用到AuthenticationManager的实现类ProviderManager
  4. 在ProviderManager.authenticate()中,调用AuthenticationProvider.authenticate()方法执行真正的认证逻辑。默认调用到AuthenticationProvider的实现类DaoAuthenticationProvider
  5. 在AuthenticationProvider.authenticate()中,根据请求中的用户名参数”username”,调用UserDetailsService.loadUserByUsername(),该方法返回UserDetails对象,表示数据源中存储的用户信息
  6. AuthenticationProvider获得UserDetails对象后,校验此UserDetails对象,校验依据是UserDetails接口的几个boolean方法,如isEnabled()。说明一下,因为此UserDetails对象是用请求中的用户名查找出的,所以校验此对象状态也就是对请求中的用户名进行认证
  7. 若在“第5步”中UserDetailsService.loadUserByUsername()没有找到UserDetails,或在“第6步”中校验UserDetails对象不通过,则说明请求中的用户名有误。这两个方法会抛出AuthenticationException子类,说明认证失败。这个异常向上传递最终由UsernamePasswordAuthenticationFilter捕获处理。UsernamePasswordAuthenticationFilter捕获异常后,调用AuthenticationFailureHandler.onAuthenticationFailure()进行后续处理,默认调用到实现类SimpleUrlAuthenticationFailureHandler,作用是返回403状态码,或者在设置了failureUrl时跳转到此URL。之后不会再经过其他Filter
  8. 若认证成功,则AuthenticationProvider生成通过认证的Authentication对象(含用户名、密码、权限等),并返回到AuthenticationManager.authenticate(),再向上传递到UsernamePasswordAuthenticationFilter.doFilter()
  9. doFilter()获得Authentication对象后,调用SecurityContextHolder.getContext().setAuthentication()将Authentication对象赋予给当前的SecurityContext
  10. 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
2
3
4
5
6
7
8
9
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}

它的4个boolean方法,就是DaoAuthenticationProvider对请求进行认证的依据。Spring默认使用的实现类是User

getAuthorities()方法返回一个Collection<GrantedAuthority>对象,GrantedAuthority是个接口,一般使用它的实现类SimpleGrantedAuthority

1
2
3
4
public interface GrantedAuthority extends Serializable {
// 返回表示权限的String
String getAuthority();
}

自定义实现思路

  1. 自定义AuthenticationProvider.authenticate(),实现自己的认证逻辑,如校验密码是否正确
  2. 自定义UserDetailsService.loadUserByUsername(),实现自己的查找原用户信息的方式,如从数据库查找
  3. 自定义UserDetails,这是原用户信息对象,也是认证成功后被保存在SecurityContext的对象,可以自定义需要的属性,如用户所在组、岗位等
  4. 自定义会进入认证逻辑的POST loginProcessingUrl,如果你的登录表单action不是”/login”就需要设置
  5. 自定义登录页地址
  6. 自定义认证成功和失败后的处理,AuthenticationSuccessHandlerAuthenticationFailureHandler

以上自定义可以实现基本的认证需求,更高级的如RememberMe、单点登录等这里没有列出,需要查阅其他资料。

自定义实现示例

配置示例(只写出与认证有关的部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin() // 这行下面的语句都是在设置UsernamePasswordAuthenticationFilter的属性
.loginPage("/login") // 登录页
.loginProcessingUrl("/login/submit") // 登录提交URI,访问此URI的请求会进入认证逻辑
.successHandler(new LoginSuccessHandler()) // 自定义AuthenticationSuccessHandler
.failureHandler(new LoginFailureHandler()) // 自定义AuthenticationFailureHandler
.and()
// ...
;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 给AuthenticationManager设置自定义AuthenticationProvider
auth.authenticationProvider(customAuthenticationProvider);
}

// ...
}

自定义AuthenticationProvider示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

@Autowired
@Qualifier("securityUserService")
UserDetailsService userDetailsService; // 自定义UserDetailsService

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
Object password = (String) authentication.getCredentials();
// SecurityUser是自定义的UserDetails
SecurityUser userDetails = (SecurityUser) userDetailsService.loadUserByUsername(username);
if (userDetails == null) {
throw new BadCredentialsException("账号不存在");
}
if (!userDetails.getPassword().equals(password)) {
throw new BadCredentialsException("账号密码错误");
}
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}

@Override
public boolean supports(Class<?> authentication) {
return true;
}
}

授权

在认证流程中,只有当请求URI是设置的登录提交URI loginProcessingUrl时,才会进入UsernamePasswordAuthenticationFilter的认证逻辑,且不论认证结果如何都会直接返回,不会进入授权流程。是其他URI时,就会跳过认证,直接进入授权流程

Spring Security的授权工作主要由FilterSecurityInterceptor完成,它也是个Filter实现类。它是Security过滤器链中位置倒数第二的Filter,若进入了它的授权逻辑,授权成功则走出Security过滤器链,进入下一步;授权失败则请求结束,返回响应体或跳转页面。

  1. 请求进入FilterSecurityInterceptor拦截器,调用其父类方法super.beforeInvocation(fi),继续调用FilterInvocationSecurityMetadataSource.getAttributes()获取被拦截URI所需的权限字符串集合。Security内置了一些权限字符串,如“permitAll”
  2. FilterInvocationSecurityMetadataSource.getAttributes()方法返回NULL,则FilterSecurityInterceptor执行完毕,请求走出Security过滤器链。因此可以认为返回NULL的URI是无需权限访问的
  3. 被拦截URI的权限不为NULL,则FilterSecurityInterceptor通过SecurityContext.getAuthentication()取得当前用户的权限信息,再调用授权管理器AccessDecisionManager.decide()方法决定是否允许用户访问此URI。这个方法由AccessDecisionManager子类实现,Spring已实现的decide策略有AffirmativeBased一票肯定,UnanimousBased一票否定,ConsensusBased少数服从多数。默认使用AffirmativeBased
  4. 若用户无权访问,AccessDecisionManager.decide()抛出AccessDeniedException异常,这个异常会被ExceptionTranslationFilter处理,这个后面再讲
  5. AccessDecisionManager.decide()顺利执行完毕,表示用户有权访问,即授权成功,请求走出Security过滤器链

若未登录用户直接访问URI,AccessDecisionManager会怎么判断?
这个要看URI的权限,如果URI的权限是“permitAll”,则未登录用户也可以访问。具体看AffirmativeBased类的源码。

另外,请求走出Security过滤器链后,不一定就直接进入Controller,因为走完Security过滤器链之后可能还有过滤器。这部分内容见“过滤器链”一节。

授权成功流程:

授权失败流程:

AccessDecisionManager和FilterInvocationSecurityMetadataSource

接口源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface AccessDecisionManager {

/**
* authentication是当前用户信息,里面包含用户拥有的权限
* object是被拦截URI
* configAttributes是被拦截URI对应的权限
*/
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;

// 返回true表示此类失效
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}

decide()方法参数介绍:

  1. authentication是从SecurityContext中拿到的当前用户信息,里面包含用户拥有的权限
  2. object代表被拦截URI
  3. configAttributesFilterInvocationSecurityMetadataSource.getAttributes()返回的,代表被拦截URI所需权限

FilterInvocationSecurityMetadataSource.getAttributes()源码:

1
Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;

getAttributes()方法返回一个Collection<ConfigAttribute>对象,ConfigAttribute是个接口,一般使用它的实现类SecurityConfig

1
2
3
4
public interface ConfigAttribute extends Serializable {
// 返回表示权限的String
String getAttribute();
}

这个Collection<ConfigAttribute>是不是和认证流程中提到的Collection<GrantedAuthority>很像?它们都是权限字符串的集合,区别在于前者是被拦截URI的权限集合,后者是用户拥有的权限集合。

ExceptionTranslationFilter

ExceptionTranslationFilter在Security过滤器链中放在FilterSecurityInterceptor前一位,但它可以捕获过滤器链中,甚至过滤器链之后的,任一个Filter抛出的异常,这其中就包括FilterSecurityInterceptor抛出的异常。这是因为,它在doFilter方法中,把chain.doFilter(request, response);整个try-catch了。

虽然它捕获了过滤器链中所有类型的异常,但它只处理两种异常:AuthenticationExceptionAccessDeniedException,其它的异常它会继续抛出。

若异常为AuthenticationException,它会调用AuthenticationEntryPoint.commence()处理。一般的做法是重定向到登录页。若异常为AccessDeniedException,它分两种情况,若用户未登录,它调用AuthenticationEntryPoint.commence()处理;若用户已登录但无权限,它调用AccessDeniedHandler.handle()处理。

值得一提的是,在认证过程中抛出的AuthenticationExceptionUsernamePasswordAuthenticationFilter自己处理后返回,不会进入ExceptionTranslationFilter。即只有授权时抛出的异常会由它处理

自定义实现思路

  1. 自定义AccessDecisionManager.decide(),实现自己的授权逻辑,比如直接判断用户权限是否包含此URI,而不是用Security提供的投票方式
  2. 自定义FilterInvocationSecurityMetadataSource.getAttributes(),实现自己的查找URI所需权限的方式,如从数据库查找
  3. 自定义ExceptionTranslationFilter处理异常的方法,AuthenticationEntryPoint.commence()AccessDeniedHandler.handle()

以上自定义可以实现基本的授权需求,更高级的需要查阅其他资料。

自定义实现示例

配置示例(只写出与授权有关的部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated() // 所有请求都需要认证授权
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { // 设置FilterSecurityInterceptor属性
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setAccessDecisionManager(new CustomAccessDecisionManager()); // 自定义AccessDecisionManager
fsi.setSecurityMetadataSource(new CustomFilterInvocationSecurityMetadataSource()); // 自定义FilterInvocationSecurityMetadataSource
return fsi;
}
})
.and()
.exceptionHandling() // 以下两行设置ExceptionTranslationFilter属性
.accessDeniedHandler(new CustomAccessDeniedHandler()) // 自定义AccessDeniedHandler
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()) // 自定义AuthenticationEntryPoint
.and()
// ...
;
}

// ...
}

自定义AccessDecisionManager示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CustomAccessDecisionManager implements AccessDecisionManager {

// authentication是从spring的全局缓存SecurityContextHolder中拿到的,里面是用户的权限信息(权限编码)
// object是被拦截URI
// configAttributes是被拦截URI对应的权限(权限编码)
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
System.out.println("=========================AccessDecisionManager==============================");
if (CollectionUtils.isEmpty(configAttributes)) { // 被拦截URI所需权限为空,直接返回
return;
}

Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 简单的判断用户权限中是否包含URI权限
for (ConfigAttribute attr : configAttributes) {
String attribute = attr.getAttribute();
for (GrantedAuthority authority : authorities) {
String authority2 = authority.getAuthority();
if (authority2.equals(attribute)) {
return;
}
}
}
throw new AccessDeniedException("无权访问");
}

@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

过滤器链

在Security配置类WebSecurityConfigurerAdapter的初始化方法中,会给HttpSecurity设置一些过滤器。这个动作在执行我们重写的configure()方法之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected final HttpSecurity getHttp() throws Exception {
if (!disableDefaults) { // 启用默认配置
http
.csrf().and() // 4.CsrfFilter
.addFilter(new WebAsyncManagerIntegrationFilter()) // 1.WebAsyncManagerIntegrationFilter
.exceptionHandling().and() //11.ExceptionTranslationFilter
.headers().and() // 3.HeaderWriterFilter
.sessionManagement().and() // 10.SessionManagementFilter
.securityContext().and() // 2.SecurityContextPersistenceFilter
.requestCache().and() //7.RequestCacheAwareFilter
.anonymous().and() // 9.AnonymousAuthenticationFilter
.servletApi().and() // 8.SecurityContextHolderAwareRequestFilter
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout(); // 5.LogoutFilter
}
// 其他略...
}

上面展示的部分源码,作用主要是注册了10个Filter,数字代表它们在过滤器链中的顺序。这些过滤器的顺序是在FilterComparator类中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
FilterComparator() {
int order = 100;
put(ChannelProcessingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(WebAsyncManagerIntegrationFilter.class, order);
order += STEP;
put(SecurityContextPersistenceFilter.class, order);
order += STEP;
put(HeaderWriterFilter.class, order);
// 太多了,其余略...
}

那在我们什么配置都没有的情况下,UsernamePasswordAuthenticationFilter是在哪里注册并起作用的?
在默认的WebSecurityConfigurerAdapter.configure()方法:

1
2
3
4
5
6
7
8
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and() // UsernamePasswordAuthenticationFilter
.httpBasic();
}

一行formLogin(),工作包括初始化UsernamePasswordAuthenticationFilter,并设置其成员变量requiresAuthenticationRequestMatcher=POST /login,这个成员变量就代表登录提交URI loginProcessingUrl,即要进入认证流程的URI。

注意:Spring Security的这一整套过滤器,是以一个过滤器的身份被添加到ApplicationFilterChain中。ApplicationFilterChain就是Tomcat整理的过滤器链。这“一个过滤器”名叫“SpringSecurityFilterChain”。若我们自定义Filter没有添加到Security过滤器链中,这个Filter会排在Security过滤器链之后,即执行完Security所有过滤器后才会执行到这个Filter。

像下面这个自定义Filter就是没有加入到Security过滤器链中,只是加入了ApplicationFilterChain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// TODO Auto-generated method stub
chain.doFilter(request, response);
}

@Override
public void destroy() {
// TODO Auto-generated method stub
}
}

如何把自定义Filter添加到Security过滤器链中?下面说明。

添加自定义过滤器

想在Security的过滤器链中的某个位置加入自定义过滤器,可以使用HttpSecurity的3种方法:

  1. addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
  2. addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)
  3. 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
2
3
4
5
6
7
http
.authorizeRequests()
.antMatchers("/favicon.ico").permitAll() // 1
.and()
.formLogin()
.permitAll()// 2
// ...

第一个permitAll是ExpressionUrlAuthorizationConfigurer.AuthorizedUrl.permitAll(),第二个是FormLoginConfigurer<HttpSecurity>.permitAll()。它们的作用都是,把这些指定的URI的权限字符串设置为“permitAll”,表示允许任何用户访问这些指定的URI,即这些URI无需权限访问,即使用户未登录。

第一个指定URI是自己添加的,第二个指定的URI是认证过程中会用到的loginPage、loginProcessingUrl、failureUrl这三个URI,它们有默认值。

无论使用哪个permitAll,这些URI最后会被添加到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class AbstractConfigAttributeRequestMatcherRegistry<C> extends
AbstractRequestMatcherRegistry<C> {

// URI和对应权限保存在此变量
private List<UrlMapping> urlMappings = new ArrayList<>();

static final class UrlMapping {
private RequestMatcher requestMatcher; // URI
private Collection<ConfigAttribute> configAttrs; // URI对应权限

UrlMapping(RequestMatcher requestMatcher, Collection<ConfigAttribute> configAttrs) {
this.requestMatcher = requestMatcher;
this.configAttrs = configAttrs;
}

public RequestMatcher getRequestMatcher() {
return requestMatcher;
}

public Collection<ConfigAttribute> getConfigAttrs() {
return configAttrs;
}
}

// ...
}

这些URI保存后,在创建ExpressionBasedFilterInvocationSecurityMetadataSource时会被取出。这个类是Spring在授权时默认使用的FilterInvocationSecurityMetadataSource实现类。

被取出时的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class AbstractConfigAttributeRequestMatcherRegistry<C> extends
AbstractRequestMatcherRegistry<C> {

final LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> createRequestMap() {

LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>();
for (UrlMapping mapping : getUrlMappings()) {
RequestMatcher matcher = mapping.getRequestMatcher();
Collection<ConfigAttribute> configAttrs = mapping.getConfigAttrs();
requestMap.put(matcher, configAttrs);
}
return requestMap;
}

// ...
}

取出后被封装为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
2
3
4
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/webjars/**", "/img/**", "/js/**");
}

这些URI会跳过Security的过滤器链,但不会跳过在Security过滤器链之后的Filter。

Security保存登录用户信息原理

在“授权”过程中我们提到,FilterSecurityInterceptor 通过 SecurityContext.getAuthentication() 取得当前用户的权限信息来做访问权限校验。那么在用户登录后的每次请求中,SecurityContext 如何准确获取该用户的认证信息?

首先说明, 在一次请求线程中,SecurityContext 整个对象都是是通过SecurityContextHolder来存取的。在默认配置下,SecurityContextHolder 用ThreadLocal<SecurityContext> contextHolder存储 SecurityContext 对象,使得该对象在一次请求中都有效且无线程安全问题。这个存储方式可配置修改,也可自定义,具体见 SecurityContextHolder 类源码。

那么请求结束后,线程销毁,SecurityContextHolder 如何在用户下一次请求时还能取得该对象?答案在 Security 过滤器 SecurityContextPersistenceFilter 中,这个过滤器在过滤器链中排第二。

SecurityContextPersistenceFilter 部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class SecurityContextPersistenceFilter extends GenericFilterBean {

private SecurityContextRepository repo;

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

if (request.getAttribute(FILTER_APPLIED) != null) { // 这是为了控制该过滤器只走一次
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}

final boolean debug = logger.isDebugEnabled();

request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

if (forceEagerSessionCreation) { // 这个值默认false,起个预读取session的作用
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}

HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 这一步就是从session中取SecurityContext对象,如果取不到会创建一个空对象
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

try {
// 把从session中取的SecurityContext对象保存到SecurityContextHolder中
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 执行后面的过滤器
chain.doFilter(holder.getRequest(), holder.getResponse());
}
// 后面所有过滤器执行完且程序也处理完请求后,回到这里
finally {
// 这次请求过后的SecurityContext对象
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 清除threadLocal
SecurityContextHolder.clearContext();
// 把新的SecurityContext对象保存到session
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());

request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}

// ...
}

从这个过滤器中可以看到,Security在每次请求过后,把用户的 SecurityContext 对象保存在 HttpSession 中。下一次用户请求中会带着Cookie:JSESSIONID=xxxx过来,Security 就能找到这个用户的 SecurityContext 对象了。

如果用户禁用了Cookie,要怎么实现这个功能呢?
用户禁用了cookie也不妨碍我们使用session,关键是要拿到JSESSIONID。网上有教程介绍禁用cookie继续使用session的方法,一种方法就是把JSESSIONID放在每个URL之后。