Add support for multiple sessions in a browser

Fixes gh-49
This commit is contained in:
Rob Winch
2014-11-16 22:13:27 -06:00
parent 9df0594573
commit a0805e8411
26 changed files with 1509 additions and 105 deletions

View File

@@ -15,28 +15,38 @@
*/
package org.springframework.session.web.http;
import org.springframework.session.Session;
import org.springframework.util.Assert;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringTokenizer;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.session.Session;
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".
* 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:
* 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:
* 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
@@ -44,46 +54,194 @@ import javax.servlet.http.HttpServletResponse;
* 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:
* 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/?u=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
* u=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();
*
* Session session = repo.getSession(sessionId);
* if(session == null) {
* continue;
* }
*
* String username = (String) 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);
* </code>
*
*
* @since 1.0
* @author Rob Winch
*/
public final class CookieHttpSessionStrategy implements HttpSessionStrategy {
public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy, HttpSessionManager {
static final String DEFAULT_ALIAS = "0";
static final String DEFAULT_SESSION_ALIAS_PARAM_NAME = "u";
private String cookieName = "SESSION";
private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
@Override
public String getRequestedSessionId(HttpServletRequest request) {
Cookie session = getCookie(request, cookieName);
return session == null ? null : session.getValue();
Map<String,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
return sessionIds.get(sessionAlias);
}
@Override
public String getCurrentSessionAlias(HttpServletRequest request) {
if(sessionParam == null) {
return DEFAULT_ALIAS;
}
String u = request.getParameter(sessionParam);
if(u == null) {
return DEFAULT_ALIAS;
}
return u;
}
@Override
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
Cookie cookie = new Cookie(cookieName, session.getId());
cookie.setHttpOnly(true);
cookie.setSecure(request.isSecure());
cookie.setPath(cookiePath(request));
response.addCookie(cookie);
Map<String,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
sessionIds.put(sessionAlias, session.getId());
Cookie sessionCookie = createSessionCookie(request, sessionIds);
response.addCookie(sessionCookie);
}
// TODO set the domain?
private Cookie createSessionCookie(HttpServletRequest request,
Map<String, String> sessionIds) {
Cookie sessionCookie = new Cookie(cookieName,"");
sessionCookie.setHttpOnly(true);
sessionCookie.setSecure(request.isSecure());
sessionCookie.setPath(cookiePath(request));
// TODO set domain?
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;
}
@Override
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
Cookie sessionCookie = new Cookie(cookieName,"");
sessionCookie.setMaxAge(0);
sessionCookie.setHttpOnly(true);
sessionCookie.setSecure(request.isSecure());
sessionCookie.setPath(cookiePath(request));
Map<String,String> sessionIds = getSessionIds(request);
String requestedAlias = getCurrentSessionAlias(request);
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
@@ -116,4 +274,78 @@ public final class CookieHttpSessionStrategy implements HttpSessionStrategy {
private static String cookiePath(HttpServletRequest request) {
return request.getContextPath() + "/";
}
@Override
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, " ");
if(tokens.countTokens() == 1) {
result.put(DEFAULT_ALIAS, tokens.nextToken());
return result;
}
while(tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
String id = tokens.nextToken();
result.put(alias, id);
}
return result;
}
@Override
public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
request.setAttribute(HttpSessionManager.class.getName(), this);
return request;
}
@Override
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);
}
}
@Override
public String encodeURL(String url, String sessionAlias) {
int queryStart = url.indexOf("?");
boolean isDefaultAlias = DEFAULT_ALIAS.equals(sessionAlias);
if(queryStart < 0) {
return isDefaultAlias ? url : url + "?" + sessionParam + "=" + sessionAlias;
}
String path = url.substring(0, queryStart);
String query = url.substring(queryStart + 1, url.length());
String replacement = isDefaultAlias ? "" : "$1"+sessionAlias;
query = query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement);
if(!isDefaultAlias && url.endsWith(query)) {
// no existing alias
if(!(query.endsWith("&") || query.length() == 0)) {
query += "&";
}
query += sessionParam + "=" + sessionAlias;
}
return path + "?" + query;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2002-2013 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.util.Map;
import javax.servlet.http.HttpServletRequest;
/**
* Allows managing a mapping of alias to the session id for having multiple
* active sessions at the same time.
*
* @author Rob Winch
* @since 1.0
*
*/
public interface HttpSessionManager {
/**
* Gets the current session's alias from the {@link HttpServletRequest}.
*
* @param request
* the {@link HttpServletRequest} to obtain the current session's
* alias from.
* @return the current sessions' alias. Cannot be null.
*/
String getCurrentSessionAlias(HttpServletRequest request);
/**
* Gets a mapping of the session alias to the session id from the
* {@link HttpServletRequest}
*
* @param request
* the {@link HttpServletRequest} to obtain the mapping from.
* Cannot be null.
* @return a mapping of the session alias to the session id from the
* {@link HttpServletRequest}. Cannot be null.
*/
Map<String, String> getSessionIds(HttpServletRequest request);
/**
* Provides the ability to encode the URL for a given session alias.
*
* @param url the url to encode.
* @param sessionAlias the session alias to encode.
* @return the encoded URL
*/
String encodeURL(String url, String sessionAlias);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2013 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* Some {@link HttpSessionStrategy} may also want to further customize
* {@link HttpServletRequest} and {@link HttpServletResponse} objects. For
* example, {@link CookieHttpSessionStrategy} customizes how URL rewriting is
* done to select which session should be used in the event multiple sessions
* are active.
* </p>
*
* @see CookieHttpSessionStrategy
*
* @author Rob Winch
* @since 1.0
*/
public interface MultiHttpSessionStrategy extends HttpSessionStrategy, RequestResponsePostProcessor {
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2002-2013 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Allows customizing the {@link HttpServletRequest} and/or the
* {@link HttpServletResponse}.
*
* @author Rob Winch
* @since 1.0
*/
public interface RequestResponsePostProcessor {
/**
* Allows customizing the {@link HttpServletRequest}.
*
* @param request
* the original {@link HttpServletRequest}. Cannot be null.
* @param response
* the original {@link HttpServletResponse}. This is NOT the
* result of
* {@link #wrapResponse(HttpServletRequest, HttpServletResponse)}
* Cannot be null. .
* @return a non-null {@link HttpServletRequest}
*/
HttpServletRequest wrapRequest(HttpServletRequest request,
HttpServletResponse response);
/**
* Allows customizing the {@link HttpServletResponse}.
*
* @param request
* the original {@link HttpServletRequest}. This is NOT the
* result of
* {@link #wrapRequest(HttpServletRequest, HttpServletResponse)}.
* Cannot be null.
* @param response
* the original {@link HttpServletResponse}. Cannot be null.
* @return a non-null {@link HttpServletResponse}
*/
HttpServletResponse wrapResponse(HttpServletRequest request,
HttpServletResponse response);
}

View File

@@ -17,6 +17,7 @@ package org.springframework.session.web.http;
import org.springframework.core.Ordered;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.util.Assert;
@@ -53,13 +54,15 @@ import java.util.Set;
* @author Rob Winch
*/
public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter implements Ordered {
public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class.getName();
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 50;
private int order = DEFAULT_ORDER;
private final SessionRepository<S> sessionRepository;
private HttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
/**
* Creates a new instance
@@ -77,15 +80,31 @@ public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerR
* @param httpSessionStrategy the {@link HttpSessionStrategy} to use. Cannot be null.
*/
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
Assert.notNull(httpSessionStrategy,"httpSessionIdStrategy cannot be null");
this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(httpSessionStrategy);
}
/**
* Sets the {@link MultiHttpSessionStrategy} to be used. The default is a {@link CookieHttpSessionStrategy}.
*
* @param httpSessionStrategy the {@link MultiHttpSessionStrategy} to use. Cannot be null.
*/
public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
Assert.notNull(httpSessionStrategy,"httpSessionIdStrategy cannot be null");
this.httpSessionStrategy = httpSessionStrategy;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);
HttpServletRequest strategyRequest = httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
@@ -145,7 +164,9 @@ public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerR
} else {
S session = wrappedSession.session;
sessionRepository.save(session);
httpSessionStrategy.onNewSession(session, this, response);
if(!requestedValidSession) {
httpSessionStrategy.onNewSession(session, this, response);
}
}
}
@@ -346,4 +367,38 @@ public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerR
public int getOrder() {
return order;
}
static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {
private HttpSessionStrategy delegate;
public MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {
this.delegate = delegate;
}
public String getRequestedSessionId(HttpServletRequest request) {
return delegate.getRequestedSessionId(request);
}
public void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response) {
delegate.onNewSession(session, request, response);
}
public void onInvalidateSession(HttpServletRequest request,
HttpServletResponse response) {
delegate.onInvalidateSession(request, response);
}
@Override
public HttpServletRequest wrapRequest(HttpServletRequest request,
HttpServletResponse response) {
return request;
}
@Override
public HttpServletResponse wrapResponse(HttpServletRequest request,
HttpServletResponse response) {
return response;
}
}
}