侧边栏壁纸
博主头像
憨憨大头个人博客博主等级

心存希冀,目有繁星

  • 累计撰写 110 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

2、Spring security认证

Administrator
2024-09-02 / 0 评论 / 0 点赞 / 20 阅读 / 52172 字

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接口。

image-20230519175033126

默认的登录用户名是user,登录密码则是一个随机生成的UUID字符串,在项目启动日志中可以看到登录密码(这也意味着项目每次启动时,密码都会发生变化)。
输入默认的用户名和密码,就可以成功登录了。这就是spring security的强大之处,只需要引入一个依赖,所有的接口就会被自动保护起来。

2.1.2 流程分析

  1. 客户端(浏览器)发起请求去访问/hello接口,这个接口默认是需要认证之后才能访问的。

  2. 这个请求会走一遍spring security中的过滤器链,在最后的FilterSecurityInterceptor过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出AccessDeniedException异常。

  3. 抛出的AccessDeniedException异常在ExceptionTranslationFilter过滤器中被捕获,ExceptionTranslationFilter过滤器通过调用LoginUrlAuthenticationEntryPoint#commence方法给客户端返回302,要求客户端重定向到/login

  4. 客户端发送/login请求。

  5. /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;
}
在这里插入图片描述
  • UserDetailsManagerUserDetailsService的基础上,继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在供5种方法。

  • JdbcDaoImplUserDetailsService的基础上,通过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中的用户信息进行了封装,封装之后,开发者若再想获取用户登录数据就会有两种不同的思路:

  1. SecurityContextHolder中获取。
  2. 从当前请求对象中获取。

无论哪种获取方式,都离不开一个重要的对象:Authentication。在spring security中,Authentication对象主要有两方面的功能:

  1. 作为AuthenticationManager的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的isAuthenticated方法返回false,表示用户还未认证。
  2. 代表已经经过身份认证的用户,此时的Authentication可以从SecurityContext中获取。

一个Authentication对象主要包含三个方面的信息:

  1. principal:定义认证的用户。如果用户使用用户名/密码的方式登录,principal通常就是一个UserDetails对象。
  2. credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防止泄露。
  3. authorities:用户被授予的权限信息。

不同的认证方式对应不同的Authentication实例,其实现类如图所示:

2955da51f13e4574bc3080d4c8d3d7b1

  1. AbstractAuthenticationToken:该类实现了AuthenticationCredentialsContainer两个接口,在AbstractAuthenticationToken中对Authentication接口定义的各个数据获取方法进行了实现,CredentialsContainer则提供了登录凭证擦除方法。一般在登录成功后,为了防止用户信息泄露,可以将登录凭证(例如密码)擦除。
  2. RememberMeAuthenticationToken(最常用):如果用户使用了remember-me的方式登录,登录信息将封装在RememberMeAuthenticationToken中。
  3. TestingAuthenticationToken:单元测试时封装的用户对象。
  4. AnonymousAuthenticationToken:匿名登录时封装的用户对象。
  5. RunAsUserToken:替换验证身份时封装的用户对象。
  6. UsernamePasswordAuthenticationToken(最常用):表单登录时封装的用户对象。
  7. JaasAuthenticationToken:JAAS认证时封装的用户对象。
  8. 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

a002b56e4ee84008adea382aec6b3e8e

SecurityContextHolder中存储的是SecurityContext(通过SecurityContextHolderStrategy获取),SecurityContext中存储的则是Authentication

SecurityContextHolder中定义了三种不同的数组存储策略,这实际上是一种典型的策略模式:

  1. MODE_THREADLOCAL:这种存放策略是将SecurityContext存放在ThreadLocal中,其特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达Servlet, 都是由一个线程来处理的。这也是SecurityContextHolder的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中获取登录用户数据,就会获取不到。
  2. MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  3. MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在java web开发中,这种模式很少使用到。
public interface SecurityContextHolderStrategy {
    // 清除存储的SecurityContext对象
    void clearContext();

    // 获取存储的SecurityContext对象
    SecurityContext getContext();

