Add CookieSerializer Strategy

This allows for custom seralization of the Cookie.

Fixes gh-299
This commit is contained in:
Rob Winch
2015-11-09 10:08:51 -06:00
parent c701e1877e
commit 8c07537bec
19 changed files with 1310 additions and 80 deletions

View File

@@ -0,0 +1,101 @@
= Spring Session - Custom Cookie
Rob Winch
:toc:
This guide describes how to configure Spring Session to use custom cookies with Java Configuration.
The guide assumes you have already link:./httpsession.html[setup Spring Session in your project].
NOTE: The completed guide can be found in the <<custom-cookie-sample, Custom Cookie sample application>>.
[[custom-cookie-spring-configuration]]
== Spring Java Configuration
Once you have setup Spring Session you can easily customize how the session cookie is written by exposing a `CookieSerializer` as a Spring Bean.
Out of the box, Spring Session comes with `DefaultCookieSerializer`.
Simply exposing the `DefaultCookieSerializer` as a Spring Bean will augment the existing configuration when using configurations like `@EnableRedisHttpSession`.
You can find an example of customizing Spring Session's cookie below:
[source,java]
----
include::{samples-dir}custom-cookie/src/main/java/sample/Config.java[tags=cookie-serializer]
----
<1> We customize the name of the cookie to be JSESSIONID
<2> We customize the path of the cookie to be "/" (rather than the default of the context root)
<3> We customize the domain name pattern (a regular expression) to be `^.+?\\.(\\w+\\.[a-z]+)$`
This allows sharing a session across domains and applications.
If the regular expression does not match, no domain is set and the existing domain will be used.
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] will be used as the domain.
This means that a request to https://child.example.com will set the domain to example.com.
However, a request to http://localhost:8080/ or http://192.168.1.100:8080/ will leave the cookie unset and thus still work in development without any changes necessary for production.
[WARNING]
====
It is important to note that users should only match on valid domain characters since the domain name is reflected in the response.
This is prevent a malicious user from performing attacks like https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
====
[[custom-cookie-options]]
== Configuration Options
The configuration options available are:
* `cookieName` - the name of the cookie to use
Default "SESSION"
* `useSecureCookie` - specify if a secure cookie be used
Default use value of `HttpServletRequest.isSecure()` at the time of creation.
* `cookiePath` - the path of the cookie
Default is context root
* `cookieMaxAge` - specifies the max age of the cookie to be set at the time the session is created.
Default is -1 which indicates the cookie will be removed when the browser is closed.
* `jvmRoute` - specifies a suffix to be appended to the session id and included in the cookie.
Used to identify which JVM to route to for session affinity.
With some implementations (i.e. Redis) this provides no performance benefit.
However, this can help with tracing logs of a particular user.
* `domainName` - allows specifying a specific domain name to be used for the cookie.
This option is simple to understand, but will likely require a different configuration between development and production environments.
See domainNamePattern as an alternative.
* `domainNamePattern` - a case insensitive pattern used to extract the domain name from the `HttpServletRequest#getServerName()`.
The pattern should provide a single grouping used to extract the value of the cookie domain.
If the regular expression does not match, no domain is set and the existing domain will be used.
If the regular expression matches, the first https://docs.oracle.com/javase/tutorial/essential/regex/groups.html[grouping] will be used as the domain.
[WARNING]
====
It is important to note that users should only match on valid domain characters since the domain name is reflected in the response.
This is prevent a malicious user from performing attacks like https://en.wikipedia.org/wiki/HTTP_response_splitting[HTTP Response Splitting].
====
[[custom-cookie-sample]]
== custom-cookie Sample Application
=== Running the custom-cookie Sample Application
You can run the sample by obtaining the {download-url}[source code] and invoking the following command:
[NOTE]
====
For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379).
Alternatively, you can update the `JedisConnectionFactory` to point to a Redis server.
====
----
$ ./gradlew :samples:custom-cookie:tomcatRun
----
You should now be able to access the application at http://localhost:8080/
=== Exploring the custom-cookie Sample Application
Try using the application. Fill out the form with the following information:
* **Attribute Name:** _username_
* **Attribute Value:** _rob_
Now click the **Set Attribute** button.
You should now see the values displayed in the table.
If you look at the cookies for the application, you can see the cookie is saved to the custom name of JSESSIONID

