Commit 922f8b6b authored by Phillip Webb's avatar Phillip Webb

Add `server.session.store-dir` support

Add support for a `server.session.store-dir` property which can be used
to specify where session data source be saved.

Fixes gh-4191
parent 1a764d9c
...@@ -162,6 +162,7 @@ public class ServerProperties ...@@ -162,6 +162,7 @@ public class ServerProperties
container.setSessionTimeout(getSession().getTimeout()); container.setSessionTimeout(getSession().getTimeout());
} }
container.setPersistSession(getSession().isPersistent()); container.setPersistSession(getSession().isPersistent());
container.setSessionStoreDir(getSession().getStoreDir());
if (getSsl() != null) { if (getSsl() != null) {
container.setSsl(getSsl()); container.setSsl(getSsl());
} }
...@@ -392,6 +393,11 @@ public class ServerProperties ...@@ -392,6 +393,11 @@ public class ServerProperties
*/ */
private boolean persistent; private boolean persistent;
/**
* The directory used to store session data.
*/
private File storeDir;
private Cookie cookie = new Cookie(); private Cookie cookie = new Cookie();
public Cookie getCookie() { public Cookie getCookie() {
...@@ -422,6 +428,14 @@ public class ServerProperties ...@@ -422,6 +428,14 @@ public class ServerProperties
this.persistent = persistent; this.persistent = persistent;
} }
public File getStoreDir() {
return this.storeDir;
}
public void setStoreDir(File storeDir) {
this.storeDir = storeDir;
}
public static class Cookie { public static class Cookie {
/** /**
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.web; package org.springframework.boot.autoconfigure.web;
import java.io.File;
import java.net.InetAddress; import java.net.InetAddress;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
...@@ -384,6 +385,17 @@ public class ServerPropertiesTests { ...@@ -384,6 +385,17 @@ public class ServerPropertiesTests {
verify(container).setUseForwardHeaders(true); verify(container).setUseForwardHeaders(true);
} }
@Test
public void sessionStoreDir() throws Exception {
Map<String, String> map = new HashMap<String, String>();
map.put("server.session.store-dir", "myfolder");
bindProperties(map);
JettyEmbeddedServletContainerFactory container = spy(
new JettyEmbeddedServletContainerFactory());
this.properties.customize(container);
verify(container).setSessionStoreDir(new File("myfolder"));
}
private void bindProperties(Map<String, String> map) { private void bindProperties(Map<String, String> map) {
new RelaxedDataBinder(this.properties, "server") new RelaxedDataBinder(this.properties, "server")
.bind(new MutablePropertyValues(map)); .bind(new MutablePropertyValues(map));
......
...@@ -87,6 +87,7 @@ content into your application; rather pick only the properties that you need. ...@@ -87,6 +87,7 @@ content into your application; rather pick only the properties that you need.
server.display-name= # the display name of the application server.display-name= # the display name of the application
server.use-forward-headers= # if X-Forwarded-* headers should be used (default is off unless running in a known cloud) server.use-forward-headers= # if X-Forwarded-* headers should be used (default is off unless running in a known cloud)
server.session.persistent=false # true if session should be saved across restarts server.session.persistent=false # true if session should be saved across restarts
server.session.store-dir= # dir used to save session data
server.session.timeout= # session timeout in seconds server.session.timeout= # session timeout in seconds
server.session.tracking-modes= # tracking modes (one or more of "cookie" ,"url", "ssl") server.session.tracking-modes= # tracking modes (one or more of "cookie" ,"url", "ssl")
server.session.cookie.name= # session cookie name server.session.cookie.name= # session cookie name
......
...@@ -3584,7 +3584,7 @@ Spring Session provides support for managing a user's session information. If yo ...@@ -3584,7 +3584,7 @@ Spring Session provides support for managing a user's session information. If yo
writing a web application and Spring Session and Spring Data Redis are both on the writing a web application and Spring Session and Spring Data Redis are both on the
classpath, Spring Boot will auto-configure Spring Session through its classpath, Spring Boot will auto-configure Spring Session through its
`@EnableRedisHttpSession`. Session data will be stored in Redis and the session timeout `@EnableRedisHttpSession`. Session data will be stored in Redis and the session timeout
can be configured using the `server.session-timeout` property. can be configured using the `server.session.timeout` property.
......
...@@ -66,6 +66,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer ...@@ -66,6 +66,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer
private boolean persistSession; private boolean persistSession;
private File sessionStoreDir;
private Ssl ssl; private Ssl ssl;
private JspServlet jspServlet = new JspServlet(); private JspServlet jspServlet = new JspServlet();
...@@ -191,6 +193,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer ...@@ -191,6 +193,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer
return this.persistSession; return this.persistSession;
} }
@Override
public void setSessionStoreDir(File sessionStoreDir) {
this.sessionStoreDir = sessionStoreDir;
}
public File getSessionStoreDir() {
return this.sessionStoreDir;
}
@Override @Override
public void setInitializers(List<? extends ServletContextInitializer> initializers) { public void setInitializers(List<? extends ServletContextInitializer> initializers) {
Assert.notNull(initializers, "Initializers must not be null"); Assert.notNull(initializers, "Initializers must not be null");
......
...@@ -26,6 +26,9 @@ import java.util.Arrays; ...@@ -26,6 +26,9 @@ import java.util.Arrays;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ApplicationHome;
import org.springframework.boot.ApplicationTemp;
import org.springframework.util.Assert;
/** /**
* Abstract base class for {@link EmbeddedServletContainerFactory} implementations. * Abstract base class for {@link EmbeddedServletContainerFactory} implementations.
...@@ -140,4 +143,24 @@ public abstract class AbstractEmbeddedServletContainerFactory ...@@ -140,4 +143,24 @@ public abstract class AbstractEmbeddedServletContainerFactory
} }
} }
protected final File getValidSessionStoreDir() {
return getValidSessionStoreDir(true);
}
protected final File getValidSessionStoreDir(boolean mkdirs) {
File dir = getSessionStoreDir();
if (dir == null) {
return new ApplicationTemp().getFolder("servlet-sessions");
}
if (!dir.isAbsolute()) {
dir = new File(new ApplicationHome().getDir(), dir.getPath());
}
if (!dir.exists() && mkdirs) {
dir.mkdirs();
}
Assert.state(!mkdirs || dir.exists(), "Session dir " + dir + " does not exist");
Assert.state(!dir.isFile(), "Session dir " + dir + " points to a file");
return dir;
}
} }
...@@ -79,6 +79,12 @@ public interface ConfigurableEmbeddedServletContainer { ...@@ -79,6 +79,12 @@ public interface ConfigurableEmbeddedServletContainer {
*/ */
void setPersistSession(boolean persistSession); void setPersistSession(boolean persistSession);
/**
* Set the directory used to store serialized session data.
* @param sessionStoreDir the directory or {@code null} to use a default location.
*/
void setSessionStoreDir(File sessionStoreDir);
/** /**
* Sets the specific network address that the server should bind to. * Sets the specific network address that the server should bind to.
* @param address the address to set (defaults to {@code null}) * @param address the address to set (defaults to {@code null})
......
...@@ -53,7 +53,6 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; ...@@ -53,7 +53,6 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.AbstractConfiguration;
import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebAppContext;
import org.springframework.boot.ApplicationTemp;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.Compression;
import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer;
...@@ -304,8 +303,8 @@ public class JettyEmbeddedServletContainerFactory ...@@ -304,8 +303,8 @@ public class JettyEmbeddedServletContainerFactory
private void configurePersistSession(SessionManager sessionManager) { private void configurePersistSession(SessionManager sessionManager) {
try { try {
File storeDirectory = new ApplicationTemp().getFolder("jetty-sessions"); ((HashSessionManager) sessionManager)
((HashSessionManager) sessionManager).setStoreDirectory(storeDirectory); .setStoreDirectory(getValidSessionStoreDir());
} }
catch (IOException ex) { catch (IOException ex) {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
......
...@@ -52,7 +52,6 @@ import org.apache.coyote.ProtocolHandler; ...@@ -52,7 +52,6 @@ import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.AbstractHttp11JsseProtocol;
import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.boot.ApplicationTemp;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.Compression;
import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer;
...@@ -411,7 +410,7 @@ public class TomcatEmbeddedServletContainerFactory ...@@ -411,7 +410,7 @@ public class TomcatEmbeddedServletContainerFactory
Assert.state(manager instanceof StandardManager, Assert.state(manager instanceof StandardManager,
"Unable to persist HTTP session state using manager type " "Unable to persist HTTP session state using manager type "
+ manager.getClass().getName()); + manager.getClass().getName());
File folder = new ApplicationTemp().getFolder("tomcat-sessions"); File folder = getValidSessionStoreDir();
File file = new File(folder, "SESSIONS.ser"); File file = new File(folder, "SESSIONS.ser");
((StandardManager) manager).setPathname(file.getAbsolutePath()); ((StandardManager) manager).setPathname(file.getAbsolutePath());
} }
......
...@@ -38,7 +38,6 @@ import javax.servlet.ServletContainerInitializer; ...@@ -38,7 +38,6 @@ import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import org.springframework.boot.ApplicationTemp;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
...@@ -360,7 +359,7 @@ public class UndertowEmbeddedServletContainerFactory ...@@ -360,7 +359,7 @@ public class UndertowEmbeddedServletContainerFactory
configureAccessLog(deployment); configureAccessLog(deployment);
} }
if (isPersistSession()) { if (isPersistSession()) {
File folder = new ApplicationTemp().getFolder("undertow-sessions"); File folder = getValidSessionStoreDir();
deployment.setSessionPersistenceManager(new FileSessionPersistence(folder)); deployment.setSessionPersistenceManager(new FileSessionPersistence(folder));
} }
DeploymentManager manager = Servlets.defaultContainer().addDeployment(deployment); DeploymentManager manager = Servlets.defaultContainer().addDeployment(deployment);
......
...@@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded; ...@@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
...@@ -58,6 +59,8 @@ import org.junit.Test; ...@@ -58,6 +59,8 @@ import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.springframework.boot.ApplicationHome;
import org.springframework.boot.ApplicationTemp;
import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.boot.context.embedded.Ssl.ClientAuth;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
...@@ -72,6 +75,7 @@ import org.springframework.util.concurrent.ListenableFuture; ...@@ -72,6 +75,7 @@ import org.springframework.util.concurrent.ListenableFuture;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
...@@ -564,6 +568,54 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { ...@@ -564,6 +568,54 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
assertThat(message, s3.split(":")[0], equalTo(s2.split(":")[1])); assertThat(message, s3.split(":")[0], equalTo(s2.split(":")[1]));
} }
@Test
public void persistSessionInSpecificSessionStoreDir() throws Exception {
AbstractEmbeddedServletContainerFactory factory = getFactory();
File sessionStoreDir = this.temporaryFolder.newFolder();
factory.setPersistSession(true);
factory.setSessionStoreDir(sessionStoreDir);
this.container = factory
.getEmbeddedServletContainer(sessionServletRegistration());
this.container.start();
getResponse(getLocalUrl("/session"));
this.container.stop();
File[] dirContents = sessionStoreDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !(".".equals(name) || "..".equals(name));
}
});
assertThat(dirContents.length, greaterThan(0));
}
@Test
public void getValidSessionStoreWhenSessionStoreNotSet() throws Exception {
AbstractEmbeddedServletContainerFactory factory = getFactory();
File dir = factory.getValidSessionStoreDir(false);
assertThat(dir.getName(), equalTo("servlet-sessions"));
assertThat(dir.getParentFile(), equalTo(new ApplicationTemp().getFolder()));
}
@Test
public void getValidSessionStoreWhenSessionStoreIsRelative() throws Exception {
AbstractEmbeddedServletContainerFactory factory = getFactory();
factory.setSessionStoreDir(new File("sessions"));
File dir = factory.getValidSessionStoreDir(false);
assertThat(dir.getName(), equalTo("sessions"));
assertThat(dir.getParentFile(), equalTo(new ApplicationHome().getDir()));
}
@Test
public void getValidSessionStoreWhenSessionStoreReferencesFile() throws Exception {
AbstractEmbeddedServletContainerFactory factory = getFactory();
factory.setSessionStoreDir(this.temporaryFolder.newFile());
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("points to a file");
factory.getValidSessionStoreDir(false);
}
@Test @Test
public void compression() throws Exception { public void compression() throws Exception {
assertTrue(doTestCompression(10000, null, null)); assertTrue(doTestCompression(10000, null, null));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment