本周,大部分时间去撰写毕业设计中期报告,在部署 Alice
学生管理系统测试环境时想起本系统借助 Redis
实现分布式 Session
。
为什么要分布式 Session
呢?
请参考下图:
当后台集群部署时,单机的 Session
维护就会出现问题。
假设登录的认证授权发生在 Tomcat A
服务器上, Tomcat A
在本地存储了用户 Session
,并签发认证令牌,用于验证用户身份。
下次请求可能分发给 Tomcat B
服务器,而 Tomcat B
并没有用户 Session
,用户携带的认证令牌无效,得到 401
。
除了 JWT
无状态的认证方式,另一种主流的实现方案就是采用分布式 Session
。
public interface HttpSession { public void setAttribute(String name, Object value); }
HttpSession
内的存储就是 name
与 value
的键值对映射,且存在过期时间,这与 Redis
的设计相符合,分布式 Session
通常使用 Redis
进行实现。
无论是在单机环境,还是在引入了 Spring Session
的集群环境下,代码实现都是相同的,即屏蔽了底层的细节,可以在不改动 HttpSession
使用的相关代码的情况下,实现 Session
存储环境的切换。
logger.debug("记录当前用户ID"); httpSession.setAttribute(UserService.USER_ID, persistUser.getId());
这听起来很酷,那么 Spring Session
具体是如何在不改动代码的情况下进行 Session
存储环境切换的呢?
官方文档: How HttpSession Integration Works - Spring Session
之前在学习 Spring Security
原理之时,我们从官方文档中找到了这样一张图。
所有的认证授权拦截都是基于 Filter
实现的,而这里的 Spring Session
,也是基于 Filter
。
因为 HttpSession
和 HttpServletRequest
(获取 HttpSession
的 API
)都是接口,这意味着可以将这些 API
替换成自定义的实现。
核心源码如下:
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { /** 替换 request */ SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext); /** 替换 response */ SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response); /** try-finally,finally 必定执行 */ try { /** 执行后续过滤器链 */ filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { /** 后续过滤器链执行完毕,提交 session,用于存储 session 信息并返回 set-cookie 信息 */ wrappedRequest.commitSession(); } } }
response
封装器核心源码如下:
private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper { SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) { super(response); this.request = request; } @Override protected void onResponseCommitted() { /** response 提交后提交 session */ this.request.commitSession(); } }
request
封装器核心源码如下:
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper { private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) { super(request); this.response = response; this.servletContext = servletContext; } /** * 将 sessionId 写入 reponse,并持久化 session */ private void commitSession() { /** 获取当前 session 信息 */ S session = getCurrentSession().getSession(); /** 持久化 session */ SessionRepositoryFilter.this.sessionRepository.save(session); /** reponse 写入 sessionId */ SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, session.getId()); } /** * 重写 HttpServletRequest 的 getSession 方法 */ @Override public HttpSessionWrapper getSession(boolean create) { /** 从持久化中查询 session */ S requestedSession = getRequestedSession(); /** session 存在,直接返回 */ if (requestedSession != null) { currentSession = new HttpSessionWrapper(requestedSession, getServletContext()); currentSession.setNew(false); return currentSession; } /** 设置不创建,返回空 */ if (!create) { return null; } /** 创建 session 并返回 */ S session = SessionRepositoryFilter.this.sessionRepository.createSession(); currentSession = new HttpSessionWrapper(session, getServletContext()); return currentSession; } /** * 从 repository 查询 session */ private S getRequestedSession() { /** 查询 sessionId 信息 */ List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this); /** 遍历查询 */ for (String sessionId : sessionIds) { S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId); if (session != null) { this.requestedSession = session; break; } } /** 返回持久化 session */ return this.requestedSession; } /** * http session 包装器 */ private final class HttpSessionWrapper extends HttpSessionAdapter<S> { HttpSessionWrapper(S session, ServletContext servletContext) { super(session, servletContext); } @Override public void invalidate() { super.invalidate(); /** session 不合法,从存储中删除信息 */ SessionRepositoryFilter.this.sessionRepository.deleteById(getId()); } } }
原理简单,装饰 HttpSession
, Session
失效时从存储中删除,在请求结束之后,存储 session
。
分布式环境下的认证方案: JWT
与分布式 Session
。
个人觉得两种方案都很好, JWT
,无状态,服务器不用维护 Session
信息,但如何让 JWT
失效是一个难题。
分布式 Session
,使用起来简单,但需要额外的存储空间。
实际应用中,要兼顾当前的业务场景与安全性进行方案的选择。