蘇格拉底曰:我唯一知道的,就是自己一無所知
源頭
最近在翻閱Springboot Security板塊中的會話管理器過濾器SessionManagementFilter源碼的時候,發現其會對單用戶的多會話進行校驗控制,比如其下的某個策略ConcurrentSessionControlAuthenticationStrategy,節選部分代碼
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) {
// 獲取單用戶的多會話
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
authentication.getPrincipal(), false);
// 一系列判斷
int sessionCount = sessions.size();
int allowedSessions = getMaximumSessionsForThisUser(authentication);
....
....
// session超出后的操作,一般是拋異常結束filter的過濾
allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}
筆者一般的思維是認為單個校驗通過的用戶有單一的會話,為何會有多個會話呢?那多個會話其又是如何管理的呢?帶着疑問探究下HttpSession的概念
何為HttpSession
通俗的理解應該是基於HTTP協議而產生的服務器級別的對象。其獨立於客戶端發的請求,並不是客戶端每一次的請求便會創建此對象,也不是客戶端關閉了就會被注銷。
故其依賴於HTTP服務器的運行,是獨立於客戶端的一種會話。目的也是保存公共的屬性供頁面間跳轉的參數傳遞。
如何使用HttpSession
HttpSession主要是通過HttpServletRequest#getSession()方法來創建,且只依賴於此方法的創建。一般都是用戶校驗通過后,應用才會調用此方法保存一些公共的屬性,方便頁面間傳遞。
HttpSession的實現機制
為了理解清楚上述的疑問,那么HttpSession的實現機制必須深入的了解一下。因為其依賴於相應的HTTP服務器,就以Springboot內置的Tomcat服務器作為分析的入口吧。
代碼層
筆者以唯一入口HttpServletRequest#getSession()方法為源頭,倒推其代碼實現邏輯,大致梳理了下Tomcat服務器的HTTP請求步驟
AbstractEndpoint作為服務的創建入口,其子類NioEndpoint則采用NIO思想創建TCP服務並運行多個Poller線程用於接收客戶端(瀏覽器)的請求-->
通過Poller#processSocket()方法調用內部類SocketProcessor來間接引用AbstractProtocol內部類ConnectionHandler處理具體的請求-->
HTTP相關的請求則交由AbstractHttp11Protocol#createProcessor()方法創建Http11Processor對象處理---->
Http11Processor引用CoyoteAdapter對象來包裝成org.apache.catalina.connector.Request對象來最終處理創建HttpSession-->
優先解析URL中的JSESSIONID參數,如果沒有則嘗試獲取客戶端Cookie中的JSESSIONID鍵值,最終存入至相應Session對象屬性sessionId中,避免對來自同一來源的客戶端重復創建HttpSession
基於上述的步驟用戶在獲取HttpSession對象時,會調用Request#doGetSession()方法來創建,具體的筆者不分析了。
總而言之,HttpSession的關鍵之處在於其對應的sessionId,每個HttpSession都會有獨一無二的sessionId與之對應,至於sessionId的創建讀者可自行分析,只需要知道其在應用服務期間會對每個HttpSession創建唯一的sessionId即可。
保存方式
上述講解了HttpSession的獲取方式是基於sessionId的,那么肯定有一個出口去保存相應的鍵值對,仔細一看發現其是基於cookie去實現的,附上Request#doGetSession()方法關鍵源碼
protected Session doGetSession(boolean create) {
.....
.....
// session不為空且支持cookie機制
if (session != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
// 默認創建Key為JSESSIONID的Cookie對象,並設置maxAge=-1
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
很明顯,由上述的代碼可知,HttpSession的流通還需要依賴Cookie機制的使用。此處談及一下Cookie對象中的maxAge,可以看下其API說明
/**
* Sets the maximum age of the cookie in seconds.
* <p>
* A positive value indicates that the cookie will expire after that many
* seconds have passed. Note that the value is the <i>maximum</i> age when
* the cookie will expire, not the cookie's current age.
* <p>
* A negative value means that the cookie is not stored persistently and
* will be deleted when the Web browser exits. A zero value causes the
* cookie to be deleted.
*
* @param expiry
* an integer specifying the maximum age of the cookie in
* seconds; if negative, means the cookie is not stored; if zero,
* deletes the cookie
* @see #getMaxAge
*/
public void setMaxAge(int expiry) {
maxAge = expiry;
}
默認maxAge值為-1,即當瀏覽器進程重開之前,此對應的JSESSIONID的cookie值都會在訪問服務應用的時候被帶上。
由此處其實可以理解,如果多次重開瀏覽器進程並登錄應用,則會出現單用戶有多個session的情況。所以才有了限制Session最大可擁有量
HttpSession的管理
這里淺談下Springboot Security中對Session的管理,主要是針對單個用戶多session的情況。由HttpSecurity#sessionManagement()來進行相應的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 單用戶最大session數為2
http.sessionManagement().maximumSessions(2);
}
經過上述的配置,便會引入兩個關於session管理的過濾鏈,筆者按照過濾順序分開淺析
ConcurrentSessionFilter
主要是針對過期的session進行相應的注銷以及退出操作,看下關鍵的處理代碼
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 獲取HttpSession
HttpSession session = request.getSession(false);
if (session != null) {
SessionInformation info = sessionRegistry.getSessionInformation(session
.getId());
if (info != null) {
// 如果設置為過期標志,則開始清理操作
if (info.isExpired()) {
// 默認使用SecurityContextLogoutHandler處理退出操作,內含session注銷
doLogout(request, response);
// 事件推送,默認是直接輸出session數過多的信息
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
else {
// Non-expired - update last request date/time
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}
chain.doFilter(request, response);
}
前文也提及,如果服務應用期間,要注銷session,只能調用相應的session.invalid()方法。直接看下SecurityContextLogoutHandler#logout()源碼
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
// 注銷
session.invalidate();
}
}
if (clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
}
// 清理上下文
SecurityContextHolder.clearContext();
}
SessionManagementFilter
筆者只展示ConcurrentSessionControlAuthenticationStrategy策略類用於展示session的最大值校驗
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) {
// 獲取當前校驗通過的用戶所關聯的session數量
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
authentication.getPrincipal(), false);
int sessionCount = sessions.size();
// 最大session支持,可配置
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
// Only permit it though if this request is associated with one of the
// already registered sessions
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
// If the session is null, a new one will be created by the parent class,
// exceeding the allowed number
}
// 超出對應數的處理
allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}
繼續跟蹤allowableSessionsExceeded()方法
protected void allowableSessionsExceeded(List<SessionInformation> sessions,
int allowableSessions, SessionRegistry registry)
throws SessionAuthenticationException {
// 1.要么拋異常
if (exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(messages.getMessage(
"ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] { Integer.valueOf(allowableSessions) },
"Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used session, and mark it for invalidation
SessionInformation leastRecentlyUsed = null;
for (SessionInformation session : sessions) {
if ((leastRecentlyUsed == null)
|| session.getLastRequest()
.before(leastRecentlyUsed.getLastRequest())) {
leastRecentlyUsed = session;
}
}
// 2.要么設置對應的expired為true,最后交由上述的ConcurrentSessionFilter來處理
leastRecentlyUsed.expireNow();
}
關於session的保存,大家可以關注RegisterSessionAuthenticationStrategy注冊策略,其是排在上述的策略之后的,就是先判斷再注冊,很順暢的邏輯。筆者此處就不分析了,讀者可自行分析
小結
HttpSession是HTTP服務中比較常用的對象,理解它的含義以及應用邏輯可以幫助我們更好的使用它。以蘇格拉底的話來說就是我唯一知道的,就是自己一無所知