本週,大部分時間去撰寫畢業設計中期報告,在部署Alice
學生管理系統測試環境時想起本系統藉助Redis
實現分佈式Session
。html
爲何要分佈式Session
呢?html5
請參考下圖:java
當後臺集羣部署時,單機的Session
維護就會出現問題。spring
假設登陸的認證受權發生在Tomcat A
服務器上,Tomcat A
在本地存儲了用戶Session
,並簽發認證令牌,用於驗證用戶身份。安全
下次請求可能分發給Tomcat B
服務器,而Tomcat B
並無用戶Session
,用戶攜帶的認證令牌無效,獲得401
。服務器
除了JWT
無狀態的認證方式,另外一種主流的實現方案就是採用分佈式Session
。cookie
public interface HttpSession { public void setAttribute(String name, Object value); }
HttpSession
內的存儲就是name
與value
的鍵值對映射,且存在過時時間,這與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
原理之時,咱們從官方文檔中找到了這樣一張圖。
全部的認證受權攔截都是基於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
,使用起來簡單,但須要額外的存儲空間。
實際應用中,要兼顧當前的業務場景與安全性進行方案的選擇。
丹墀(chí)對策三千字,金榜題名五色春。
預祝黃庭祥考研順利,期待着下一個春天的好消息。