之前几篇我们基本上讲述了SpringSecurity 登录认证和授权认证的实现和源码分析。我们大致清楚了 :
今天我们分析下整个认证过程和企业级redis使用的思想,是为了解决什么问题,以及怎么解决的?
我们简单回顾下 SpringSecurity 流程。
简单就可以发现上面流程从头到尾都少不了一个关键东西上下文SecurityContext。它是由SecurityContextPersistenceFilter 过滤器处理的,那我们就先看下它是怎么个实现逻辑:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
// 中获取上下文
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 将上下文SecurityContextHolder
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// 结束认证之后,将 SecurityContextHolder清除
SecurityContextHolder.clearContext();
// SecurityContextRepository 存入session
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
讲解下什么意思:
请求API的时候进行以下步骤:
1. 通过该方法 SecurityContextRepository#loadContext(holder)去获取当前登录用户的上下文。它的实现类有2个:HttpSessionSecurityContextRepository 和 NullSecurityContextRepository
HttpSessionSecurityContextRepository :从session中获取上下文。
NullSecurityContextRepository:直接创建一个空的上下文。(当设置session为无状态时会使用他)
2. 将获取到的上下文set给 SecurityContextHolder#setContext。SecurityContextHolder 他是我们整个流程获取上下文数据的类,所以在一开始就通过session获取到上下文放到SecurityContextHolder 中,以便在后续认证流程中使用。
3. 直接 doFilter 下一个过滤器调用,也就是进入验证流程。
4. 请求认证结束后清除 SecurityContextHolder,并将上下存有上下文 SecurityContextRepository存到session中。
通过上面流程我们清楚了几个点:
SecurityContextHolder 是整个认证过程上下文信息的持有者,但是认证结束后会被清除。
上下文信息由SecurityContextRepository管理,每次请求都会事先从session获取上下文或者直接为空上下文,给后续使用,如果确实登录过了上下文就有信息,否则后续验证不过关。
每次请求认证结束后,会清除SecurityContextHolder ,并存到session中。下次来还是从session获取。
但是对于企业级项目来说,这也欠缺的太多了:
1. 只要用户登录之后,访问任何api都是从session获取当前信息,从而api本身安全性低。我们所需要的是每个api都应该是无状态的,要求你每次api的请求都要进行安全认证(token)。
2. 用户信息存入session,那么对用过多的登录认证,session会越来越庞大,内存吃不消啊。
3. session是基于jvm内存的,服务被重启就没得了。
所以我们需要基于缓存存储token数据比如redis,大致的思路:
首先关闭springsecurity session管理,要求每一个请求要被验证, 实现一个过滤器对请求头中token进行验证。如果验证通过,则封装一个 authenticationToken给上下文,供后续认证使用。如果不通过,此时的上下文是没有数据的,后续的过滤器自然会拦截掉。
清晰思路:(每次请求都是这个流程)
从redis拿上下文 ----> 放入SecurityContextHolder ------> 用完清除 ---> 从redis拿上下文
用户认证后会把用户信息(上下文)存入redis,并返回一个JWT令牌(令牌内有redis的key),之后所有请求都需要验证的这个令牌访问,验证过滤器一开始就会提取JWT中的key,并从redis获取用户信息(上下文):
获取不到-- >未登录,直接拦截;
获取到了--> 说明该用户确实登录了,但是最好还是需要校验一下token的有效性 ---> 一切都通过,则将redis中获取到的用户信息(上下文)存入SecurityContextHolder中。 这样后面的授权认证工作就可以顺利获取信息进行。
当然了没带token的自然一开始就给直接拦截。
注意:
1. 记得关闭session管理
2. 在登录认证之后我们就要把用户信息存入redis,作为缓存。
3. springsecurity session关闭后springsecurity 会跳过所有的 filter chain:HttpSessionSecurityContextRepository, SessionManagementFilter, RequestCacheFilter