6

引言

本周,大部分时间去撰写毕业设计中期报告,在部署Alice学生管理系统测试环境时想起本系统借助Redis实现分布式Session

为什么要分布式Session呢?

请参考下图:

image.png

当后台集群部署时,单机的Session维护就会出现问题。

假设登录的认证授权发生在Tomcat A服务器上,Tomcat A在本地存储了用户Session,并签发认证令牌,用于验证用户身份。

下次请求可能分发给Tomcat B服务器,而Tomcat B并没有用户Session,用户携带的认证令牌无效,得到401

image.png

除了JWT无状态的认证方式,另一种主流的实现方案就是采用分布式Session

public interface HttpSession {
    public void setAttribute(String name, Object value);
}

HttpSession内的存储就是namevalue的键值对映射,且存在过期时间,这与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原理之时,我们从官方文档中找到了这样一张图。

image.png

所有的认证授权拦截都是基于Filter实现的,而这里的Spring Session,也是基于Filter

原理分析

因为HttpSessionHttpServletRequest(获取HttpSessionAPI)都是接口,这意味着可以将这些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());
    }
  }
}

原理简单,装饰HttpSessionSession失效时从存储中删除,在请求结束之后,存储session

总结

分布式环境下的认证方案:JWT与分布式Session

个人觉得两种方案都很好,JWT,无状态,服务器不用维护Session信息,但如何让JWT失效是一个难题。

分布式Session,使用起来简单,但需要额外的存储空间。

实际应用中,要兼顾当前的业务场景与安全性进行方案的选择。

丹墀(chí)对策三千字,金榜题名五色春。

预祝黄庭祥考研顺利,期待着下一个春天的好消息。


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。


引用和评论

0 条评论