HttpSessionStrategy
HttpSessionStrategy 定了三個方法如下:
/**
*從提供的{@link javax.servlet.http.HttpServletRequest}獲取請求的會話ID。
* 例如,會話ID可能來自Cookie或請求標頭。
*/
String getRequestedSessionId(HttpServletRequest request);
/**
*此方法在創(chuàng)建新會話時調(diào)用,并應(yīng)通知客戶端新會話ID是什么。
*例如,它可能創(chuàng)建一個帶有會話ID的新Cookie,或者設(shè)置一個帶有新會話ID值的HTTP響應(yīng)頭。
*注意這里的 session 是 org.springframework.session.Session
*/
void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response);
/**
*當會話無效時調(diào)用此方法,并應(yīng)通知客戶端會話標識不再有效。
*例如,它可能會刪除其中包含會話ID的Cookie,或者設(shè)置一個帶有空值的HTTP響應(yīng)標頭,指示客戶端不再提交該會話ID。
*/
void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);
HeaderHttpSessionStrategy
HeaderHttpSessionStrategy 實現(xiàn)了 HttpSessionStrategy 接口。 它主要的功能是在 HTTP 請請求頭中設(shè)置設(shè)置我們的 sessionId,已經(jīng)從請求頭中獲取我們的 sessionId 。默人的請求頭參數(shù)為 x-auth-token,同時提供了 setHeaderName(String headerName)方法一遍個性化設(shè)置存儲 sessionId 的請求頭參數(shù)。HeaderHttpSessionStrategy 通知客戶端使session 失效的方式值是將 session 在HTTP頭中的請求參數(shù)設(shè)置為空串("");
private String headerName = "x-auth-token";
public String getRequestedSessionId(HttpServletRequest request) {
return request.getHeader(headerName);
}
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
response.setHeader(headerName, session.getId());
}
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
response.setHeader(headerName, "");
}
/**
* The name of the header to obtain the session id from. Default is "x-auth-token".
* @param headerName the name of the header to obtain the session id from.
*/
public void setHeaderName(String headerName) {
Assert.notNull(headerName, "headerName cannot be null");
this.headerName = headerName;
}
MultiHttpSessionStrategy
在某些業(yè)務(wù)場景下可能還需要進一步 HttpServletRequest、HttpServletResponse進行處理,自定義包裝。 例如,CookieHttpSessionStrategy 自定義了如何完成網(wǎng)址重寫,以選擇在多個會話處于活動狀態(tài)的情況下應(yīng)使用哪個會話。因此定義此MultiHttpSessionStrategy 接口,此接口繼承了HttpSessionStrategy接口、RequestResponsePostProcessor接口。其中在上文提到 HttpSessionStrategy 定義了 session 基本處理方法,而RequestResponsePostProcessor 定義 HttpServletRequest、HttpServletResponse 包裝方法。
public interface MultiHttpSessionStrategy extends HttpSessionStrategy, RequestResponsePostProcessor {
}
RequestResponsePostProcessor
RequestResponsePostProcessor 提供一下兩個方法便于開發(fā)人員去個性化定制我們的 HttpServletRequest、HttpServletResponse 對象。
/**包裝HttpServletRequest */
HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response);
/**包裝HttpServletResponse */
HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response);
HttpSessionManager
HttpSessionManager 維護著一組 session Id 別名映射,以支持多個同時會話管理。
/** 從 HttpServletRequest 獲取當前回話的 session id 別名*/
String getCurrentSessionAlias(HttpServletRequest request);
/** 從{@link HttpServletRequest}中獲取session Alias 到session id的映射,*/
Map<String, String> getSessionIds(HttpServletRequest request);
/** 通過指定的 sessionAlias 加密 url */
String encodeURL(String url, String sessionAlias);
/**
* 通過 HttpServletRequest 獲取一個新的,唯一的 session Alias。
* <code>
* String newAlias = httpSessionManager.getNewSessionAlias(request);
* String addAccountUrl = httpSessionManager.encodeURL("./", newAlias);
* </code>
*/
String getNewSessionAlias(HttpServletRequest request);
CookieHttpSessionStrategy
提供通過 cookie 的方式來傳遞 sessionId。cookie 參數(shù)名默認為 SESSION。CookieHttpSessionStrategy 實現(xiàn)了MultiHttpSessionStrategy 接口以及HttpSessionManager 接口。
當一個 session 創(chuàng)建后, HTTP 的響應(yīng)頭中將會以指定 cookie名稱來存儲 sessionID,此cookie 將會被標識為會話cookie,同時以 context path 作為該 cookie 的 path,并被標識了HTTPOnly。此外如果 javax.servlet.http.HttpServletRequest#isSecure()返回true,則cookie將被標記為Secure。如下:
HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
客戶端在訪問的時候必須帶上此cookie:
GET /messages/ HTTP/1.1
Host: example.com
Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
當 session 過期以后,服務(wù)端將會給客戶端返回一個過期的 cookie,如下:
HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
此外 CookieHttpSessionStrategy 默認支持多個同時會話。 一旦與瀏覽器建立會話,可以通過為{@link #setSessionAliasParamName(String)}指定唯一值來啟動另一個會話。 請求如下:
GET /messages/?_s=1416195761178 HTTP/1.1
Host: example.com
Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
服務(wù)端返回的結(jié)果為:
HTTP/1.1 200 OK
Set-Cookie: SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d"; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
要使用原始會話,您可以創(chuàng)建一個沒有HTTP參數(shù)的請求。 要使用新會話,可以使用HTTP參數(shù)_s = 1416195761178的請求。默認情況將會重寫urls以包含當前使用的session。
CookieHttpSessionStrategy 源碼解析
public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy, HttpSessionManager {
private static final String SESSION_IDS_WRITTEN_ATTR = CookieHttpSessionStrategy.class.getName().concat(".SESSIONS_WRITTEN_ATTR");
static final String DEFAULT_ALIAS = "0";
static final String DEFAULT_SESSION_ALIAS_PARAM_NAME = "_s";
// session alias 的正則表達式
private Pattern ALIAS_PATTERN = Pattern.compile("^[\\w-]{1,50}$");
// 用于存儲 session 信息的 cookie 名,可以地址定義。
private String cookieName = "SESSION";
// session alias 的參數(shù)名,可以自定義
private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
private boolean isServlet3Plus = isServlet3();
/**
* 獲取當前回話的 session id.
* 1.首先獲取當前客戶端所有的session alias 到 session id 的映射 sessionIds (Map).
* 2.獲取當前回話的 session alias
* 3.使用獲取到的 session alias 從 sessionIds 中獲取相應(yīng)的 session id
*/
public String getRequestedSessionId(HttpServletRequest request) {
Map<String,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
//通過sessionAlias 獲取 session id
return sessionIds.get(sessionAlias);
}
/**
* 獲取當前 session 的 alias。
* 當前 sessionParam 不存在時返回默認的 DEFAULT_ALIAS (0) 。 sessionParam 用于在 http 請求中保存當前 session 的 alias 的參數(shù)名,默認為 "_s".
* 從當前的 request 獲取 sessionParam 的值,如果為空返回默認的 DEFAULT_ALIAS (0) ,否則使用聲明 ALIAS_PATTERN 進行正則匹配,驗證參數(shù)值是否合法。
* 合法返回值,不能合法返回 DEFAULT_ALIAS。
*
*/
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 alias。
*/
public String getNewSessionAlias(HttpServletRequest request) {
// 獲取當前請求客戶端所有的 session alias 到 session ids 的映射中所有的 alias,如果為空返回 DEFAULT_ALIAS
Set<String> sessionAliases = getSessionIds(request).keySet();
if(sessionAliases.isEmpty()) {
return DEFAULT_ALIAS;
}
/*
* 遍歷所有的 alias,選取值最大的 alias。這個最大的 alias 同時也代表了最后的創(chuàng)建的 session 的 alias。
* 最大的 alias + 1 變得到了新的 alias,同時將 long 轉(zhuǎn)化為 16 進制字符串
*/
long lastAlias = Long.decode(DEFAULT_ALIAS);
for(String alias : sessionAliases) {
long selectedAlias = safeParse(alias);
if(selectedAlias > lastAlias) {
lastAlias = selectedAlias;
}
}
return Long.toHexString(lastAlias + 1);
}
/**
* 將16進制字符串轉(zhuǎn)化為 long 型。
* @param hex
* @return
*/
private long safeParse(String hex) {
try {
return Long.decode("0x" + hex);
} catch(NumberFormatException notNumber) {
return 0;
}
}
/**
* 向客戶端重寫新的session cookie 信息。
*
*/
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
// 獲取客戶端當前已經(jīng)存在 session id 集合,如果已經(jīng)包含指定的 session id 直接返回。
Set<String> sessionIdsWritten = getSessionIdsWritten(request);
if(sessionIdsWritten.contains(session.getId())) {
return;
}
/*
* 將session id 加入 sessionIdsWritten 集合中,獲取請求中的 session alias 和 session id 的映射信息
* 獲取當前 session 的 session alias ,并將其添加到 session alias 和 session id 的映射 map 中。
* 構(gòu)建新的 cookie 信息,寫入 response.
*
* 從這里看出,調(diào)用此方法可以創(chuàng)建一個新的 session 或者改變當前請求的 session 。因為 getCurrentSessionAlias(request) 獲取的當前的 session alias。
*/
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);
}
@SuppressWarnings("unchecked")
private Set<String> getSessionIdsWritten(HttpServletRequest request) {
/*
* 這里要特別注意,改方法不僅僅是獲取 request 中 SESSION_IDS_WRITTEN_ATTR 屬性值 。
* 當 SESSION_IDS_WRITTEN_ATTR 不存在事,會主動設(shè)置一個新的 Set 到 request 中,
* 因此在上一個 onNewSession() 中便將參數(shù)中傳遞的 session id 已保存在里面。所以,在
* 我個人看來,此方法名應(yīng)該改為 getSessionIdsWrittenOrNew() 之類的。
*/
Set<String> sessionsWritten = (Set<String>) request.getAttribute(SESSION_IDS_WRITTEN_ATTR);
if(sessionsWritten == null) {
sessionsWritten = new HashSet<String>();
request.setAttribute(SESSION_IDS_WRITTEN_ATTR, sessionsWritten);
}
return sessionsWritten;
}
/**
* 根據(jù)指定的 session ids 給指定的請求創(chuàng)建 session cookie. 特別注意,當sessionIds為空時,相當于使當前 session 失效
* @param request
* @param sessionIds
* @return
*/
private Cookie createSessionCookie(HttpServletRequest request,
Map<String, String> sessionIds) {
Cookie sessionCookie = new Cookie(cookieName,"");
if(isServlet3Plus) {
sessionCookie.setHttpOnly(true);
}
sessionCookie.setSecure(request.isSecure());
sessionCookie.setPath(cookiePath(request));
// TODO set domain?
// 使 session 失效
if(sessionIds.isEmpty()) {
sessionCookie.setMaxAge(0);
return sessionCookie;
}
// 當客戶端只有一個請求回話時,我們的 session cookie 里面并沒有維護 session alias
if(sessionIds.size() == 1) {
String cookieValue = sessionIds.values().iterator().next();
sessionCookie.setValue(cookieValue);
return sessionCookie;
}
/*
* 當客戶端維護多個請求回話時
* 我們的 session cookie 中 session id 和 session alias 的組合模式如下:SESSION="alias1 sessionId1 alias2 sessionId2 ......";
* 例如:SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d";
*/
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;
}
/**
* 當會話無效時調(diào)用此方法,并應(yīng)通知客戶端會話標識不再有效。
* 從 sessionId 中刪除當前回話,并重寫 cookie。
**/
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
Map<String,String> sessionIds = getSessionIds(request);
String requestedAlias = getCurrentSessionAlias(request);
// 這里移除了當前 session alias 和 session id的映射,并重寫了cookie,但是沒有移除 request 中 SESSION_IDS_WRITTEN_ATTR 屬性中的 session id
sessionIds.remove(requestedAlias);
Cookie sessionCookie = createSessionCookie(request, sessionIds);
response.addCookie(sessionCookie);
}
/**
* Sets the name of the HTTP parameter that is used to specify the session
* alias. If the value is null, then only a single session is supported per
* browser.
*
* @param sessionAliasParamName
* the name of the HTTP parameter used to specify the session
* alias. If null, then ony a single session is supported per
* browser.
*/
public void setSessionAliasParamName(String sessionAliasParamName) {
this.sessionParam = sessionAliasParamName;
}
/**
* Sets the name of the cookie to be used
* @param cookieName the name of the cookie to be used
*/
public void setCookieName(String cookieName) {
if(cookieName == null) {
throw new IllegalArgumentException("cookieName cannot be null");
}
this.cookieName = cookieName;
}
/**
* Retrieve the first cookie with the given name. Note that multiple
* cookies can have the same name but different paths or domains.
* @param request current servlet request
* @param name cookie name
* @return the first cookie with the given name, or {@code null} if none is found
*/
private static Cookie getCookie(HttpServletRequest request, String name) {
if(request == null) {
throw new IllegalArgumentException("request cannot be null");
}
Cookie cookies[] = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie;
}
}
}
return null;
}
private static String cookiePath(HttpServletRequest request) {
return request.getContextPath() + "/";
}
/**
* 獲取當前請求客戶端所有的 session alias 到 session ids 的映射。session alias 和 sessoin ids 保存在名為“SESSION”的 cookie 中, 如下:
* <pre>
* HTTP/1.1 200 OK
* Set-Cookie: SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d"; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
* </pre>
*/
public Map<String,String> getSessionIds(HttpServletRequest request) {
// 獲取 session cookie 字符串
Cookie session = getCookie(request, cookieName);
// 如果不存在 session 返回空串,如果存在獲取session cookie 的值。
String sessionCookieValue = session == null ? "" : session.getValue();
// 申明一個 map 集合用于保存 session alias 到 session id 的映射
Map<String,String> result = new LinkedHashMap<String,String>();
/*
* StringTokenizer 字符串分隔解析類型, 用于根據(jù)指定的模式分割字符串。
* session cookie 的值中的分割符為“空格字符串”,因此這里使用它解析。
*/
StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " ");
/*
* 如果分割后的值只有一個,那么說明當前客戶端只開啟一個回話,tokens里面的唯一的元素便是當前的 session id。
* 當只有一個會話是,使用 DEFAULT_ALIAS(0)作為當前 session ids 的別名。
*/
if(tokens.countTokens() == 1) {
result.put(DEFAULT_ALIAS, tokens.nextToken());
return result;
}
/*
* 當tokens 大于1時,說明當前客戶端開啟了多個回話,此時 tokens 的個數(shù)一點是偶數(shù)。
* session cookie 中的session id 和 session alias 的組合模式如下:SESSION="alias1 sessionId1 alias2 sessionId2 ......";
* 例如:SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d";
*/
while(tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
if(!tokens.hasMoreTokens()) {
break;
}
String id = tokens.nextToken();
result.put(alias, id);
}
return result;
}
/**
* 在 request 中設(shè)置 session 管理策略
*/
public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
request.setAttribute(HttpSessionManager.class.getName(), this);
return request;
}
/**
* 封裝 HttpServletResponse
*/
public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) {
return new MultiSessionHttpServletResponse(response, request);
}
class MultiSessionHttpServletResponse extends HttpServletResponseWrapper {
private final HttpServletRequest request;
public MultiSessionHttpServletResponse(HttpServletResponse response, HttpServletRequest request) {
super(response);
this.request = request;
}
@Override
public String encodeRedirectURL(String url) {
url = super.encodeRedirectURL(url);
return CookieHttpSessionStrategy.this.encodeURL(url, getCurrentSessionAlias(request));
}
@Override
public String encodeURL(String url) {
url = super.encodeURL(url);
String alias = getCurrentSessionAlias(request);
return CookieHttpSessionStrategy.this.encodeURL(url, alias);
}
}
public String encodeURL(String url, String sessionAlias) {
String encodedSessionAlias = urlEncode(sessionAlias);
int queryStart = url.indexOf("?");
boolean isDefaultAlias = DEFAULT_ALIAS.equals(encodedSessionAlias);
if(queryStart < 0) {
return isDefaultAlias ? url : url + "?" + sessionParam + "=" + encodedSessionAlias;
}
String path = url.substring(0, queryStart);
String query = url.substring(queryStart + 1, url.length());
// 這里主要的是為了替換查詢參數(shù)里面的 sessionParim 的值。
String replacement = isDefaultAlias ? "" : "$1"+encodedSessionAlias;
query = query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement);
/*
* 需要這一步是為了防止上在上述的 query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement)
* 中沒有找到 sessionParam 對應(yīng)的請求參數(shù),這需要在 query 后面拼接 sessionParam。
* 怎么造成這種情景的呢,大概是客戶端開啟一個新的 session。
*/
if(!isDefaultAlias && url.endsWith(query)) {
// no existing alias
if(!(query.endsWith("&") || query.length() == 0)) {
query += "&";
}
query += sessionParam + "=" + encodedSessionAlias;
}
return path + "?" + query;
}
private String urlEncode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Returns true if the Servlet 3 APIs are detected.
* @return
*/
private boolean isServlet3() {
try {
ServletRequest.class.getMethod("startAsync");
return true;
} catch(NoSuchMethodException e) {}
return false;
}
public static void main(String[] args) {
String query = "&_s=ygkhldjg&123=jkljkj";
query = query.replaceFirst( "((^|&)_s=)([^&]+)?", "$1ranqi");
System.out.println(query);
}
}