2.1 Spring security基本认证
2.1.1快速入门
在spring boot项目中使用spring security非常方便,创建一个新的spring boot项目,只需要引入web和spring security依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
然后在项目中提供一个用于测试的/hello
接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello spring security";
}
}
接下来启动项目,/hello
接口就已经被自动保护起来了。当用户访问/hello
接口时,会自动跳转到登录页面,用户登录成功后,才能访问到/hello
接口。
默认的登录用户名是user,登录密码则是一个随机生成的UUID字符串,在项目启动日志中可以看到登录密码(这也意味着项目每次启动时,密码都会发生变化)。
输入默认的用户名和密码,就可以成功登录了。这就是spring security的强大之处,只需要引入一个依赖,所有的接口就会被自动保护起来。
2.1.2 流程分析
-
客户端(浏览器)发起请求去访问
/hello
接口,这个接口默认是需要认证之后才能访问的。 -
这个请求会走一遍spring security中的过滤器链,在最后的
FilterSecurityInterceptor
过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出AccessDeniedException
异常。 -
抛出的
AccessDeniedException
异常在ExceptionTranslationFilter
过滤器中被捕获,ExceptionTranslationFilter
过滤器通过调用LoginUrlAuthenticationEntryPoint#commence
方法给客户端返回302,要求客户端重定向到/login
-
客户端发送
/login
请求。 -
/login
请求被DefaultLoginPageGeneratingFilter
过滤器拦截下来,并在该过滤器中返回登录页面。所以当用户访问/hello
接口时会首先看到登录页面。在整个过程中,相当于客户端一共发送了两个请求,第一个请求是
/hello
,服务端收到之后返回302,要求客户端重定向到/login
,于是客户端又发送了/login
请求
2.1.3原理分析
Spring security中定义了UserDetails
接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表已经固定的系统集到spring security认证体系中:
/**
* 提供核心的用户信息
*/
public interface UserDetails extends Serializable {
// 返回当前账户所具备的权限
Collection<? extends GrantedAuthority> getAuthorities();
// 返回当前账户的密码
String getPassword();
// 返回当前账户的用户名
String getUsername();
// 返回当前账户是否未过期
boolean isAccountNonExpired();
// 返回当前账户是否未锁定
boolean isAccountNonLocked();
// 返回当前账户凭证(如密码)是否未过期
boolean isCredentialsNonExpired();
// 返回当前账户是否可用
boolean isEnabled();
}
而负责提供用户数据源的接口是UserDetailsService
:
/**
* 加载用户特定数据的核心接口
*/
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

