Spring Session 原理分析

引言

本週,大部分時間去撰寫畢業設計中期報告,在部署Alice學生管理系統測試環境時想起本系統藉助Redis實現分佈式Sessionhtml

爲何要分佈式Session呢?html5

請參考下圖:java

image.png

當後臺集羣部署時,單機的Session維護就會出現問題。spring

假設登陸的認證受權發生在Tomcat A服務器上,Tomcat A在本地存儲了用戶Session,並簽發認證令牌,用於驗證用戶身份。安全

下次請求可能分發給Tomcat B服務器,而Tomcat B並無用戶Session,用戶攜帶的認證令牌無效,獲得401服務器

image.png

除了JWT無狀態的認證方式,另外一種主流的實現方案就是採用分佈式Sessioncookie

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

HttpSession內的存儲就是namevalue的鍵值對映射,且存在過時時間,這與Redis的設計相符合,分佈式Session一般使用Redis進行實現。session

不管是在單機環境,仍是在引入了Spring Session的集羣環境下,代碼實現都是相同的,即屏蔽了底層的細節,能夠在不改動HttpSession使用的相關代碼的狀況下,實現Session存儲環境的切換。app

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í)對策三千字,金榜題名五色春。

預祝黃庭祥考研順利,期待着下一個春天的好消息。

相關文章
相關標籤/搜索