文章目录
1 概述
本篇基于《SpringSecurity in Action》一: 基于Session实现登陆认证
2 认证流程
在《SpringSecurity in Action》书中,对登陆认证流程的示意图,可以看到流程还是很简单的,所以我们只要清楚的认识到就是这么几步,对登陆认证过程的认识就不会差到哪去。
虽然流程图简单,但在细节上还是有很多需要学习的地方的,比如如何获取的用户信息,如何进行密码校验,登陆完成后做了什么,还有就是我们的重点-认证过程Session是怎么参与的。这些问题都是值得到认真研究研究的。
2.1 Session是如何参与的
这个要从SecurityContextPersistenceFilter
这个过滤器说起,这是一个很靠前的过滤器,该过滤器做了三件事:
- 从
SecurityContextRepository
中获取SecurityContext
并将其填充到SecurityContextHolder
中 - 请求完成后,清除
SecurityContextHolder
中的SecurityContext
(这里清除的肯定是与当前用户相关的那个SecurityContext
) - 请求完成后,将
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中的过程。