View File

@@ -39,6 +39,10 @@ If you are looking to get started with Spring Session, the best place to start i
| Demonstrates how to use Spring Session to replace the `HttpSession` with a Redis store using XML based configuration.
| link:guides/httpsession-xml.html[HttpSession XML Guide]
| {gh-samples-url}custom-cookie[Custom Cookie]
| Demonstrates how to use Spring Session and customize the cookie.
| link:guides/custom-cookie.html[Custom Cookie Guide]
| {gh-samples-url}boot[Spring Boot]
| Demonstrates how to use Spring Session with Spring Boot.
| link:guides/boot.html[Spring Boot Guide]
@@ -635,6 +639,9 @@ To run it use the following:
./gradlew :samples:hazelcast-spring:tomcatRun
[[api-mapsessionrepository-hazelcast]]
==== Using Spring Session and Hazlecast
[[community]]
== Spring Session Community

View File

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

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2002-2015 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 geb.spock.*
import sample.pages.HomePage;
import spock.lang.Stepwise
import pages.*
/**
* Tests the CAS sample application using service tickets.
*
* @author Rob Winch
*/
@Stepwise
class AttributeTests extends GebReportingSpec {
def 'first visit no attributes'() {
when:
to HomePage
then:
attributes.empty
}
def 'create attribute'() {
when:
createAttribute('a','b')
then:
attributes.size() == 1
attributes[0].name == 'a'
attributes[0].value == 'b'
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2002-2015 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.pages
import geb.*
/**
* The home page
*
* @author Rob Winch
*/
class HomePage extends Page {
static url = ''
static at = { assert driver.title == 'Session Attributes'; true}
static content = {
form { $('form') }
submit { $('input[type=submit]') }
createAttribute(required:false) { name, value ->
form.attributeName = name
form.attributeValue = value
submit.click(HomePage)
}
attributes { moduleList AttributeRow, $("table tr").tail() }
}
}
class AttributeRow extends Module {
static content = {
cell { $("td", it) }
name { cell(0).text() }
value { cell(1).text() }
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2015 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 org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@EnableRedisHttpSession
public class Config {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
// tag::cookie-serializer[]
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID"); // <1>
serializer.setCookiePath("/"); // <2>
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); // <3>
return serializer;
}
// end::cookie-serializer[]
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2002-2015 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 org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
// tag::class[]
public class Initializer
extends AbstractHttpSessionApplicationInitializer { // <1>
public Initializer() {
super(Config.class); // <2>
}
}
// end::class[]

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2015 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 javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.io.IOException;
// tag::class[]
@WebServlet("/session")
public class SessionServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String attributeName = req.getParameter("attributeName");
String attributeValue = req.getParameter("attributeValue");
req.getSession().setAttribute(attributeName, attributeValue);
resp.sendRedirect(req.getContextPath() + "/");
}
private static final long serialVersionUID = 2878267318695777395L;
}
// tag::end[]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Session Attributes</title>
<link rel="stylesheet" href="assets/bootstrap.min.css">
<style type="text/css">
body {
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<h1>Description</h1>
<p>This application demonstrates how to use a Redis instance to back your session. Notice that there is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested session id is.</p>
<h1>Try it</h1>
<form class="form-inline" role="form" action="./session" method="post">
<label for="attributeName">Attribute Name</label>
<input id="attributeName" type="text" name="attributeName"/>
<label for="attributeValue">Attribute Value</label>
<input id="attributeValue" type="text" name="attributeValue"/>
<input type="submit" value="Set Attribute"/>
</form>
<hr/>
<table class="table table-striped">
<thead>
<tr>
<th>Attribute Name</th>
<th>Attribute Value</th>
</tr>
</thead>
<tbody>
<c:forEach items="${sessionScope}" var="attr">
<tr>
<td><c:out value="${attr.key}"/></td>
<td><c:out value="${attr.value}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -3,6 +3,7 @@ rootProject.name = 'spring-session-build'
include 'docs'
include 'samples:boot'
include 'samples:custom-cookie'
include 'samples:findbyusername'
include 'samples:hazelcast'
include 'samples:hazelcast-spring'

View File

@@ -29,6 +29,8 @@ import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.SessionEventHttpSessionListenerAdapter;
import org.springframework.session.web.http.SessionRepositoryFilter;
@@ -82,10 +84,13 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@EnableScheduling
public class SpringHttpSessionConfiguration {
private HttpSessionStrategy httpSessionStrategy;
private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();
private HttpSessionStrategy httpSessionStrategy = defaultHttpSessionStrategy;
private List<HttpSessionListener> httpSessionListeners = new ArrayList<HttpSessionListener>();
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(httpSessionListeners);
@@ -95,12 +100,15 @@ public class SpringHttpSessionConfiguration {
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository, ServletContext servletContext) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
sessionRepositoryFilter.setServletContext(servletContext);
if(httpSessionStrategy != null) {
sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
}
sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
return sessionRepositoryFilter;
}
@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer);
}
@Autowired(required = false)
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
this.httpSessionStrategy = httpSessionStrategy;