    // 设置存储的SecurityContext对象
    void setContext(SecurityContext context);

    // 创建一个空的SecurityContext对象
    SecurityContext createEmptyContext();
}

其一共有三个实现类,对应了三种不同的存储策略:

  1. 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主要做两件事情:

  1. 当一个请求到来时,从HttpSession中获取SecurityContext并存入SecurityContextHolder中,这样在同一个请求的后续处理过程中,开发者始终可以通过SecurityContextHolder获取到当前登录用户信息。

  2. 当一个请求处理完毕时,从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);
}
  1. TestSecurityContextRepository为单元测试提供支持。
  2. NullSecurityContextRepository实现类中,loadContext方法总是返回一个空的SecurityContext对象,saveContext方法未做任何实现, containsContext总是返回false,所以NullSecurityContextRepository实现类实际上未做SecurityContext的存储工作。
  3. HttpSessionSecurityContextRepository是spring security中默认使用的实现类,实现了将SecurityContext存储到HttpSession以及从HttpSession中加载SecurityContext出来。

c42862cd748448dfacfa9a8a299d7e79

SaveToSessionResponseWrapper实际上就是我们所熟知的HttpServletResponse功能的扩展。有三个关键的实现类:

  1. HttpServletResponseWrapper:是HttpServletResponse的装饰类,利用HttpServletResponseWrapper可以方便地操作参数和输出流等。

  2. OnCommittedResponseWrapper:对HttpServletResponseWrapper的功能进行了增强,最重要的增强在于可以获取其提交行为。当HttpServletResponsesendErrorsendRedirectflushBufferflush以及close等方法被调用时,onResponseCommitted方法会被触发,开发者可以在该方法中做一些数据保存操作,例如保存SecurityContext。不过OnCommittedResponseWrapper中的onResponseCommitted只是一个抽象方法,具体的实现在它的实现类SaveContextOnUpdateOrErrorResponseWrapper中。

  3. SaveContextOnUpdateOrErrorResponseWrapper:对onResponseCommitted方法做了实现。在该类中声明了一个contextSaved变量,表示SecurityContext是否已经存储成功。当HttpServletResponse提交时,会调用onResponseCommitted方法,在该方法中调用saveContext方法,

    SecurityContext保存到HttpSession中,同时将contextSaved变量标记为truesaveContext也是一个抽象方法,具体的实现在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中其实主要定义了三个方法:saveContextcontextChanged以及createNewSessionIfAllowed

  1. saveContext:该方法主要是用来保存SecurityContext,如果authentication对象为null或者它是一个匿名对象,则不需要保存SecurityContext

    同时,如果httpSession不为null并且authBeforeExecution也不为null,就从httpSession中将保存的登录用户数据移除,这个主要是为了防止开发者在注销成功的回调中继续调用chain.doFilter方法,进而导致原始的登录信息无法清除的问题;如果httpSessionnull,则去创建一个HttpSession对象;最后,如果SecurityContext发生了变化,或者httpSession中没有保存SecurityContext,则调用httpSession中的setAttribute方法将SecurityContext保存起来。

  2. contextChanged:该方法主要用来判断SecurityContext是否发生变化,因为在程序运行过程中,开发者可能修改了SecurityContext中的Authentication对象。

  3. createNewSessionIfAllowed:该方法用来创建一个HttpSession对象。SaveToSessionResponseWrapper一个核心的功能就是在HttpServletResponse提交的时候,将SecurityContext保存到HttpSession中。

相对来说,SaveToSessionRequestWrapper就要简单很多,源码可以自行查看,其主要作用是禁止在异步servlet提交时,自动保存SecurityContext(异步servlet使用较少,感兴趣的可以自行了解)。

因为在异步servlet中,当任务执行完毕之后,HttpServletResponse也会自动提交,在提交的过程中会自动保存SecurityContext因为在异步servlet中,当任务执行完毕之后,HttpServletResponse也会自动提交,在提交的过程中会自动保存SecurityContextHttpSession中,但是由于是在子线程中,因此无法获取到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中的登录用户信息。

0

评论区