0
点赞
收藏
分享

微信扫一扫

《SpringSecurity in Action》六: 基于Session实现登陆认证源码分析

眸晓 2022-05-01 阅读 78

文章目录

1 概述

本篇基于《SpringSecurity in Action》一: 基于Session实现登陆认证

2 认证流程

登陆认证流程图
在《SpringSecurity in Action》书中,对登陆认证流程的示意图,可以看到流程还是很简单的,所以我们只要清楚的认识到就是这么几步,对登陆认证过程的认识就不会差到哪去。

虽然流程图简单,但在细节上还是有很多需要学习的地方的,比如如何获取的用户信息,如何进行密码校验,登陆完成后做了什么,还有就是我们的重点-认证过程Session是怎么参与的。这些问题都是值得到认真研究研究的。

2.1 Session是如何参与的

这个要从SecurityContextPersistenceFilter这个过滤器说起,这是一个很靠前的过滤器,该过滤器做了三件事:

  1. SecurityContextRepository中获取SecurityContext并将其填充到SecurityContextHolder
  2. 请求完成后,清除SecurityContextHolder中的SecurityContext(这里清除的肯定是与当前用户相关的那个SecurityContext)
  3. 请求完成后,将SecurityContext再保存到SecurityContextRepository

该过滤器的作用是,在后续的过滤器或是我们自己的业务逻辑中,如果想获取当前用户的认证信息,都可以从SecurityContextHolder中获取到。

对于上面第3步,将SecurityContext再保存到SecurityContextRepository中这一环节中,SecurityContextRepository的一个默认实现类,就是HttpSessionSecurityContextRepository,这好像快要与Session有关系了。是的,HttpSessionSecurityContextRepository这个Repository就是把SecurityContext存储在Session中的 – 当登陆谁成功时,把SecurityContext存储到Session中;当请求业务逻辑时,从请求中获取SessionID,根据SessionID获取到Session,再从Session中获取到SecurityContext
示意图如下
在这里插入图片描述

3 源码解析

SecurityContextPersistenceFilter开始的过滤器,在下面的代码中,只体现了主要的代码。

public class SecurityContextPersistenceFilter extends GenericFilterBean {
	
	private SecurityContextRepository repo;

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
				throws IOException, ServletException {
			/*
			 * 省略其它代码
			 * 这里省略了一些代码,主要体现了认证过程中的那三步
			 */
	
			HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
					response);
			// 1. 从`SecurityContextRepository`
			SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
	
			try {
			// 将`SecurityContext`填充到`SecurityContextHolder`中
				SecurityContextHolder.setContext(contextBeforeChainExecution);
	
				chain.doFilter(holder.getRequest(), holder.getResponse());
	
			}
			finally {
				SecurityContext contextAfterChainExecution = SecurityContextHolder
						.getContext();
				// 2. 请求完成后,清除`SecurityContextHolder`中的`SecurityContext`(这里清除的肯定是与当前用户相关的那个`SecurityContext`)
				SecurityContextHolder.clearContext();
				//  3. 请求完成后,将`SecurityContext`再保存到`SecurityContextRepository`中
				repo.saveContext(contextAfterChainExecution, holder.getRequest(),
						holder.getResponse());
				/*
			 	 * 省略其它代码
			     */
			}
		}
}

下面看一下第1步中loadContext这个方法是怎么执行的:

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
	public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
		HttpServletRequest request = requestResponseHolder.getRequest();
		HttpServletResponse response = requestResponseHolder.getResponse();
		/* 
		 * 从request中获取Session,这里顺便说一下,是HttpSession,SecurityContext也定义了
		 * 一些跟Session有关的对象,如Session,RedisSession,SessionInformation等,这
		 * 里还是要区分一下
		 */
		HttpSession httpSession = request.getSession(false);
		// 从HttpSession中获取SecurityContext对象
		SecurityContext context = readSecurityContextFromSession(httpSession);

		/*
	 	 * 省略很多代码
	     */
	    // 返回从HttpSession中获取的SecurityContext
		return context;
	}

	private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
		/*
	 	 * 省略很多代码
	     */
	     // 从HttpSession中获取SecurityContext对象
		Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
		return (SecurityContext) contextFromSession;
	}
}

到这里,在一个请求中如果获取SecurityContext 的源码就基本搞完了,那么在认证请求中,SecurityContext 是如何被存到Session中的呢?

SecurityContextPersistenceFilter过滤器的finally语句块里,有一行代码repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());,没错,这行就是保存SecurityContext的地方,对于 不同的repo,会有不同的存储方式,对于HttpSessionSecurityContextRepository来讲,就是保存到HttpSession中。

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
	public void saveContext(SecurityContext context, HttpServletRequest request,
			HttpServletResponse response) {
		/*
	 	 * 省略一些代码
	 	 * 这里做了一个判断,SecurityContext是否已经保存过,如果保存过,就不再重复执行保存了
	 	 * 可以猜一下,不只有这一个地方会执行保存动作,其它地方可能也会保存,所以这里才会做这么
	 	 * 一个判断。是的,其它地方确实有,在请求响应被提交的时候,就是onResponseCommitted的时候
	 	 * 就会执行保存动作。为什么会有两个地方都执行保存动作,这个我还不是很清楚
	     */
		if (!responseWrapper.isContextSaved()) {
			responseWrapper.saveContext(context);
		}
	}
}

多次测试结果是,在SecurityContextPersistenceFilter过滤器的finally语句块里的repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());这行代码被执行时,if (!responseWrapper.isContextSaved())判断都会是false,也就是onResponseCommitted这个动作一直都会先于这个finally语句块,这里确实还没弄明白是怎么回事。下面我们来看一下onResponseCommitted这个方法里都发生了什么吧。

	@Override
	protected void onResponseCommitted() {
		// 在这里执行了保存动作
		saveContext(SecurityContextHolder.getContext());
		this.contextSaved = true;
	}

这个saveContext保存动作与上面responseWrapper.saveContext(context)执行的是同一个方法,都是保存SecurityContext到HttpSession的。

		@Override
		protected void saveContext(SecurityContext context) {
			final Authentication authentication = context.getAuthentication();
			HttpSession httpSession = request.getSession(false);

			/*
	 	     * 省略很多代码
	         */
			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) {
					// 保存context到HttpSession中
					httpSession.setAttribute(springSecurityContextKey, context);
				}
			}
		}

这样就完成了SecurityContext对象保存到HttpSession中的过程。

举报

相关推荐

0 条评论