View File

@@ -19,18 +19,19 @@ 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.Pattern;
import javax.servlet.ServletRequest;
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.session.web.http.CookieSerializer.CookieValue;
import org.springframework.util.Assert;
/**
* A {@link HttpSessionStrategy} that uses a cookie to obtain the session from.
@@ -161,11 +162,9 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
private Pattern ALIAS_PATTERN = Pattern.compile("^[\\w-]{1,50}$");
private String cookieName = "SESSION";
private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
private boolean isServlet3Plus = isServlet3();
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();
public String getRequestedSessionId(HttpServletRequest request) {
Map<String,String> sessionIds = getSessionIds(request);
@@ -220,8 +219,9 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
Map<String,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
sessionIds.put(sessionAlias, session.getId());
Cookie sessionCookie = createSessionCookie(request, sessionIds);
response.addCookie(sessionCookie);
String cookieValue = createSessionCookieValue(sessionIds);
cookieSerializer.writeCookieValue(new CookieValue(request,response,cookieValue));
}
@SuppressWarnings("unchecked")
@@ -234,26 +234,14 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
return sessionsWritten;
}
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?
private String createSessionCookieValue(Map<String, String> sessionIds) {
if(sessionIds.isEmpty()) {
sessionCookie.setMaxAge(0);
return sessionCookie;
return "";
}
if(sessionIds.size() == 1) {
return sessionIds.values().iterator().next();
}
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();
@@ -265,9 +253,7 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
buffer.append(" ");
}
buffer.deleteCharAt(buffer.length()-1);
sessionCookie.setValue(buffer.toString());
return sessionCookie;
return buffer.toString();
}
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
@@ -275,8 +261,8 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
String requestedAlias = getCurrentSessionAlias(request);
sessionIds.remove(requestedAlias);
Cookie sessionCookie = createSessionCookie(request, sessionIds);
response.addCookie(sessionCookie);
String cookieValue = createSessionCookieValue(sessionIds);
cookieSerializer.writeCookieValue(new CookieValue(request,response,cookieValue));
}
/**
@@ -294,45 +280,30 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
}
/**
* Sets the name of the cookie to be used
* @param cookieName the name of the cookie to be used
* Sets the {@link CookieSerializer} to be used.
*
* @param cookieSerializer the cookieSerializer to set. Cannot be null.
*/
public void setCookieName(String cookieName) {
if(cookieName == null) {
throw new IllegalArgumentException("cookieName cannot be null");
}
this.cookieName = cookieName;
public void setCookieSerializer(CookieSerializer cookieSerializer) {
Assert.notNull(cookieSerializer, "cookieSerializer cannot be null");
this.cookieSerializer = cookieSerializer;
}
/**
* 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
* Sets the name of the cookie to be used
* @param cookieName the name of the cookie to be used
* @deprecated use {@link #setCookieSerializer(CookieSerializer)}
*/
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() + "/";
@Deprecated
public void setCookieName(String cookieName) {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName(cookieName);
this.cookieSerializer = serializer;
}
public Map<String,String> getSessionIds(HttpServletRequest request) {
Cookie session = getCookie(request, cookieName);
String sessionCookieValue = session == null ? "" : session.getValue();
List<String> cookieValues = cookieSerializer.readCookieValues(request);
String sessionCookieValue = cookieValues.isEmpty() ? "" : cookieValues.iterator().next();
Map<String,String> result = new LinkedHashMap<String,String>();
StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " ");
if(tokens.countTokens() == 1) {
@@ -411,16 +382,4 @@ public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy
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;
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2015 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.List;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Strategy for reading and writing a cookie value to the
* {@link HttpServletResponse}.
*
* @author Rob Winch
* @since 1.1
*/
public interface CookieSerializer {
/**
* Writes a given {@link CookieValue} to the provided
* {@link HttpServletResponse}
*
* @param cookieValue
* the {@link CookieValue} to write to
* {@link CookieValue#getResponse()}. Cannot be null.
*/
void writeCookieValue(CookieValue cookieValue);
/**
* Reads all the matching cookies from the {@link HttpServletRequest}. The
* result is a List since there can be multiple {@link Cookie} in a single
* request with a matching name. For example, one Cookie may have a path of
* / and another of /context, but the path is not transmitted in the
* request.
*
* @param request
* the {@link HttpServletRequest} to read the cookie from. Cannot
* be null.
* @return the values of all the matching cookies
*/
List<String> readCookieValues(HttpServletRequest request);
/**
* Contains the information necessary to write a value to the
* {@link HttpServletResponse}.
*
* @author Rob Winch
* @since 1.1
*/
public class CookieValue {
private final HttpServletRequest request;
private final HttpServletResponse response;
private final String cookieValue;
/**
* Creates a new instance
*
* @param request
* the {@link HttpServletRequest} to use. Useful for
* determining the context in which the cookie is set. Cannot
* be null.
* @param response
* the {@link HttpServletResponse} to use.
* @param cookieValue
* the value of the cookie to be written. This value may be
* modified by the {@link CookieSerializer} when writing to
* the actual cookie so long as the original value is
* returned when the cookie is read.
*/
public CookieValue(HttpServletRequest request, HttpServletResponse response, String cookieValue) {
this.request = request;
this.response = response;
this.cookieValue = cookieValue;
}
/**
* Gets the request to use.
* @return the request to use. Cannot be null.
*/
public HttpServletRequest getRequest() {
return request;
}
/**
* Gets the response to write to.
* @return the response to write to. Cannot be null.
*/
public HttpServletResponse getResponse() {
return response;
}
/**
* The value to be written. This value may be modified by the {@link CookieSerializer} before written to the cookie. However, the value must be the same as the original when it is read back in.
*
* @return the value to be written
*/
public String getCookieValue() {
return cookieValue;
}
}
}

View File

@@ -0,0 +1,276 @@
/*
* Copyright 2002-2015 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.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* The default implementation of {@link CookieSerializer}
*
* @author Rob Winch
* @since 1.1
*/
public class DefaultCookieSerializer implements CookieSerializer {
private String cookieName = "SESSION";
private Boolean useSecureCookie;
private boolean useHttpOnlyCookie = isServlet3();
private String cookiePath;
private int cookieMaxAge = -1;
private String domainName;
private Pattern domainNamePattern;
private String jvmRoute;
/*
* (non-Javadoc)
* @see org.springframework.session.web.http.CookieSerializer#readCookieValues(javax.servlet.http.HttpServletRequest)
*/
public List<String> readCookieValues(HttpServletRequest request) {
Cookie cookies[] = request.getCookies();
List<String> matchingCookieValues = new ArrayList<String>();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
String sessionId = cookie.getValue();
if(jvmRoute != null && sessionId.endsWith(jvmRoute)) {
sessionId = sessionId.substring(0, sessionId.length() - jvmRoute.length());
}
matchingCookieValues.add(sessionId);
}
}
}
return matchingCookieValues;
}
/*
* (non-Javadoc)
* @see
* org.springframework.session.web.http.CookieWriter#writeCookieValue(org.
* springframework.session.web.http.CookieWriter.CookieValue)
*/
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = jvmRoute == null ? requestedCookieValue : requestedCookieValue + jvmRoute;
Cookie sessionCookie = new Cookie(cookieName, actualCookieValue);
sessionCookie.setSecure(isSecureCookie(request));
sessionCookie.setPath(getCookiePath(request));
String domainName = getDomainName(request);
if (domainName != null) {
sessionCookie.setDomain(domainName);
}
if (useHttpOnlyCookie) {
sessionCookie.setHttpOnly(true);
}
if ("".equals(requestedCookieValue)) {
sessionCookie.setMaxAge(0);
} else {
sessionCookie.setMaxAge(cookieMaxAge);
}
response.addCookie(sessionCookie);
}
/**
* Sets if a Cookie marked as secure should be used. The default is to use
* the value of {@link HttpServletRequest#isSecure()}.
*
* @param useSecureCookie
* determines if the cookie should be marked as secure.
*/
public void setUseSecureCookie(boolean useSecureCookie) {
this.useSecureCookie = useSecureCookie;
}
/**
* Sets if a Cookie marked as HTTP Only should be used. The default is true
* in Servlet 3+ environments, else false.
*
* @param useHttpOnlyCookie
* determines if the cookie should be marked as HTTP Only.
*/
public void setUseHttpOnlyCookie(boolean useHttpOnlyCookie) {
if(useHttpOnlyCookie && !isServlet3()) {
throw new IllegalArgumentException("You cannot set useHttpOnlyCookie to true in pre Servlet 3 environment");
}
this.useHttpOnlyCookie = useHttpOnlyCookie;
}
private boolean isSecureCookie(HttpServletRequest request) {
if (useSecureCookie == null) {
return request.isSecure();
}
return useSecureCookie;
}
/**
* Sets the path of the Cookie. The default is to use the context path from
* the {@link HttpServletRequest}.
*
* @param cookiePath
* the path of the Cookie. If null, the default of the context
* path will be used.
*/
public void setCookiePath(String cookiePath) {
this.cookiePath = cookiePath;
}
public void setCookieName(String cookieName) {
if (cookieName == null) {
throw new IllegalArgumentException("cookieName cannot be null");
}
this.cookieName = cookieName;
}
/**
* Sets the maxAge property of the Cookie. The default is -1 which signals
* to delete the cookie when the browser is closed.
*
* @param cookieMaxAge
* the maxAge property of the Cookie
*/
public void setCookieMaxAge(int cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
}
/**
* Sets an explicit Domain Name. This allow the domain of "example.com" to
* be used when the request comes from www.example.com. This allows for
* sharing the cookie across subdomains. The default is to use the current
* domain.
*
* @param domainName
* the name of the domain to use. (i.e. "example.com")
* @throws IllegalStateException if the domainNamePattern is also set
*/
public void setDomainName(String domainName) {
if (this.domainNamePattern != null) {
throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
}
this.domainName = domainName;
}
/**
* <p>
* Sets a case insensitive pattern used to extract the domain name from the
* {@link HttpServletRequest#getServerName()}. The pattern should provide a
* single grouping that defines what the value is that should be matched.
* User's should be careful not to output malicious characters like new
* lines to prevent from things like
* <a href= "https://www.owasp.org/index.php/HTTP_Response_Splitting">HTTP
* Response Splitting</a>.
* </p>
*
* <p>
* If the pattern does not match, then no domain will be set. This is useful
* to ensure the domain is not set during development when localhost might
* be used.
* </p>
* <p>
* An example value might be "^.+?\\.(\\w+\\.[a-z]+)$". For the given input,
* it would provide the following explicit domain (null means no domain name
* is set):
* </p>
*
* <ul>
* <li>example.com - null</li>
* <li>child.sub.example.com - example.com</li>
* <li>localhost - null</li>
* <li>127.0.1.1 - null</li>
* </ul>
*
* @param domainNamePattern
* the case insensitive pattern to extract the domain name with
* @throws IllegalStateException if the domainName is also set
*/
public void setDomainNamePattern(String domainNamePattern) {
if (this.domainName != null) {
throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
}
this.domainNamePattern = Pattern.compile(domainNamePattern, Pattern.CASE_INSENSITIVE);
}
/**
* <p>
* Used to identify which JVM to route to for session affinity. With some
* implementations (i.e. Redis) this provides no performance benefit.
* However, this can help with tracing logs of a particular user.
* </p>
* <p>
* To use set a custom route on each JVM instance and setup a frontend proxy
* to forward all requests to the JVM based on the route.
* </p>
*
* @param jvmRoute
* the JVM Route to use (i.e. "node01jvmA", "n01ja", etc)
*/
public void setJvmRoute(String jvmRoute) {
this.jvmRoute = jvmRoute;
}
private String getDomainName(HttpServletRequest request) {
if (domainName != null) {
return domainName;
}
if (domainNamePattern != null) {
Matcher matcher = domainNamePattern.matcher(request.getServerName());
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
private String getCookiePath(HttpServletRequest request) {
if (cookiePath == null) {
return request.getContextPath() + "/";
}
return cookiePath;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2002-2015 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.config.annotation.web.http;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.http.CookieSerializer;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* @author Rob Winch
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class EnableSpringHttpSessionCustomCookieSerializerTests {
@Autowired
MockHttpServletRequest request;
@Autowired
MockHttpServletResponse response;
MockFilterChain chain;
@Autowired
SessionRepositoryFilter<? extends ExpiringSession> sessionRepositoryFilter;
@Autowired
CookieSerializer cookieSerializer;
@Before
public void setup() {
chain = new MockFilterChain();
}
@Test
public void usesReadSessionIds() throws Exception {
String sessionId = "sessionId";
when(cookieSerializer.readCookieValues(any(HttpServletRequest.class))).thenReturn(Arrays.asList(sessionId));
sessionRepositoryFilter.doFilter(request, response, chain);
assertThat(getRequest().getRequestedSessionId()).isEqualTo(sessionId);
}
@Test
public void usesWrite() throws Exception {
sessionRepositoryFilter.doFilter(request, response, new MockFilterChain() {
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
((HttpServletRequest) request).getSession();
super.doFilter(request, response);
}
});
verify(cookieSerializer).writeCookieValue(any(CookieValue.class));
}
private HttpServletRequest getRequest() {
return (HttpServletRequest) chain.getRequest();
}
@EnableSpringHttpSession
@Configuration
static class Config {
@Bean
public MapSessionRepository mapSessionRepository() {
return new MapSessionRepository();
}
@Bean
public CookieSerializer cookieSerializer() {
return mock(CookieSerializer.class);
}
}
}

View File

@@ -162,6 +162,7 @@ public class CookieHttpSessionStrategyTests {
assertThat(getSessionId()).isEqualTo(existing.getId());
}
@SuppressWarnings("deprecation")
@Test(expected = IllegalArgumentException.class)
public void setCookieNameNull() throws Exception {
strategy.setCookieName(null);
@@ -439,6 +440,7 @@ public class CookieHttpSessionStrategyTests {
return buffer.toString();
}
@SuppressWarnings("deprecation")
public void setCookieName(String cookieName) {
strategy.setCookieName(cookieName);
this.cookieName = cookieName;

View File

@@ -0,0 +1,373 @@
/*
* Copyright 2002-2015 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 static org.fest.assertions.Assertions.assertThat;
import javax.servlet.http.Cookie;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
/**
* @author Rob Winch
*
*/
public class DefaultCookieSerializerTests {
String cookieName;
MockHttpServletRequest request;
MockHttpServletResponse response;
DefaultCookieSerializer serializer;
String sessionId;
@Before
public void setup() {
cookieName = "SESSION";
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
sessionId = "sessionId";
serializer = new DefaultCookieSerializer();
}
// --- readCookieValues ---
@Test
public void readCookieValuesNull() {
assertThat(serializer.readCookieValues(request)).isEmpty();
}
@Test
public void readCookieValuesSingle() {
request.setCookies(new Cookie(cookieName, sessionId));
assertThat(serializer.readCookieValues(request)).containsOnly(sessionId);
}
@Test
public void readCookieValuesSingleAndInvalidName() {
request.setCookies(new Cookie(cookieName, sessionId), new Cookie(cookieName+"INVALID", sessionId + "INVALID"));
assertThat(serializer.readCookieValues(request)).containsOnly(sessionId);
}
@Test
public void readCookieValuesMulti() {
String secondSession = "secondSessionId";
request.setCookies(new Cookie(cookieName, sessionId), new Cookie(cookieName, secondSession));
assertThat(serializer.readCookieValues(request)).containsExactly(sessionId, secondSession);
}
@Test
public void readCookieValuesMultiCustomSessionCookieName() {
setCookieName("JSESSIONID");
String secondSession = "secondSessionId";
request.setCookies(new Cookie(cookieName, sessionId), new Cookie(cookieName, secondSession));
assertThat(serializer.readCookieValues(request)).containsExactly(sessionId, secondSession);
}
// --- writeCookie ---
@Test
public void writeCookie() {
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getValue()).isEqualTo(sessionId);
}
// --- httpOnly ---
@Test
public void writeCookieHttpOnlyDefault() {
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().isHttpOnly()).isTrue();
}
@Test
public void writeCookieHttpOnlySetTrue() {
serializer.setUseHttpOnlyCookie(true);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().isHttpOnly()).isTrue();
}
@Test
public void writeCookieHttpOnlySetFalse() {
serializer.setUseHttpOnlyCookie(false);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().isHttpOnly()).isFalse();
}
// --- domainName ---
@Test
public void writeCookieDomainNameDefault() {
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getDomain()).isNull();
}
@Test
public void writeCookieDomainNameCustom() {
String domainName = "example.com";
serializer.setDomainName(domainName);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getDomain()).isEqualTo(domainName);
}
@Test(expected=IllegalStateException.class)
public void setDomainNameAndDomainNamePatternThrows() {
serializer.setDomainName("example.com");
serializer.setDomainNamePattern(".*");
}
// --- domainNamePattern ---
@Test
public void writeCookieDomainNamePattern() {
String domainNamePattern = "^.+?\\.(\\w+\\.[a-z]+)$";
serializer.setDomainNamePattern(domainNamePattern);
String[] matchingDomains = {"child.sub.example.com","www.example.com"};
for(String domain : matchingDomains) {
request.setServerName(domain);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getDomain()).isEqualTo("example.com");
response = new MockHttpServletResponse();
}
String[] notMatchingDomains = {"example.com", "localhost","127.0.0.1"};
for(String domain : notMatchingDomains) {
request.setServerName(domain);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getDomain()).isNull();
response = new MockHttpServletResponse();
}
}
@Test(expected=IllegalStateException.class)
public void setDomainNamePatternAndDomainNameThrows() {
serializer.setDomainNamePattern(".*");
serializer.setDomainName("example.com");
}
// --- cookieName ---
@Test
public void writeCookieCookieNameDefault() {
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getName()).isEqualTo("SESSION");
}
@Test
public void writeCookieCookieNameCustom() {
String cookieName = "JSESSIONID";
setCookieName(cookieName);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getName()).isEqualTo(cookieName);
}
@Test(expected=IllegalArgumentException.class)
public void setCookieNameNullThrows() {
serializer.setCookieName(null);
}
// --- cookiePath ---
@Test
public void writeCookieCookiePathDefaultEmptyContextPathUsed() {
request.setContextPath("");
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getPath()).isEqualTo("/");
}
@Test
public void writeCookieCookiePathDefaultContextPathUsed() {
request.setContextPath("/context");
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
}
@Test
public void writeCookieCookiePathExplicitNullCookiePathContextPathUsed() {
request.setContextPath("/context");
serializer.setCookiePath(null);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getPath()).isEqualTo("/context/");
}
@Test
public void writeCookieCookiePathExplicitCookiePath() {
request.setContextPath("/context");
serializer.setCookiePath("/");
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getPath()).isEqualTo("/");
}
// --- cookieMaxAge ---
@Test
public void writeCookieCookieMaxAgeDefault() {
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(-1);
}
@Test
public void writeCookieCookieMaxAgeExplicit() {
serializer.setCookieMaxAge(100);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getMaxAge()).isEqualTo(100);
}
@Test
public void writeCookieCookieMaxAgeExplicitEmptyCookie() {
serializer.setCookieMaxAge(100);
serializer.writeCookieValue(cookieValue(""));
assertThat(getCookie().getMaxAge()).isEqualTo(0);
}
// --- secure ---
@Test
public void writeCookieDefaultInsecureRequest() {
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
@Test
public void writeCookieSecureSecureRequest() {
request.setSecure(true);
serializer.setUseSecureCookie(true);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getSecure()).isTrue();
}
@Test
public void writeCookieSecureInsecureRequest() {
serializer.setUseSecureCookie(true);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getSecure()).isTrue();
}
@Test
public void writeCookieInsecureSecureRequest() {
request.setSecure(true);
serializer.setUseSecureCookie(false);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
@Test
public void writeCookieInecureInsecureRequest() {
serializer.setUseSecureCookie(false);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getSecure()).isFalse();
}
// --- jvmRoute ---
@Test
public void writeCookieJvmRoute() {
String jvmRoute = "route";
serializer.setJvmRoute(jvmRoute);
serializer.writeCookieValue(cookieValue(sessionId));
assertThat(getCookie().getValue()).isEqualTo(sessionId + jvmRoute);
}
@Test
public void readCookieJvmRoute() {
String jvmRoute = "route";
serializer.setJvmRoute(jvmRoute);
request.setCookies(new Cookie(cookieName, sessionId + jvmRoute));
assertThat(serializer.readCookieValues(request)).containsOnly(sessionId);
}
@Test
public void readCookieJvmRouteRouteMissing() {
String jvmRoute = "route";
serializer.setJvmRoute(jvmRoute);
request.setCookies(new Cookie(cookieName, sessionId));
assertThat(serializer.readCookieValues(request)).containsOnly(sessionId);
}
@Test
public void readCookieJvmRouteOnlyRoute() {
String jvmRoute = "route";
serializer.setJvmRoute(jvmRoute);
request.setCookies(new Cookie(cookieName, jvmRoute));
assertThat(serializer.readCookieValues(request)).containsOnly("");
}
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
this.serializer.setCookieName(cookieName);
}
private Cookie getCookie() {
return response.getCookie(cookieName);
}
private CookieValue cookieValue(String cookieValue) {
return new CookieValue(request, response, cookieValue);
}
}