Spring Session 原理分析

為什麼要分佈式 Session 呢?

請參考下圖:

Spring Session 原理分析

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

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

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

Spring Session 原理分析

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

<code>public interface HttpSession {
public void setAttribute(String name, Object value);
}/<code>

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

無論是在單機環境,還是在引入了 Spring Session 的集群環境下,代碼實現都是相同的,即屏蔽了底層的細節,可以在不改動 HttpSession 使用的相關代碼的情況下,實現 Session 存儲環境的切換。

<code>logger.debug("記錄當前用戶ID");
httpSession.setAttribute(UserService.USER_ID, persistUser.getId());/<code>

這聽起來很酷,那麼 Spring Session 具體是如何在不改動代碼的情況下進行 Session 存儲環境切換的呢?

原理

官方文檔: How HttpSession Integration Works - Spring Session

回顧

之前在學習 Spring Security 原理之時,我們從官方文檔中找到了這樣一張圖。

Spring Session 原理分析

所有的認證授權攔截都是基於 Filter 實現的,而這裡的 Spring Session ,也是基於 Filter 。

原理分析

因為 HttpSession 和 HttpServletRequest (獲取 HttpSession 的 API )都是接口,這意味著可以將這些 API 替換成自定義的實現。

核心源碼如下:

注:以下代碼中部分無關代碼已被刪減。

<code>public class SessionRepositoryFilter 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();
}
}
}
/<code>

response 封裝器核心源碼如下:

<code>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();
}
}/<code>

request 封裝器核心源碼如下:

<code>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 {

HttpSessionWrapper(S session, ServletContext servletContext) {
super(session, servletContext);
}

@Override
public void invalidate() {
super.invalidate();
/** session 不合法,從存儲中刪除信息 */
SessionRepositoryFilter.this.sessionRepository.deleteById(getId());
}

}
}
/<string>/<code>

原理簡單,裝飾 HttpSession , Session 失效時從存儲中刪除,在請求結束之後,存儲 session 。

總結

分佈式環境下的認證方案: JWT 與分佈式 Session 。

個人覺得兩種方案都很好, JWT ,無狀態,服務器不用維護 Session 信息,但如何讓 JWT 失效是一個難題。

分佈式 Session ,使用起來簡單,但需要額外的存儲空間。

實際應用中,要兼顧當前的業務場景與安全性進行方案的選擇。


分享到:


相關文章: