Spring Session Strategy 详解
HttpSessionStrategy
HttpSessionStrategy 定了三个方法如下:
/**
*从提供的{@link javax.servlet.http.HttpServletRequest}获取请求的会话ID。
* 例如,会话ID可能来自Cookie或请求标头。
*/
String getRequestedSessionId(HttpServletRequest request);
/**
*此方法在创建新会话时调用,并应通知客户端新会话ID是什么。
*例如,它可能创建一个带有会话ID的新Cookie,或者设置一个带有新会话ID值的HTTP响应头。
*注意这里的 session 是 org.springframework.session.Session
*/
void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response);
/**
*当会话无效时调用此方法,并应通知客户端会话标识不再有效。
*例如,它可能会删除其中包含会话ID的Cookie,或者设置一个带有空值的HTTP响应标头,指示客户端不再提交该会话ID。
*/
void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);
HeaderHttpSessionStrategy
HeaderHttpSessionStrategy 实现了 HttpSessionStrategy 接口。 它主要的功能是在 HTTP 请请求头中设置设置我们的 sessionId,已经从请求头中获取我们的 sessionId 。默人的请求头参数为 x-auth-token,同时提供了 setHeaderName(String headerName)方法一遍个性化设置存储 sessionId 的请求头参数。HeaderHttpSessionStrategy 通知客户端使session 失效的方式值是将 session 在HTTP头中的请求参数设置为空串("");
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
在某些业务场景下可能还需要进一步 HttpServletRequest、HttpServletResponse进行处理,自定义包装。 例如,CookieHttpSessionStrategy 自定义了如何完成网址重写,以选择在多个会话处于活动状态的情况下应使用哪个会话。因此定义此MultiHttpSessionStrategy 接口,此接口继承了HttpSessionStrategy接口、RequestResponsePostProcessor接口。其中在上文提到 HttpSessionStrategy 定义了 session 基本处理方法,而RequestResponsePostProcessor 定义 HttpServletRequest、HttpServletResponse 包装方法。
public interface MultiHttpSessionStrategy extends HttpSessionStrategy, RequestResponsePostProcessor {
}
RequestResponsePostProcessor
RequestResponsePostProcessor 提供一下两个方法便于开发人员去个性化定制我们的 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 参数名默认为 SESSION。CookieHttpSessionStrategy 实现了MultiHttpSessionStrategy 接口以及HttpSessionManager 接口。
当一个 session 创建后, HTTP 的响应头中将会以指定 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 过期以后,服务端将会给客户端返回一个过期的 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
服务端返回的结果为:
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
要使用原始会话,您可以创建一个没有HTTP参数的请求。 要使用新会话,可以使用HTTP参数_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 的参数名,可以自定义
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 中获取相应的 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 的参数名,默认为 "_s".
* 从当前的 request 获取 sessionParam 的值,如果为空返回默认的 DEFAULT_ALIAS (0) ,否则使用声明 ALIAS_PATTERN 进行正则匹配,验证参数值是否合法。
* 合法返回值,不能合法返回 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 同时也代表了最后的创建的 session 的 alias。
* 最大的 alias + 1 变得到了新的 alias,同时将 long 转化为 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进制字符串转化为 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) {
// 获取客户端当前已经存在 session id 集合,如果已经包含指定的 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 中。
* 构建新的 cookie 信息,写入 response.
*
* 从这里看出,调用此方法可以创建一个新的 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 不存在事,会主动设置一个新的 Set 到 request 中,
* 因此在上一个 onNewSession() 中便将参数中传递的 session id 已保存在里面。所以,在
* 我个人看来,此方法名应该改为 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;
}
/**
* 根据指定的 session ids 给指定的请求创建 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;
}
/**
* 当会话无效时调用此方法,并应通知客户端会话标识不再有效。
* 从 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 字符串分隔解析类型, 用于根据指定的模式分割字符串。
* 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 的个数一点是偶数。
* 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 中设置 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());
// 这里主要的是为了替换查询参数里面的 sessionParim 的值。
String replacement = isDefaultAlias ? "" : "$1"+encodedSessionAlias;
query = query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement);
/*
* 需要这一步是为了防止上在上述的 query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement)
* 中没有找到 sessionParam 对应的请求参数,这需要在 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);
}
}