SpringSecurity 核心过滤器——SecurityContextPersistenceFilter-LMLPHP

前言

SecurityContextHolder,这个是一个非常基础的对象,存储了当前应用的上下文SecurityContext,而在SecurityContext可以获取Authentication对象。也就是当前认证的相关信息会存储在Authentication对象中。

SpringSecurity 核心过滤器——SecurityContextPersistenceFilter-LMLPHP

默认情况下,SecurityContextHolder是通过 ThreadLocal来存储对应的信息的。也就是在一个线程中我们可以通过这种方式来获取当前登录的用户的相关信息。而在SecurityContext中就只提供了对Authentication对象操作的方法。

在项目中可以通过以下方式获取当前登录的用户信息

    public String hello(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        if(principal instanceof UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            System.out.println(userDetails.getUsername());
            return "当前登录的账号是:" + userDetails.getUsername();
        }
        return "当前登录的账号-->" + principal.toString();
    }

调用 getContext()返回的对象是 SecurityContext接口的一个实例,这个对象就是保存在线程中的。接下来将看到,Spring Security中的认证大都返回一个 UserDetails的实例作为principa。

过滤器介绍

在Session中维护一个用户的安全信息就是这个过滤器处理的。从request中获取session,从Session中取出已认证用户的信息保存在SecurityContext中,提高效率,避免每一次请求都要解析用户认证信息,方便接下来的filter直接获取当前的用户信息。

用户信息的存储

用户信息的存储是通过SecutiryContextRepository接口操作的,定义了对SecurityContext的存储操作,在该接口中定义了如下的几个方法

public interface SecurityContextRepository {

	/**
	 * 获取SecurityContext对象
	 */
	SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

	/**
	 * 存储SecurityContext
	 */
	void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);

	/**
	 * 判断是否存在SecurityContext对象
	 */
	boolean containsContext(HttpServletRequest request);

}

默认的实现是HttpSessionSecurityContextRepository。也就是把SecurityContext存储在了HttpSession中。对应的抽象方法实现如下:

获取用户信息

	public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        // 获取对有的Request和Response对象
		HttpServletRequest request = requestResponseHolder.getRequest();
		HttpServletResponse response = requestResponseHolder.getResponse();
		// 获取HttpSession对象
		HttpSession httpSession = request.getSession(false);
        // 从HttpSession中获取SecurityContext对象
		SecurityContext context = readSecurityContextFromSession(httpSession);
		if (context == null) {
			// 如果HttpSession中不存在SecurityContext对象就创建一个
			// SecurityContextHolder.createEmptyContext();
			// 默认是ThreadLocalSecurityContextHolderStrategy存储在本地线程中
			context = generateNewContext();
			if (this.logger.isTraceEnabled()) {
				this.logger.trace(LogMessage.format("Created %s", context));
			}
		}
		// 包装Request和Response对象
		SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
				httpSession != null, context);
		requestResponseHolder.setResponse(wrappedResponse);
		requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
		return context;
	}

存储用户信息

	@Override
	public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
		SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
				SaveContextOnUpdateOrErrorResponseWrapper.class);
		Assert.state(responseWrapper != null, () -> "Cannot invoke saveContext on response " + response
				+ ". You must use the HttpRequestResponseHolder.response after invoking loadContext");

		responseWrapper.saveContext(context);
	}
@Override
		protected void saveContext(SecurityContext context) {
			// 获取Authentication对象
			final Authentication authentication = context.getAuthentication();
			// 获取HttpSession对象
			HttpSession httpSession = this.request.getSession(false);
			// 
			String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
			// 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;
				}
				if (this.logger.isDebugEnabled()) {
					if (authentication == null) {
						this.logger.debug("Did not store empty SecurityContext");
					}
					else {
						this.logger.debug("Did not store anonymous SecurityContext");
					}
				}
				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 中存储SecurityContext
					httpSession.setAttribute(springSecurityContextKey, context);
					this.isSaveContextInvoked = true;
					if (this.logger.isDebugEnabled()) {
						this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
					}
				}
			}
		}

获取用户信息

	@Override
	public boolean containsContext(HttpServletRequest request) {
		// 获取HttpSession
		HttpSession session = request.getSession(false);
		if (session == null) {
			return false;
		}
		// 从session中能获取就返回true否则false
		return session.getAttribute(this.springSecurityContextKey) != null;
	}

处理逻辑

在请求到达时,SecurityContextPersistenceFilter会从HTTP请求中读取用户凭证,并使用这些信息来恢复用户的安全上下文。在请求完成后,它会在HTTP响应中写入用户凭证,以便在下一次请求时可以恢复用户的安全上下文。具体处理逻辑:

	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);
		// 是否提前创建 HttpSession
		if (this.forceEagerSessionCreation) {
			// 创建HttpSession
			HttpSession session = request.getSession();
			if (this.logger.isDebugEnabled() && session.isNew()) {
				this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
			}
		}
		// 把Request和Response对象封装为HttpRequestResponseHolder对象
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		// 获取SecurityContext对象
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
			// SecurityContextHolder绑定SecurityContext对象
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			if (contextBeforeChainExecution.getAuthentication() == null) {
				logger.debug("Set SecurityContextHolder to empty SecurityContext");
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger
							.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
				}
			}// 结束交给下一个过滤器处理
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			// 当其他过滤器都处理完成后
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// 移除SecurityContextHolder中的Security
			SecurityContextHolder.clearContext();
			// 把
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			// 存储Context在HttpSession中
			request.removeAttribute(FILTER_APPLIED);
			this.logger.debug("Cleared SecurityContextHolder to complete request");
		}
	}

通过上面的代码逻辑其实就清楚了在SpringSecurity中的认证信息的流转方式了。首先用户的认证状态Authentication是存储在SecurityContext中的,而每个用户的SecurityContext是统一存储在HttpSession中的。一次请求流转中我们需要获取当前的认证信息是通过SecurityContextHolder来获取的,默认是在ThreadLocal中存储的。

总结

SecurityContextPersistenceFilter是Spring Security框架中一个重要的过滤器,用于处理与用户安全上下文相关的操作,通常位于Spring Security过滤器链的最前面,因为需要在其他过滤器执行之前恢复用户的安全上下文。在Spring Security的过滤器链中,SecurityContextPersistenceFilter之后的过滤器可能会改变用户的身份或安全上下文,例如AuthenticationFilter可能会对用户的身份进行认证。

09-23 21:26