diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 3999b8443a..5d76463506 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -162,6 +162,7 @@ public class ServerProperties container.setSessionTimeout(getSession().getTimeout()); } container.setPersistSession(getSession().isPersistent()); + container.setSessionStoreDir(getSession().getStoreDir()); if (getSsl() != null) { container.setSsl(getSsl()); } @@ -392,6 +393,11 @@ public class ServerProperties */ private boolean persistent; + /** + * The directory used to store session data. + */ + private File storeDir; + private Cookie cookie = new Cookie(); public Cookie getCookie() { @@ -422,6 +428,14 @@ public class ServerProperties this.persistent = persistent; } + public File getStoreDir() { + return this.storeDir; + } + + public void setStoreDir(File storeDir) { + this.storeDir = storeDir; + } + public static class Cookie { /** diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 2ef2f65353..29f1980671 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.web; +import java.io.File; import java.net.InetAddress; import java.nio.charset.Charset; import java.util.Collections; @@ -384,6 +385,17 @@ public class ServerPropertiesTests { verify(container).setUseForwardHeaders(true); } + @Test + public void sessionStoreDir() throws Exception { + Map map = new HashMap(); + 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 map) { new RelaxedDataBinder(this.properties, "server") .bind(new MutablePropertyValues(map)); diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 6311ebcd20..8e089c688d 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -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.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.store-dir= # dir used to save session data server.session.timeout= # session timeout in seconds server.session.tracking-modes= # tracking modes (one or more of "cookie" ,"url", "ssl") server.session.cookie.name= # session cookie name diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 005bb2a884..96e2e9c0de 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -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 classpath, Spring Boot will auto-configure Spring Session through its `@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. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java index 5d2a3415d2..66838d991d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java @@ -66,6 +66,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer private boolean persistSession; + private File sessionStoreDir; + private Ssl ssl; private JspServlet jspServlet = new JspServlet(); @@ -191,6 +193,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer return this.persistSession; } + @Override + public void setSessionStoreDir(File sessionStoreDir) { + this.sessionStoreDir = sessionStoreDir; + } + + public File getSessionStoreDir() { + return this.sessionStoreDir; + } + @Override public void setInitializers(List initializers) { Assert.notNull(initializers, "Initializers must not be null"); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java index 83248ea00f..690cc3b695 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java @@ -26,6 +26,9 @@ import java.util.Arrays; import org.apache.commons.logging.Log; 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. @@ -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; + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java index d9d4188332..321680cb9f 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java @@ -79,6 +79,12 @@ public interface ConfigurableEmbeddedServletContainer { */ 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. * @param address the address to set (defaults to {@code null}) diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index c861bd5385..83d969086b 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -53,7 +53,6 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; -import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -304,8 +303,8 @@ public class JettyEmbeddedServletContainerFactory private void configurePersistSession(SessionManager sessionManager) { try { - File storeDirectory = new ApplicationTemp().getFolder("jetty-sessions"); - ((HashSessionManager) sessionManager).setStoreDirectory(storeDirectory); + ((HashSessionManager) sessionManager) + .setStoreDirectory(getValidSessionStoreDir()); } catch (IOException ex) { throw new IllegalStateException(ex); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index cf734863a2..f4028c0d94 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -52,7 +52,6 @@ import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.beans.BeanUtils; -import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -411,7 +410,7 @@ public class TomcatEmbeddedServletContainerFactory Assert.state(manager instanceof StandardManager, "Unable to persist HTTP session state using manager type " + manager.getClass().getName()); - File folder = new ApplicationTemp().getFolder("tomcat-sessions"); + File folder = getValidSessionStoreDir(); File file = new File(folder, "SESSIONS.ser"); ((StandardManager) manager).setPathname(file.getAbsolutePath()); } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java index d58c9df95e..53eca72cd6 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java @@ -38,7 +38,6 @@ import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; -import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; @@ -360,7 +359,7 @@ public class UndertowEmbeddedServletContainerFactory configureAccessLog(deployment); } if (isPersistSession()) { - File folder = new ApplicationTemp().getFolder("undertow-sessions"); + File folder = getValidSessionStoreDir(); deployment.setSessionPersistenceManager(new FileSessionPersistence(folder)); } DeploymentManager manager = Servlets.defaultContainer().addDeployment(deployment); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index 68ac17d9f4..44e16adedf 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; +import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -58,6 +59,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; 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.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -72,6 +75,7 @@ import org.springframework.util.concurrent.ListenableFuture; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.notNullValue; @@ -564,6 +568,54 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { 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 public void compression() throws Exception { assertTrue(doTestCompression(10000, null, null));