/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.web.http;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.session.Session;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
import org.springframework.util.Assert;
/**
* A {@link HttpSessionStrategy} that uses a cookie to obtain the session from.
* Specifically, this implementation will allow specifying a cookie name using
* {@link CookieHttpSessionStrategy#setCookieName(String)}. The default is "SESSION".
*
* When a session is created, the HTTP response will have a cookie with the specified
* cookie name and the value of the session id. The cookie will be marked as a session
* cookie, use the context path for the path of the cookie, marked as HTTPOnly, and if
* {@link javax.servlet.http.HttpServletRequest#isSecure()} returns true, the cookie will
* be marked as secure. For example:
*
* <pre>
* HTTP/1.1 200 OK
* Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
* </pre>
*
* The client should now include the session in each request by specifying the same cookie
* in their request. For example:
*
* <pre>
* GET /messages/ HTTP/1.1
* Host: example.com
* Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
* </pre>
*
* When the session is invalidated, the server will send an HTTP response that expires the
* cookie. For example:
*
* <pre>
* HTTP/1.1 200 OK
* Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
* </pre>
*
* <h2>Supporting Multiple Simultaneous Sessions</h2>
*
* <p>
* By default multiple sessions are also supported. Once a session is established with the
* browser, another session can be initiated by specifying a unique value for the
* {@link #setSessionAliasParamName(String)}. For example, a request to:
* </p>
*
* <pre>
* GET /messages/?_s=1416195761178 HTTP/1.1
* Host: example.com
* Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
* </pre>
*
* Will result in the following response:
*
* <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>
*
* <p>
* To use the original session a request without the HTTP parameter u can be made. To use
* the new session, a request with the HTTP parameter _s=1416195761178 can be used. By
* default URLs will be rewritten to include the currently selected session.
* </p>
*
* <h2>Selecting Sessions</h2>
*
* <p>
* Sessions can be managed by using the HttpSessionManager and SessionRepository. If you
* are not using Spring in the rest of your application you can obtain a reference from
* the HttpServletRequest attributes. An example is provided below:
* </p>
*
* <code>
* HttpSessionManager sessionManager =
* (HttpSessionManager) req.getAttribute(HttpSessionManager.class.getName());
* SessionRepository<Session> repo =
* (SessionRepository<Session>) req.getAttribute(SessionRepository.class.getName());
*
* String currentSessionAlias = sessionManager.getCurrentSessionAlias(req);
* Map<String, String> sessionIds = sessionManager.getSessionIds(req);
* String newSessionAlias = String.valueOf(System.currentTimeMillis());
*
* String contextPath = req.getContextPath();
* List<Account> accounts = new ArrayList<>();
* Account currentAccount = null; for(Map.Entry<String, String> entry :
* sessionIds.entrySet()) { String alias = entry.getKey(); String sessionId =
* entry.getValue();
* </code>
*
* Session session = repo.getSession(sessionId); if(session == null) { continue; }
*
* String username = session.getAttribute("username"); if(username == null) {
* newSessionAlias = alias; continue; }
*
* String logoutUrl = sessionManager.encodeURL("./logout", alias); String switchAccountUrl
* = sessionManager.encodeURL("./", alias); Account account = new Account(username,
* logoutUrl, switchAccountUrl); if(currentSessionAlias.equals(alias)) { currentAccount =
* account; } else { accounts.add(account); } }
*
* req.setAttribute("currentAccount", currentAccount); req.setAttribute("addAccountUrl",
* sessionManager.encodeURL(contextPath, newSessionAlias)); req.setAttribute("accounts",
* accounts); }
*
*
* @author Rob Winch
* @since 1.0
*/
public final class CookieHttpSessionStrategy
implements MultiHttpSessionStrategy, HttpSessionManager {
/**
* The default delimiter for both serialization and deserialization.
*/
private static final String DEFAULT_DELIMITER = " ";
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";
private static final Pattern ALIAS_PATTERN = Pattern.compile("^[\\w-]{1,50}$");
private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();
/**
* The delimiter between a session alias and a session id when reading a cookie value.
* The default value is " ".
*/
private String deserializationDelimiter = DEFAULT_DELIMITER;
/**
* The delimiter between a session alias and a session id when writing a cookie value.
* The default is " ".
*/
private String serializationDelimiter = DEFAULT_DELIMITER;
public String getRequestedSessionId(HttpServletRequest request) {
Map<String, String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
return sessionIds.get(sessionAlias);
}
public String getCurrentSessionAlias(HttpServletRequest request) {
if (this.sessionParam == null) {
return DEFAULT_ALIAS;
}
String u = safeGetParameter(request, sessionParam);
if (u == null) {
return DEFAULT_ALIAS;
}
if (!ALIAS_PATTERN.matcher(u).matches()) {
return DEFAULT_ALIAS;
}
return u;
}
public String safeGetParameter(final HttpServletRequest request, final String parameter) {
return request.getQueryString() == null || !request.getQueryString().contains(parameter) ? null : request
.getParameter(parameter);
}
public String getNewSessionAlias(HttpServletRequest request) {
Set<String> sessionAliases = getSessionIds(request).keySet();
if (sessionAliases.isEmpty()) {
return DEFAULT_ALIAS;
}
long lastAlias = Long.decode(DEFAULT_ALIAS);
for (String alias : sessionAliases) {
long selectedAlias = safeParse(alias);
if (selectedAlias > lastAlias) {
lastAlias = selectedAlias;
}
}
return Long.toHexString(lastAlias + 1);
}
private long safeParse(String hex) {
try {
return Long.decode("0x" + hex);
}
catch (NumberFormatException notNumber) {
return 0;
}
}
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());
String cookieValue = createSessionCookieValue(sessionIds);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, cookieValue));
}
@SuppressWarnings("unchecked")
private Set<String> getSessionIdsWritten(HttpServletRequest request) {
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;
}
private String createSessionCookieValue(Map<String, String> sessionIds) {
if (sessionIds.isEmpty()) {
return "";
}
if (sessionIds.size() == 1 && sessionIds.keySet().contains(DEFAULT_ALIAS)) {
return sessionIds.values().iterator().next();
}
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(this.serializationDelimiter);
buffer.append(id);
buffer.append(this.serializationDelimiter);
}
buffer.deleteCharAt(buffer.length() - 1);
return buffer.toString();
}
public void onInvalidateSession(HttpServletRequest request,
HttpServletResponse response) {
Map<String, String> sessionIds = getSessionIds(request);
String requestedAlias = getCurrentSessionAlias(request);
sessionIds.remove(requestedAlias);
String cookieValue = createSessionCookieValue(sessionIds);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, cookieValue));
}
/**
* 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 {@link CookieSerializer} to be used.
*
* @param cookieSerializer the cookieSerializer to set. Cannot be null.
*/
public void setCookieSerializer(CookieSerializer cookieSerializer) {
Assert.notNull(cookieSerializer, "cookieSerializer cannot be null");
this.cookieSerializer = cookieSerializer;
}
/**
* Sets the name of the cookie to be used.
* @param cookieName the name of the cookie to be used
* @deprecated use {@link #setCookieSerializer(CookieSerializer)}
*/
@Deprecated
public void setCookieName(String cookieName) {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName(cookieName);
this.cookieSerializer = serializer;
}
/**
* Sets the delimiter between a session alias and a session id when deserializing a
* cookie. The default is " " This is useful when using
* <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> for writing the cookies
* which doesn't allow for spaces in the cookie values.
*
* @param delimiter the delimiter to set (i.e. "_ " will try a delimeter of either "_"
* or " ")
*/
public void setDeserializationDelimiter(String delimiter) {
this.deserializationDelimiter = delimiter;
}
/**
* Sets the delimiter between a session alias and a session id when deserializing a
* cookie. The default is " ". This is useful when using
* <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> for writing the cookies
* which doesn't allow for spaces in the cookie values.
*
* @param delimiter the delimiter to set (i.e. "_")
*/
public void setSerializationDelimiter(String delimiter) {
this.serializationDelimiter = delimiter;
}
public Map<String, String> getSessionIds(HttpServletRequest request) {
List<String> cookieValues = this.cookieSerializer.readCookieValues(request);
String sessionCookieValue = cookieValues.isEmpty() ? ""
: cookieValues.iterator().next();
Map<String, String> result = new LinkedHashMap<String, String>();
StringTokenizer tokens = new StringTokenizer(sessionCookieValue,
this.deserializationDelimiter);
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;
}
public HttpServletRequest wrapRequest(HttpServletRequest request,
HttpServletResponse response) {
request.setAttribute(HttpSessionManager.class.getName(), this);
return request;
}
public HttpServletResponse wrapResponse(HttpServletRequest request,
HttpServletResponse response) {
return new MultiSessionHttpServletResponse(response, request);
}
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 + "?" + this.sessionParam + "=" + encodedSessionAlias;
}
String path = url.substring(0, queryStart);
String query = url.substring(queryStart + 1, url.length());
String replacement = isDefaultAlias ? "" : "$1" + encodedSessionAlias;
query = query.replaceFirst("((^|&)" + this.sessionParam + "=)([^&]+)?",
replacement);
String sessionParamReplacement = String.format("%s=%s", this.sessionParam,
encodedSessionAlias);
if (!isDefaultAlias && !query.contains(sessionParamReplacement)
&& url.endsWith(query)) {
// no existing alias
if (!(query.endsWith("&") || query.length() == 0)) {
query += "&";
}
query += sessionParamReplacement;
}
return path + "?" + query;
}
private String urlEncode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* A {@link CookieHttpSessionStrategy} aware {@link HttpServletResponseWrapper}.
*/
class MultiSessionHttpServletResponse extends HttpServletResponseWrapper {
private final HttpServletRequest request;
MultiSessionHttpServletResponse(HttpServletResponse response,
HttpServletRequest request) {
super(response);
this.request = request;
}
private String getCurrentSessionAliasFromUrl(String url) {
String currentSessionAliasFromUrl = null;
int queryStart = url.indexOf("?");
if (queryStart >= 0) {
String query = url.substring(queryStart + 1);
Matcher matcher = Pattern
.compile(String.format("%s=([^&]+)",
CookieHttpSessionStrategy.this.sessionParam))
.matcher(query);
if (matcher.find()) {
currentSessionAliasFromUrl = matcher.group(1);
}
}
return currentSessionAliasFromUrl;
}
@Override
public String encodeRedirectURL(String url) {
String encodedUrl = super.encodeRedirectURL(url);
String currentSessionAliasFromUrl = getCurrentSessionAliasFromUrl(encodedUrl);
String alias = (currentSessionAliasFromUrl != null)
? currentSessionAliasFromUrl : getCurrentSessionAlias(this.request);
return CookieHttpSessionStrategy.this.encodeURL(encodedUrl, alias);
}
@Override
public String encodeURL(String url) {
String encodedUrl = super.encodeURL(url);
String currentSessionAliasFromUrl = getCurrentSessionAliasFromUrl(encodedUrl);
String alias = (currentSessionAliasFromUrl != null)
? currentSessionAliasFromUrl : getCurrentSessionAlias(this.request);
return CookieHttpSessionStrategy.this.encodeURL(encodedUrl, alias);
}
}
}