-
UserDetailsManager
在UserDetailsService
的基础上,继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在供5种方法。 -
JdbcDaoImpl
在UserDetailsService
的基础上,通过spring-jdbc实现了从数据库查询用户的方法。 -
InMemoryUserDetailsManager
实现了UserDetailsManager
中关于用户的增删改查方法,不过都是基于内存的操作,数据并没有持久化。 -
JdbcUserDetailsManager
继承自JdbcDaoImpl
同时又实现了UserDetailsManager
接口,因此可以通过JdbcUserDetailsManager
实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过JdbcUserDetailsManager
有一个局限性,就是操作数据库中用户的SQL都是提前写好的,不够灵活,因此在实际开发中使用并不多。 -
CachingUserDetailsService
的特点是会将UserDetailsService
缓存起来。 -
UserDetailsServiceDelegator
则是提供了UserDetailsService
的懒加载功能。 -
ReactiveUserDetailsServiceAdapter
是webflux-web-security模块定义的UserDetailsService
实现。
当使用spring security时,如果仅仅只是引入一个spring security依赖,则默认使用的用户就是由InMemoryUserDetailsManager提供的。
针对UserDetailsService的自动化配置类是UserDetailsServiceAutoConfiguration
2.1.3 默认页面生成
和页面相关的过滤器:DefaultLoginPageGeneratingFilter
(用来生成默认的登录页面)和DefaultLogoutPageGeneratingFilter
(用来生成默认的注销页面,通过访问/logout
接口可以看到)。
先来看DefaultLoginPageGeneratingFilter
。作为spring security过滤器链中的一员,在第一次请求/hello
接口的时候,就会经过它,但是由于/hello
接口与登录无关
因此DefaultLoginPageGeneratingFilter
过滤器并未干涉/hello
接口。等到第二次重定向到/login
页面的时候,就和DefaultLoginPageGeneratingFilter
有关系了。此时请求就会在该过滤器中进行处理,生成登录页面返回给客户端。
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
// 判断是否为登录出错请求、注销成功请求或者登录请求
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
// 如果是的话,则字符串拼接成登录页面并响应
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
// 用return方法跳出过滤器链
return;
}
// 否则,请求继续往下走,执行下一个过滤器
chain.doFilter(request, response);
}
}
DefaultLogoutPageGeneratingFilter
原理类似
2.2 登录用户数据获取
在spring security中,用户登录信息本质上还是保存在HttpSession
中,但是为了方便使用,spring security对HttpSession
中的用户信息进行了封装,封装之后,开发者若再想获取用户登录数据就会有两种不同的思路:
- 从
SecurityContextHolder
中获取。 - 从当前请求对象中获取。
无论哪种获取方式,都离不开一个重要的对象:Authentication
。在spring security中,Authentication
对象主要有两方面的功能:
- 作为
AuthenticationManager
的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的isAuthenticated
方法返回false
,表示用户还未认证。 - 代表已经经过身份认证的用户,此时的
Authentication
可以从SecurityContext
中获取。
一个Authentication
对象主要包含三个方面的信息:
principal
:定义认证的用户。如果用户使用用户名/密码的方式登录,principal
通常就是一个UserDetails
对象。credentials
:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防止泄露。authorities
:用户被授予的权限信息。
不同的认证方式对应不同的Authentication
实例,其实现类如图所示:
AbstractAuthenticationToken
:该类实现了Authentication
和CredentialsContainer
两个接口,在AbstractAuthenticationToken
中对Authentication
接口定义的各个数据获取方法进行了实现,CredentialsContainer
则提供了登录凭证擦除方法。一般在登录成功后,为了防止用户信息泄露,可以将登录凭证(例如密码)擦除。RememberMeAuthenticationToken
(最常用):如果用户使用了remember-me的方式登录,登录信息将封装在RememberMeAuthenticationToken
中。TestingAuthenticationToken
:单元测试时封装的用户对象。AnonymousAuthenticationToken
:匿名登录时封装的用户对象。RunAsUserToken
:替换验证身份时封装的用户对象。UsernamePasswordAuthenticationToken
(最常用):表单登录时封装的用户对象。JaasAuthenticationToken
:JAAS认证时封装的用户对象。PreAuthenticatedAuthenticationToken
:Pre-Authentication场景下封装的用户对象
2.3.1从SecurityContextHolder
中获取
@RestController
public class UserController {
@GetMapping("/user")
public void userInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
System.out.println("name = " + name);
System.out.println("authorities = " + authorities);
}
}
SecurityContextHolder
SecurityContextHolder
中存储的是SecurityContext
(通过SecurityContextHolderStrategy
获取),SecurityContext
中存储的则是Authentication
。
SecurityContextHolder
中定义了三种不同的数组存储策略,这实际上是一种典型的策略模式:
MODE_THREADLOCAL
:这种存放策略是将SecurityContext
存放在ThreadLocal
中,其特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter
到达Servlet
, 都是由一个线程来处理的。这也是SecurityContextHolder
的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中获取登录用户数据,就会获取不到。MODE_INHERITABLETHREADLOCAL
:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。MODE_GLOBAL
:这种存储模式实际上是将数据保存在一个静态变量中,在java web开发中,这种模式很少使用到。
public interface SecurityContextHolderStrategy {
// 清除存储的SecurityContext对象
void clearContext();
// 获取存储的SecurityContext对象
SecurityContext getContext();
// 设置存储的SecurityContext对象
void setContext(SecurityContext context);
// 创建一个空的SecurityContext对象
SecurityContext createEmptyContext();
}
其一共有三个实现类,对应了三种不同的存储策略:
ThreadLocalSecurityContextHolderStrategy
:
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
// 存储数据的载体,所以针对SecurityContext的各种操作都是在ThreadLocal中进行操作
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
InheritableThreadLocalSecurityContextHolderStrategy
:相对于ThreadLocalSecurityContextHolderStrategy
来说,实现的策略基本一致,不同的是存储数据的载体变了,其变成了InheritableThreadLocal
,继承自ThreadLocal
,但是多了一个特性,就是在子线程创建的一瞬间,会自动将父线程中的数据复制到子线程中。该存储策略正是利用了这一特性,实现了在子线程中获取登录用户信息的功能。
SecurityContextHolder
的源码(仅列出核心部分):
public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
// 默认的存储策略是通过System.getProperty加载的,因此可以通过配置系统变量来修改默认的存储策略,
// 例如idea中配置VM options参数
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
}
从源码中可以看到, SecurityContextHolder定义了三个静态常量来描述三种不同的存储策略;存储策略strategy会在静态代码块中进行初始化,根据不同的strategyName初始化不同的存储策略;strategyName变量表示目前正在使用的存储策略,开发者可以通过配置系统变量或者调用setStrategyName来修改存储策略,调用setStrategyName后会重新初始化strategy。
SecurityContextHolder默认是将用户信息存储在ThreadLocal中,在spring boot中,不同的请求都是由不同的线程处理的,之所以每一次请求都能从SecurityContextHolder中获取到登录用户信息,其依赖于spring security过滤器链中重要的一环——SecurityContextPersistenceFilter。
SecurityContextPersistenceFilter
默认情况下,在spring security过滤器链中,SecurityContextPersistenceFilter
是第二道防线,位于
WebAsyncManagerIntegretionFilter
之后,其作用是为了存储SecurityContext
而设计的。
整体来说,SecurityContextPersistenceFilter
主要做两件事情:
-
当一个请求到来时,从
HttpSession
中获取SecurityContext
并存入SecurityContextHolder
中,这样在同一个请求的后续处理过程中,开发者始终可以通过SecurityContextHolder
获取到当前登录用户信息。 -
当一个请求处理完毕时,从
SecurityContextHolder
中获取SecurityContext
并存入HttpSession
中(主要针对异步servlet),方便下个请求到来时,再从HttpSession
中拿出来使用,同时擦除SecurityContextHolder
中的登录用户信息。需要注意的是,在
SecurityContextPersistenceFilter
过滤器中,当一个请求处理完毕时,从SecurityContextHolder
中获取,SecurityContext
存入HttpSession
中,这一步的操作主要是针对异步servlet。如果不是异步servlet,在响应提交时,就会将SecurityContext
保存到HttpSession
中了,而不会等到在SecurityContextPersistenceFilter
过滤器中再去存储。
将SecurityContext
存入HttpSession
,或者从HttpSession
中加载数据并转为SecurityContext
对象,这些事情都是由SecurityContextRepository
接口的实现类完成的:
public interface SecurityContextRepository {
/**
* 加载SecurityContext出来,对于没有登录的用户,这里会返回一个空的SecurityContext对象。
* 注意,空的SecurityContext对象是指其中不存在Authentication对象,而不是该方法返回null
*/
SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);
// 保存一个SecurityContext对象
void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);
// 判断SecurityContext对象是否存在
boolean containsContext(HttpServletRequest request);
}
TestSecurityContextRepository
为单元测试提供支持。NullSecurityContextRepository
实现类中,loadContext
方法总是返回一个空的SecurityContext
对象,saveContext
方法未做任何实现,containsContext
总是返回false
,所以NullSecurityContextRepository
实现类实际上未做SecurityContext
的存储工作。HttpSessionSecurityContextRepository
是spring security中默认使用的实现类,实现了将SecurityContext
存储到HttpSession
以及从HttpSession
中加载SecurityContext
出来。
SaveToSessionResponseWrapper
实际上就是我们所熟知的HttpServletResponse
功能的扩展。有三个关键的实现类:
-
HttpServletResponseWrapper
:是HttpServletResponse
的装饰类,利用HttpServletResponseWrapper
可以方便地操作参数和输出流等。 -
OnCommittedResponseWrapper
:对HttpServletResponseWrapper
的功能进行了增强,最重要的增强在于可以获取其提交行为。当HttpServletResponse
的sendError
、sendRedirect
、flushBuffer
、flush
以及close
等方法被调用时,onResponseCommitted
方法会被触发,开发者可以在该方法中做一些数据保存操作,例如保存SecurityContext
。不过OnCommittedResponseWrapper
中的onResponseCommitted
只是一个抽象方法,具体的实现在它的实现类SaveContextOnUpdateOrErrorResponseWrapper
中。 -
SaveContextOnUpdateOrErrorResponseWrapper
:对onResponseCommitted
方法做了实现。在该类中声明了一个contextSaved
变量,表示SecurityContext
是否已经存储成功。当HttpServletResponse
提交时,会调用onResponseCommitted
方法,在该方法中调用saveContext
方法,将
SecurityContext
保存到HttpSession
中,同时将contextSaved
变量标记为true
。saveContext
也是一个抽象方法,具体的实现在SaveToSeesionResponseWrapper
中。
接下来看一下SaveToSessionResponseWrapper
的定义:
final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper {
private final HttpServletRequest request;
private final boolean httpSessionExistedAtStartOfRequest;
private final SecurityContext contextBeforeExecution;
private final Authentication authBeforeExecution;
SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request,
boolean httpSessionExistedAtStartOfRequest, SecurityContext context) {
super(response, HttpSessionSecurityContextRepository.this.disableUrlRewriting);
this.request = request;
this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
this.contextBeforeExecution = context;
this.authBeforeExecution = context.getAuthentication();
}
@Override
protected void saveContext(SecurityContext context) {
final Authentication authentication = context.getAuthentication();
HttpSession httpSession = this.request.getSession(false);
String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
// 如果authentication为null或者它是一个匿名对象,则不需要保存SecurityContext
// See SEC-776
if (authentication == null
|| HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {
if (httpSession != null && this.authBeforeExecution != null) {
// SEC-1587 A non-anonymous context may still be in the session
// SEC-1735 remove if the contextBeforeExecution was not anonymous
httpSession.removeAttribute(springSecurityContextKey);
this.isSaveContextInvoked = true;
}
return;
}
httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication);
// If HttpSession exists, store current SecurityContext but only if it has
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
if (httpSession != null) {
// We may have a new session, so check also whether the context attribute
// is set SEC-1561
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
httpSession.setAttribute(springSecurityContextKey, context);
this.isSaveContextInvoked = true;
}
}
}
private boolean contextChanged(SecurityContext context) {
return this.isSaveContextInvoked || context != this.contextBeforeExecution
|| context.getAuthentication() != this.authBeforeExecution;
}
private HttpSession createNewSessionIfAllowed(SecurityContext context, Authentication authentication) {
if (isTransientAuthentication(authentication)) {
return null;
}
if (this.httpSessionExistedAtStartOfRequest) {
return null;
}
if (!HttpSessionSecurityContextRepository.this.allowSessionCreation) {
return null;
}
// Generate a HttpSession only if we need to
if (HttpSessionSecurityContextRepository.this.contextObject.equals(context)) {
return null;
}
try {
HttpSession session = this.request.getSession(true);
return session;
}
catch (IllegalStateException ex) {
// Response must already be committed, therefore can't create a new
// session
this.logger.warn("Failed to create a session, as response has been committed. "
+ "Unable to store SecurityContext.");
}
return null;
}
}
SaveToSessionResponseWrapper
中其实主要定义了三个方法:saveContext
、contextChanged
以及createNewSessionIfAllowed
:
-
saveContext
:该方法主要是用来保存SecurityContext
,如果authentication
对象为null
或者它是一个匿名对象,则不需要保存SecurityContext
;同时,如果
httpSession
不为null
并且authBeforeExecution
也不为null
,就从httpSession
中将保存的登录用户数据移除,这个主要是为了防止开发者在注销成功的回调中继续调用chain.doFilter
方法,进而导致原始的登录信息无法清除的问题;如果httpSession
为null
,则去创建一个HttpSession
对象;最后,如果SecurityContext
发生了变化,或者httpSession
中没有保存SecurityContext
,则调用httpSession
中的setAttribute
方法将SecurityContext
保存起来。 -
contextChanged
:该方法主要用来判断SecurityContext
是否发生变化,因为在程序运行过程中,开发者可能修改了SecurityContext
中的Authentication
对象。 -
createNewSessionIfAllowed
:该方法用来创建一个HttpSession
对象。SaveToSessionResponseWrapper
一个核心的功能就是在HttpServletResponse
提交的时候,将SecurityContext
保存到HttpSession
中。
相对来说,SaveToSessionRequestWrapper
就要简单很多,源码可以自行查看,其主要作用是禁止在异步servlet提交时,自动保存SecurityContext
(异步servlet使用较少,感兴趣的可以自行了解)。
因为在异步servlet中,当任务执行完毕之后,HttpServletResponse
也会自动提交,在提交的过程中会自动保存SecurityContext因为在异步servlet中,当任务执行完毕之后,HttpServletResponse
也会自动提交,在提交的过程中会自动保存SecurityContext
到HttpSession
中,但是由于是在子线程中,因此无法获取到SecurityContext
对象(默认存储在ThreadLocal
中),所以会保存失败。
如果开发者使用了异步servlet,则默认情况下会禁用HttpServletResponse
提交时自动保存SecurityContext
这一功能,改为在SecurityContextPersistenceFilter
过滤器中完成SecurityContext
保存操作。
HttpSessionSecurityContextRepository
类的功能:
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
// 定义SecurityContext在HttpSession中存储的key,可以通过该key来手动操作HttpSession中存储的SecurityContext
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
private final Object contextObject = SecurityContextHolder.createEmptyContext();
private boolean allowSessionCreation = true;
private boolean disableUrlRewriting = false;
private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;
// 用户身份评估器,用来判断当前用户是匿名用户还是remember-me登录的用户
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
// 获取SecurityContext对象
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
// 如果获取到的对象为null,则生成一个空的SecurityContext对象
context = generateNewContext();
}
// 最后构造请求和响应的装饰类并存入requestResponseHolder
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
return context;
}
/**
* 用来保存SecurityContext。正常情况下,在HttpServletResponse提交时SecurityContext就已经保存
* 到HttpSession中了;如果是异步servlet,则提交时不会自动将SecurityContext保存到HttpSession,
* 此时会在这里进行保存操作
*/
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
SaveContextOnUpdateOrErrorResponseWrapper.class);
responseWrapper.saveContext(context);
}
/**
* 判断请求中是否存在SecurityContext对象
*/
@Override
public boolean containsContext(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return false;
}
return session.getAttribute(this.springSecurityContextKey) != null;
}
/**
* 执行具体的SecurityContext读取逻辑
*/
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
if (httpSession == null) {
return null;
}
// Session exists, so try to obtain a context from it.
Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
if (contextFromSession == null) {
return null;
}
// We now have the security context object from the session.
if (!(contextFromSession instanceof SecurityContext)) {
return null;
}
// Everything OK. The only non-null return from this method.
return (SecurityContext) contextFromSession;
}
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
/**
* 设置是否允许创建HttpSession,默认是true
*/
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
/**
* 是否禁用URL重写,默认是false
*/
public void setDisableUrlRewriting(boolean disableUrlRewriting) {
this.disableUrlRewriting = disableUrlRewriting;
}
public void setSpringSecurityContextKey(String springSecurityContextKey) {
this.springSecurityContextKey = springSecurityContextKey;
}
/**
* 用来判断Authentication是否免于存储
*/
private boolean isTransientAuthentication(Authentication authentication) {
return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null;
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}
}
而HttpSessionSecurityContextRepository
中提供的所有功能都将在SecurityContextPersistenceFilter
过滤器中进行调用:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 确保请求只执行一次该过滤器
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 表示是否要在过滤器链执行之前确保会话有效,由于这是一个比较耗费资源的操作,因此默认值为false
if (this.forceEagerSessionCreation) {
// 过滤器链执行之前确保会话有效
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 加载SecurityContext实例,如果没有则创建,具体看实现方法
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
// 将SecurityContext保存在SecurityContextHolder中方便后续使用
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 使请求继续往下走,但是要注意,此时传递的请求和响应对象是在HttpSessionSecurityContextRepository
// 中封装后的对象,即SaveToSessionRequestWrapper和SaveToSessionResponseWrapper的实例
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
// 当请求处理完毕后,在finally块中,获取最新的SecurityContext对象(开发者可能在后续处理中
// 修改了SecurityContext中的Authentication对象)
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 然后清空SecurityContextHolder中的数据
SecurityContextHolder.clearContext();
// 保存SecurityContext
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
// 最后,从request中移除FILTER_APPLIED属性
request.removeAttribute(FILTER_APPLIED);
}
}
总的来说,就是请求在到达SecurityContextPersistenceFilter
过滤器之后,先从HttpSession
中读取SecurityContext
出来,并存入SecurityContextHolder
之中以备后续使用
当请求离开SecurityContextPersistenceFilter
过滤器的时候,获取最新的SecurityContext
并存入HttpSession
中,同时清空SecurityContextHolder
中的登录用户信息。
评论区