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

@@ -0,0 +1,20 @@
apply from: JAVA_GRADLE
apply from: TOMCAT_GRADLE
tasks.findByPath("artifactoryPublish")?.enabled = false
sonarRunner {
skipProject = true
}
dependencies {
compile project(':spring-session-data-redis'),
"org.springframework:spring-web:$springVersion",
"redis.embedded:embedded-redis:0.2",
jstlDependencies
providedCompile "javax.servlet:javax.servlet-api:$servletApiVersion"
testCompile 'junit:junit:4.11'
integrationTestCompile gebDependencies
}

View File

@@ -0,0 +1,104 @@
package sample
import geb.spock.*
import sample.pages.HomePage;
import sample.pages.LinkPage;
import spock.lang.Stepwise
import pages.*
/**
* Tests the demo that supports multiple sessions
*
* @author Rob Winch
*/
@Stepwise
class UserTests extends GebReportingSpec {
def 'first visit not authenticated'() {
when:
to HomePage
then:
form
!username
}
def 'login single user'() {
setup:
def user = 'rob'
when:
login(user, user)
then:
username == user
}
def 'add account'() {
when:
addAccount.click(HomePage)
then:
form
!username
}
def 'log in second user'() {
setup:
def user = 'luke'
when:
login(user, user)
then:
username == user
}
def 'following links keeps new session'() {
when:
navLink.click(LinkPage)
then:
username == 'luke'
}
def 'switch account rob'() {
setup:
def user = 'rob'
when:
switchAccount(user)
then:
username == user
}
def 'following links keeps original session'() {
when:
navLink.click(LinkPage)
then:
username == 'rob'
}
def 'switch account luke'() {
setup:
def user = 'luke'
when:
switchAccount(user)
then:
username == user
}
def 'logout luke'() {
when:
logout.click(HomePage)
then:
!username
}
def 'switch back rob'() {
setup:
def user = 'rob'
when:
switchAccount(user)
then:
username == user
}
def 'logout rob'() {
when:
logout.click(HomePage)
then:
!username
}
}

View File

@@ -0,0 +1,30 @@
package sample.pages
import geb.*
/**
* The home page
*
* @author Rob Winch
*/
class HomePage extends Page {
static url = ''
static at = { assert driver.title == 'Demonstrates Multi User Log In'; true}
static content = {
navLink { $('#navLink') }
form { $('form') }
username(required:false) { $('#un').text() }
logout(required:false) { $('#logout') }
addAccount(required:false) { $('#addAccount') }
submit { $('input[type=submit]') }
login(required:false) { user, pass ->
form.username = user
form.password = pass
submit.click(HomePage)
}
switchAccount{ un ->
$("#switchAccount${un}").click(HomePage)
}
attributes { moduleList AttributeRow, $("table tr").tail() }
}
}

View File

