spring-session原始碼解讀-5
session通用策略
Session在瀏覽器通常是通過cookie儲存的,cookie裡儲存了jessionid,代表使用者的session id。一個訪問路徑只有一個session cookie(事實上在客戶端就只有一個cookie,jsessionid是作為cookie值的一部分,這裡把cookie抽象成類似伺服器端的實現),也就是一個訪問路徑在一個瀏覽器上只有一個session,這是絕大多數容器對session的實現。而spring卻可以支援單瀏覽器多使用者session。下面就看看spring是怎樣去支援多使用者session的。
對多使用者session的支援
spring session通過增加session alias概念來實現多使用者session,每一個使用者都對映成一個session alias。當有多個session時,spring會生成“alias1 sessionid1 alias2 sessid2…….”這樣的cookie值結構。
spring session提交時如果有新session生成,會觸發onNewSession動作生成新的session cookie
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
Set<String> sessionIdsWritten = getSessionIdsWritten(request);
if(sessionIdsWritten.contains(session.getId())) {
return;
}
sessionIdsWritten.add(session.getId());
Map<String ,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
sessionIds.put(sessionAlias, session.getId());
Cookie sessionCookie = createSessionCookie(request, sessionIds);
response.addCookie(sessionCookie);
}
a) 確保已經存在cookie裡的session不會再被處理。
b) 生成一個包含所有alias的session id的map,並通過這個map構造新的session cookie值。
createSessionCookie會根據一個alias-sessionid的map去構造session cookie。
private Cookie createSessionCookie(HttpServletRequest request,
Map<String, String> sessionIds) {
//cookieName是"SESSION",spring的session cookie都是
//以"SESSION"命名的
Cookie sessionCookie = new Cookie(cookieName,"");
//省略部分非關鍵邏輯
if(sessionIds.isEmpty()) {
sessionCookie.setMaxAge(0);
return sessionCookie;
}
if(sessionIds.size() == 1) {
String cookieValue = sessionIds.values().iterator().next();
sessionCookie.setValue(cookieValue);
return sessionCookie;
}
StringBuffer buffer = new StringBuffer();
for(Map.Entry<String,String> entry : sessionIds.entrySet()) {
String alias = entry.getKey();
String id = entry.getValue();
buffer.append(alias);
buffer.append(" ");
buffer.append(id);
buffer.append(" ");
}
buffer.deleteCharAt(buffer.length()-1);
sessionCookie.setValue(buffer.toString());
return sessionCookie;
}
a) 當session被invalidate,可能會存在seesionids為空的情況,這種情況下將session cookie的最大失效時間設成立即。
b) 如果只有一個session id,則和普通session cookie一樣處理,cookie值就是session id。
c) 如果存在多個session id,則生成前文提到的session cookie值結構。
session cookie的獲取
getSessionIds方法會取出request裡的session cookie值,並且對每種可能的值結構進行相應的格式化生成一個key-value的map。
public Map<String,String> getSessionIds(HttpServletRequest request) {
Cookie session = getCookie(request, cookieName);
String sessionCookieValue = session == null ? "" : session.getValue();
Map<String,String> result = new LinkedHashMap<String,String>();
StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " ");
//單使用者cookie的情況
if(tokens.countTokens() == 1) {
result.put(DEFAULT_ALIAS, tokens.nextToken());
return result;
}
while(tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
if(!tokens.hasMoreTokens()) {
break;
}
String id = tokens.nextToken();
result.put(alias, id);
}
return result;
}
- 對單使用者session cookie的處理,只取出值,預設為是預設別名(預設為0)使用者的session。
- 對多使用者,則依據值結構的格式生成alias-sessionid的map。
- 以上兩種格式化都是對建立session的逆操作。
getCurrentSessionAlias用來獲取當前操作使用者。可以通過在request裡附加alias資訊,從而讓spring可以判斷是哪個使用者在操作。別名是通過”alias name=alias”這樣的格式傳入的,alias name預設是_s,可以通過setSessionAliasParamName(String)方法修改。我們可以在url上或者表單裡新增”_s=your user alias”這樣的形式來指明操作使用者的別名。如果不指明使用者別名,則會認為是預設使用者,可以通過setSessionAliasParamName(null)取消別名功能。
public String getCurrentSessionAlias(HttpServletRequest request) {
if(sessionParam == null) {
return DEFAULT_ALIAS;
}
String u = request.getParameter(sessionParam);
if(u == null) {
return DEFAULT_ALIAS;
}
if(!ALIAS_PATTERN.matcher(u).matches()) {
return DEFAULT_ALIAS;
}
return u;
}
觸發session提交
spring會通過兩個方面確保session提交:
a) response提交,主要包括response的sendRedirect和sendError以及其關聯的位元組字元流的flush和close方法。
abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
public OnCommittedResponseWrapper(HttpServletResponse response) {
super(response);
}
/**
* Implement the logic for handling the {@link javax.servlet.http.HttpServletResponse} being committed
*/
protected abstract void onResponseCommitted();
@Override
public final void sendError(int sc) throws IOException {
doOnResponseCommitted();
super.sendError(sc);
}
//sendRedirect處理類似sendError
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new SaveContextServletOutputStream(super.getOutputStream());
}
@Override
public PrintWriter getWriter() throws IOException {
return new SaveContextPrintWriter(super.getWriter());
}
private void doOnResponseCommitted() {
if(!disableOnCommitted) {
onResponseCommitted();
disableOnResponseCommitted();
} else if(logger.isDebugEnabled()){
logger.debug("Skip invoking on");
}
}
private class SaveContextPrintWriter extends PrintWriter {
private final PrintWriter delegate;
public SaveContextPrintWriter(PrintWriter delegate) {
super(delegate);
this.delegate = delegate;
}
public void flush() {
doOnResponseCommitted();
delegate.flush();
}
//close方法與flush方法類似
}
//SaveContextServletOutputStream處理同字元流
}
onResponseCommitted的實現由子類SessionRepositoryResponseWrapper提供
private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {
private final SessionRepositoryRequestWrapper request;
/**
* @param response the response to be wrapped
*/
public SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) {
super(response);
if(request == null) {
throw new IllegalArgumentException("request cannot be null");
}
this.request = request;
}
@Override
protected void onResponseCommitted() {
request.commitSession();
}
}
response提交後觸發了session提交。
b) SessionRespositoryFilter
僅僅通過response提交時觸發session提交併不能完全保證session的提交,有些情況下不會觸發response提交,比如對相應資源的訪問沒有servlet處理,這種情況就需要通過全域性filter做保證。
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
//省略
//filterChain會在所有filter都執行完畢後呼叫對應的servlet
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
//所有的處理都完成後提交session
wrappedRequest.commitSession()
}