@@ -0,0 +1,20 @@
package sample.pages
import geb.*
/**
* The Links Page
*
* @author Rob Winch
*/
class LinkPage extends Page {
static url = ''
static at = { assert driver.title == 'Linked Page'; true}
static content = {
form { $('#navLinks') }
username(required:false) { $('#un').text() }
switchAccount{ un ->
$("#switchAccount${un}").click(HomePage)
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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 sample;
public class Account {
private String username;
private String logoutUrl;
private String switchAccountUrl;
public Account(String username, String logoutUrl, String switchAccountUrl) {
super();
this.username = username;
this.logoutUrl = logoutUrl;
this.switchAccountUrl = switchAccountUrl;
}
public String getUsername() {
return username;
}
public String getLogoutUrl() {
return logoutUrl;
}
public String getSwitchAccountUrl() {
return switchAccountUrl;
}
}

View File

@@ -0,0 +1,36 @@
package sample;
/*
* Copyright 2002-2014 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.
*/
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* @author Rob Winch
*/
@Import(EmbeddedRedisConfiguration.class)
@Configuration
@EnableRedisHttpSession
public class Config {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}

View File

@@ -0,0 +1,57 @@
package sample;
/*
* Copyright 2002-2014 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.
*/
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Protocol;
import redis.embedded.RedisServer;
/**
* Runs an embedded Redis instance. This is only necessary since we do not want
* users to have to setup a Redis instance. In a production environment, this
* would not be used since a Redis Server would be setup.
*
* @author Rob Winch
*/
@Configuration
public class EmbeddedRedisConfiguration {
@Bean
public RedisServerBean redisServer() {
return new RedisServerBean();
}
class RedisServerBean implements InitializingBean, DisposableBean {
private RedisServer redisServer;
@Override
public void afterPropertiesSet() throws Exception {
redisServer = new RedisServer(Protocol.DEFAULT_PORT);
redisServer.start();
}
@Override
public void destroy() throws Exception {
if(redisServer != null) {
redisServer.stop();
}
}
}
}

View File

@@ -0,0 +1,36 @@
package sample;
/*
* Copyright 2002-2014 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.
*/
import javax.servlet.ServletContext;
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
/**
* @author Rob Winch
*/
public class Initializer extends AbstractHttpSessionApplicationInitializer {
public Initializer() {
super(Config.class);
}
@Override
protected void afterSessionRepositoryFilter(ServletContext servletContext) {
appendFilters(servletContext, new UserAccountsFilter());
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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 sample;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
if(username != null && username.equals(password)) {
req.getSession().setAttribute("username", username);
String url = resp.encodeRedirectURL(req.getContextPath() + "/");
resp.sendRedirect(url);
}
}
private static final long serialVersionUID = -8157634860354132501L;
}

View File

@@ -0,0 +1,42 @@
/*
* 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 sample;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession(false);
if(session != null) {
session.invalidate();
}
String url = resp.encodeRedirectURL(req.getContextPath() + "/");
resp.sendRedirect(url);
}
private static final long serialVersionUID = 4061762524521437433L;
}

View File

@@ -0,0 +1,95 @@
/*
* 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 sample;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.HttpSessionManager;
public class UserAccountsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
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);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014 Twitter, Inc.
* Licensed under the Creative Commons Attribution 3.0 Unported License. For
* details, see http://creativecommons.org/licenses/by/3.0/.
*/
// See the Getting Started docs for more information:
// http://getbootstrap.com/getting-started/#support-ie10-width
(function () {
'use strict';
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
var msViewportStyle = document.createElement('style')
msViewportStyle.appendChild(
document.createTextNode(
'@-ms-viewport{width:auto!important}'
)
)
document.querySelector('head').appendChild(msViewportStyle)
}
})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,97 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Demonstrates Multi User Log In</title>
<link rel="stylesheet" href="assets/bootstrap.min.css">
<style type="text/css">
body {
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<!-- Static navbar -->
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="https://github.com/spring-projects/spring-session/">Spring Session</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<c:url value="/" var="homeUrl"/>
<li class="active"><a id="navHome" href="${homeUrl}">Home</a></li>
<c:url value="/link.jsp" var="linkUrl"/>
<li><a id="navLink" href="${linkUrl}">Link</a></li>
</ul>
<c:if test="${currentAccount != null or not empty accounts}">
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><c:out value="${username}"/> <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<c:if test="${currentAccount != null}">
<li><a id="logout" href="${currentAccount.logoutUrl}">Log Out</a></li>
<li><a id="addAccount" href="${addAccountUrl}">Add Account</a></li>
</c:if>
<c:if test="${not empty accounts}">
<li class="divider"></li>
<li class="dropdown-header">Switch Account</li>
<li class="divider"></li>
</c:if>
<c:forEach items="${accounts}" var="account">
<li><a id="switchAccount${account.username}" href="${account.switchAccountUrl}"><c:out value="${account.username}"/></a></li>
</c:forEach>
</ul>
</li>
</ul>
</c:if>
</div><!--/.nav-collapse -->
</div><!--/.container-fluid -->
</nav>
<h1>Description</h1>
<p>This application demonstrates how to use Spring Session to authenticate as multiple users at a time. View authenticated users in the upper right corner.</p>
<c:choose>
<c:when test="${username == null}">
<h1>Please Log In</h1>
<p>You are not currently authenticated with this session. You can authenticate with any username password combination that are equal. A few examples to try:</p>
<ul>
<li><b>Username</b> rob and <b>Password</b> rob</li>
<li><b>Username</b> luke and <b>Password</b> luke</li>
</ul>
<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post">
<div class="form-group">
<label for="username">Username</label>
<input id="username" type="text" name="username"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input id="password" type="text" name="password"/>
</div>
<input type="submit" value="Login"/>
</form>
</c:when>
<c:otherwise>
<h1 id="un"><c:out value="${username}"/></h1>
<p>You are authenticated as <b><c:out value="${username}"/></b>. Observe that you can <a href="${linkUrl}">navigate links</a> and the correct session is used. Using the links in the upper right corner you can:</p>
<ul>
<li>Log Out</li>
<li>Switch Accounts</li>
<li>Add Account</li>
</ul>
</c:otherwise>
</c:choose>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="./assets/js/jquery.min.js"></script>
<script src="./assets/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="./assets/js/ie10-viewport-bug-workaround.js"></script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Linked Page</title>
<link rel="stylesheet" href="assets/bootstrap.min.css">
<style type="text/css">
body {
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<!-- Static navbar -->
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="https://github.com/spring-projects/spring-session/">Spring Session</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<c:url value="/" var="homeUrl"/>
<li><a id="navHome" href="${homeUrl}">Home</a></li>
<c:url value="/link.jsp" var="linkUrl"/>
<li class="active"><a id="navLink" href="${linkUrl}">Link</a></li>
</ul>
<c:if test="${currentAccount != null or not empty accounts}">
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><c:out value="${username}"/> <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<c:if test="${currentAccount != null}">
<li><a id="logout" href="${currentAccount.logoutUrl}">Log Out</a></li>
<li><a id="addAccount" href="${addAccountUrl}">Add Account</a></li>
</c:if>
<c:if test="${not empty accounts}">
<li class="divider"></li>
<li class="dropdown-header">Switch Account</li>
<li class="divider"></li>
</c:if>
<c:forEach items="${accounts}" var="account">
<li><a id="switchAccount${account.username}" href="${account.switchAccountUrl}"><c:out value="${account.username}"/></a></li>
</c:forEach>
</ul>
</li>
</ul>
</c:if>
</div><!--/.nav-collapse -->
</div><!--/.container-fluid -->
</nav>
<h1>Description</h1>
<p>This page demonstrates how we keep track of the correct user session between links even when multiple tabs are open. Try opening another tab with a different user and browse. Then come back to the original tab and see the correct user is maintained.</p>
<c:choose>
<c:when test="${username == null}">
<h1>Please Log In</h1>
<p>You are not currently authenticated with this session. <a href="${homeUrl}">Log In</a></p>
</c:when>
<c:otherwise>
<h1 id="un"><c:out value="${username}"/></h1>
<p>You are authenticated as <b><c:out value="${username}"/></b>. Observe that you can <a href="${linkUrl}">navigate links</a> and the correct session is used. Using the links in the upper right corner you can:</p>
<ul>
<li>Log Out</li>
<li>Switch Accounts</li>
<li>Add Account</li>
</ul>
</c:otherwise>
</c:choose>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="./assets/js/jquery.min.js"></script>
<script src="./assets/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="./assets/js/ie10-viewport-bug-workaround.js"></script>
</body>
</html>

View File

@@ -3,4 +3,5 @@ rootProject.name = 'spring-session-build'
include 'spring-session'
include 'spring-session-data-redis'
include 'samples:web'
include 'samples:users'
include 'samples:websocket'

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;
}
}
}

View File

@@ -13,99 +13,238 @@ import org.springframework.session.web.http.CookieHttpSessionStrategy;
import javax.servlet.http.Cookie;
public class CookieHttpSessionStrategyTests {
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private CookieHttpSessionStrategy strategy;
private String cookieName;
private Session session;
private CookieHttpSessionStrategy strategy;
private String cookieName;
private Session session;
@Before
public void setup() throws Exception {
cookieName = "SESSION";
session = new MapSession();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
strategy = new CookieHttpSessionStrategy();
}
@Before
public void setup() throws Exception {
cookieName = "SESSION";
session = new MapSession();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
strategy = new CookieHttpSessionStrategy();
}
@Test
public void getRequestedSessionIdNull() throws Exception {
assertThat(strategy.getRequestedSessionId(request)).isNull();
}
@Test
public void getRequestedSessionIdNull() throws Exception {
assertThat(strategy.getRequestedSessionId(request)).isNull();
}
@Test
public void getRequestedSessionIdNotNull() throws Exception {
setSessionId(session.getId());
assertThat(strategy.getRequestedSessionId(request)).isEqualTo(session.getId());
}
@Test
public void getRequestedSessionIdNotNull() throws Exception {
setSessionId(session.getId());
assertThat(strategy.getRequestedSessionId(request)).isEqualTo(session.getId());
}
@Test
public void getRequestedSessionIdNotNullCustomCookieName() throws Exception {
setCookieName("CUSTOM");
setSessionId(session.getId());
assertThat(strategy.getRequestedSessionId(request)).isEqualTo(session.getId());
}
@Test
public void getRequestedSessionIdNotNullCustomCookieName() throws Exception {
setCookieName("CUSTOM");
setSessionId(session.getId());
assertThat(strategy.getRequestedSessionId(request)).isEqualTo(session.getId());
}
@Test
public void onNewSession() throws Exception {
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
@Test
public void onNewSession() throws Exception {
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
@Test
public void onNewSessionCookiePath() throws Exception {
request.setContextPath("/somethingunique");
strategy.onNewSession(session, request, response);
@Test
public void onNewSessionExistingSessionSameAlias() throws Exception {
Session existing = new MapSession();
setSessionId(existing.getId());
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
Cookie sessionCookie = response.getCookie(cookieName);
assertThat(sessionCookie.getPath()).isEqualTo(request.getContextPath() + "/");
}
@Test
public void onNewSessionExistingSessionNewAlias() throws Exception {
Session existing = new MapSession();
setSessionId(existing.getId());
request.setParameter(CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo("0 " + existing.getId() + " new " + session.getId());
}
@Test
public void onNewSessionCustomCookieName() throws Exception {
setCookieName("CUSTOM");
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
@Test
public void onNewSessionCookiePath() throws Exception {
request.setContextPath("/somethingunique");
strategy.onNewSession(session, request, response);
@Test
public void onDeleteSession() throws Exception {
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEmpty();
}
Cookie sessionCookie = response.getCookie(cookieName);
assertThat(sessionCookie.getPath()).isEqualTo(request.getContextPath() + "/");
}
@Test
public void onDeleteSessionCookiePath() throws Exception {
request.setContextPath("/somethingunique");
strategy.onInvalidateSession(request, response);
@Test
public void onNewSessionCustomCookieName() throws Exception {
setCookieName("CUSTOM");
strategy.onNewSession(session, request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
Cookie sessionCookie = response.getCookie(cookieName);
assertThat(sessionCookie.getPath()).isEqualTo(request.getContextPath() + "/");
}
@Test
public void onDeleteSession() throws Exception {
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEmpty();
}
@Test
public void onDeleteSessionCustomCookieName() throws Exception {
setCookieName("CUSTOM");
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEmpty();
}
@Test
public void onDeleteSessionCookiePath() throws Exception {
request.setContextPath("/somethingunique");
strategy.onInvalidateSession(request, response);
@Test(expected = IllegalArgumentException.class)
public void setCookieNameNull() throws Exception {
strategy.setCookieName(null);
}
Cookie sessionCookie = response.getCookie(cookieName);
assertThat(sessionCookie.getPath()).isEqualTo(request.getContextPath() + "/");
}
public void setCookieName(String cookieName) {
strategy.setCookieName(cookieName);
this.cookieName = cookieName;
}
@Test
public void onDeleteSessionCustomCookieName() throws Exception {
setCookieName("CUSTOM");
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEmpty();
}
public void setSessionId(String id) {
request.setCookies(new Cookie(cookieName, id));
}
@Test
public void onDeleteSessionExistingSessionSameAlias() throws Exception {
Session existing = new MapSession();
setSessionId("0 " + existing.getId() + " new " + session.getId());
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEqualTo(session.getId());
}
public String getSessionId() {
return response.getCookie(cookieName).getValue();
}
@Test
public void onDeleteSessionExistingSessionNewAlias() throws Exception {
Session existing = new MapSession();
setSessionId("0 " + existing.getId() + " new " + session.getId());
request.setParameter(CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "new");
strategy.onInvalidateSession(request, response);
assertThat(getSessionId()).isEqualTo(existing.getId());
}
@Test(expected = IllegalArgumentException.class)
public void setCookieNameNull() throws Exception {
strategy.setCookieName(null);
}
@Test
public void encodeURLNoExistingQuery() {
assertThat(strategy.encodeURL("/url", "2")).isEqualTo("/url?u=2");
}
@Test
public void encodeURLNoExistingQueryEmpty() {
assertThat(strategy.encodeURL("/url?", "2")).isEqualTo("/url?u=2");
}
@Test
public void encodeURLExistingQueryNoAlias() {
assertThat(strategy.encodeURL("/url?a=b", "2")).isEqualTo("/url?a=b&u=2");
}
@Test
public void encodeURLExistingQueryExistingAliasStart() {
assertThat(strategy.encodeURL("/url?u=1&y=z", "2")).isEqualTo("/url?u=2&y=z");
}
@Test
public void encodeURLExistingQueryExistingAliasMiddle() {
assertThat(strategy.encodeURL("/url?a=b&u=1&y=z", "2")).isEqualTo("/url?a=b&u=2&y=z");
}
@Test
public void encodeURLExistingQueryExistingAliasEnd() {
assertThat(strategy.encodeURL("/url?a=b&u=1", "2")).isEqualTo("/url?a=b&u=2");
}
//
@Test
public void encodeURLExistingQueryParamEndsWithActualParamStart() {
assertThat(strategy.encodeURL("/url?xu=1&y=z", "2")).isEqualTo("/url?xu=1&y=z&u=2");
}
@Test
public void encodeURLExistingQueryParamEndsWithActualParamMiddle() {
assertThat(strategy.encodeURL("/url?a=b&xu=1&y=z", "2")).isEqualTo("/url?a=b&xu=1&y=z&u=2");
}
@Test
public void encodeURLExistingQueryParamEndsWithActualParamEnd() {
assertThat(strategy.encodeURL("/url?a=b&xu=1", "2")).isEqualTo("/url?a=b&xu=1&u=2");
}
//
@Test
public void encodeURLNoExistingQueryDefaultAlias() {
assertThat(strategy.encodeURL("/url", "0")).isEqualTo("/url");
}
@Test
public void encodeURLNoExistingQueryEmptyDefaultAlias() {
assertThat(strategy.encodeURL("/url?", "0")).isEqualTo("/url?");
}
@Test
public void encodeURLExistingQueryNoAliasDefaultAlias() {
assertThat(strategy.encodeURL("/url?a=b", "0")).isEqualTo("/url?a=b");
}
@Test
public void encodeURLExistingQueryExistingAliasStartDefaultAlias() {
// relaxed constraint as result /url?&y=z does not hurt anything (ideally should remove the &)
assertThat(strategy.encodeURL("/url?u=1&y=z", "0")).doesNotContain("u=0&u=1");
}
@Test
public void encodeURLExistingQueryExistingAliasMiddleDefaultAlias() {
assertThat(strategy.encodeURL("/url?a=b&u=1&y=z", "0")).isEqualTo("/url?a=b&y=z");
}
@Test
public void encodeURLExistingQueryExistingAliasEndDefaultAlias() {
assertThat(strategy.encodeURL("/url?a=b&u=1", "0")).isEqualTo("/url?a=b");
}
// --- getCurrentSessionAlias
@Test
public void getCurrentSessionAliasNull() {
assertThat(strategy.getCurrentSessionAlias(request)).isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSessionAliasNullParamName() {
strategy.setSessionAliasParamName(null);
request.setParameter(CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, "NOT USED");
assertThat(strategy.getCurrentSessionAlias(request)).isEqualTo(CookieHttpSessionStrategy.DEFAULT_ALIAS);
}
@Test
public void getCurrentSession() {
String expectedAlias = "1";
request.setParameter(CookieHttpSessionStrategy.DEFAULT_SESSION_ALIAS_PARAM_NAME, expectedAlias);
assertThat(strategy.getCurrentSessionAlias(request)).isEqualTo(expectedAlias);
}
// --- helper
public void setCookieName(String cookieName) {
strategy.setCookieName(cookieName);
this.cookieName = cookieName;
}
public void setSessionId(String id) {
request.setCookies(new Cookie(cookieName, id));
}
public String getSessionId() {
return response.getCookie(cookieName).getValue();
}
}

View File

@@ -2,6 +2,8 @@ package org.springframework.session.web.http;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.Arrays;
@@ -20,15 +22,23 @@ import javax.servlet.http.HttpSessionContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("deprecation")
public class SessionRepositoryFilterTests<S extends ExpiringSession> {
@Mock
private HttpSessionStrategy strategy;
private SessionRepository<ExpiringSession> sessionRepository;
private SessionRepositoryFilter<ExpiringSession> filter;
@@ -838,6 +848,72 @@ public class SessionRepositoryFilterTests<S extends ExpiringSession> {
});
}
// --- MultiHttpSessionStrategyAdapter
@Test
public void doFilterAdapterGetRequestedSessionId() throws Exception {
filter.setHttpSessionStrategy(strategy);
final String expectedId = "MultiHttpSessionStrategyAdapter-requested-id";
when(strategy.getRequestedSessionId(any(HttpServletRequest.class))).thenReturn(expectedId);
doFilter(new DoInFilter(){
@Override
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse) throws IOException {
String actualId = wrappedRequest.getRequestedSessionId();
assertThat(actualId).isEqualTo(expectedId);
}
});
}
@Test
public void doFilterAdapterOnNewSession() throws Exception {
filter.setHttpSessionStrategy(strategy);
doFilter(new DoInFilter(){
@Override
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse) throws IOException {
wrappedRequest.getSession();
}
});
HttpServletRequest request = (HttpServletRequest) chain.getRequest();
Session session = sessionRepository.getSession(request.getSession().getId());
verify(strategy).onNewSession(eq(session), any(HttpServletRequest.class),any(HttpServletResponse.class));
}
@Test
public void doFilterAdapterOnInvalidate() throws Exception {
filter.setHttpSessionStrategy(strategy);
doFilter(new DoInFilter(){
@Override
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse) throws IOException {
wrappedRequest.getSession().getId();
}
});
HttpServletRequest request = (HttpServletRequest) chain.getRequest();
String id = request.getSession().getId();
when(strategy.getRequestedSessionId(any(HttpServletRequest.class))).thenReturn(id);
doFilter(new DoInFilter(){
@Override
public void doFilter(HttpServletRequest wrappedRequest, HttpServletResponse wrappedResponse) throws IOException {
wrappedRequest.getSession().invalidate();
}
});
verify(strategy).onInvalidateSession(any(HttpServletRequest.class),any(HttpServletResponse.class));
}
// --- order
public void order() {
int expected = 5;
filter.setOrder(expected);
assertThat(filter.getOrder()).isEqualTo(expected);
}
// --- helper methods
private void assertNewSession() {