diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java index e2a0aae66f..3a55067e34 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java @@ -61,10 +61,9 @@ public class SessionAutoConfiguration { @PostConstruct public void applyConfigurationProperties() { - if (this.serverProperties.getSessionTimeout() != null) { - this.sessionRepository - .setDefaultMaxInactiveInterval(this.serverProperties - .getSessionTimeout()); + Integer timeout = this.serverProperties.getSession().getTimeout(); + if (timeout != null) { + this.sessionRepository.setDefaultMaxInactiveInterval(timeout); } } 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 8af0c07315..430728dd23 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 @@ -21,7 +21,12 @@ import java.net.InetAddress; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; import javax.validation.constraints.NotNull; import org.apache.catalina.Context; @@ -31,6 +36,7 @@ import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.springframework.boot.autoconfigure.web.ServerProperties.Session.Cookie; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; @@ -38,6 +44,7 @@ import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomi import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.InitParameterConfiguringServletContextInitializer; import org.springframework.boot.context.embedded.JspServlet; +import org.springframework.boot.context.embedded.ServletContextInitializer; import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; @@ -72,11 +79,6 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord */ private InetAddress address; - /** - * Session timeout in seconds. - */ - private Integer sessionTimeout; - /** * Context path of the application. */ @@ -87,6 +89,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord */ private String displayName = "application"; + private Session session = new Session(); + @NestedConfigurationProperty private Ssl ssl; @@ -192,12 +196,28 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord this.address = address; } + /** + * Set the session timeout + * @return the session timeout + * @deprecated since 1.3.0 in favor of {@code session.timeout}. + */ + @Deprecated public Integer getSessionTimeout() { - return this.sessionTimeout; + return this.session.getTimeout(); } + /** + * Get the session timeout + * @param sessionTimeout the session timeout + * @deprecated since 1.3.0 in favor of {@code session.timeout}. + */ + @Deprecated public void setSessionTimeout(Integer sessionTimeout) { - this.sessionTimeout = sessionTimeout; + this.session.setTimeout(sessionTimeout); + } + + public Session getSession() { + return this.session; } public Ssl getSsl() { @@ -238,8 +258,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord if (getDisplayName() != null) { container.setDisplayName(getDisplayName()); } - if (getSessionTimeout() != null) { - container.setSessionTimeout(getSessionTimeout()); + if (getSession().getTimeout() != null) { + container.setSessionTimeout(getSession().getTimeout()); } if (getSsl() != null) { container.setSsl(getSsl()); @@ -258,6 +278,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord getUndertow().customizeUndertow( (UndertowEmbeddedServletContainerFactory) container); } + container.addInitializers(new SessionConfiguringInitializer(this.session)); container.addInitializers(new InitParameterConfiguringServletContextInitializer( getContextParameters())); } @@ -288,6 +309,137 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord return prefix + path; } + public static class Session { + + /** + * Session timeout in seconds. + */ + private Integer timeout; + + /** + * Session tracking modes (one or more of the following: "cookie", "url", "ssl") + */ + private Set trackingModes; + + private Cookie cookie = new Cookie(); + + public Cookie getCookie() { + return this.cookie; + } + + public Integer getTimeout() { + return this.timeout; + } + + public void setTimeout(Integer sessionTimeout) { + this.timeout = sessionTimeout; + } + + public Set getTrackingModes() { + return this.trackingModes; + } + + public void setTrackingModes(Set trackingModes) { + this.trackingModes = trackingModes; + } + + public static class Cookie { + + /** + * Session cookie name. + */ + private String name; + + /** + * Domain for the session cookie. + */ + private String domain; + + /** + * Path of the session cookie. + */ + private String path; + + /** + * Comment for the session cookie. + */ + private String comment; + + /** + * "HttpOnly" flag for the session cookie. + */ + private Boolean httpOnly; + + /** + * "Secure" flag for the session cookie. + */ + private Boolean secure; + + /** + * Maximum age of the session cookie in seconds. + */ + private Integer maxAge; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDomain() { + return this.domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getComment() { + return this.comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Boolean getHttpOnly() { + return this.httpOnly; + } + + public void setHttpOnly(Boolean httpOnly) { + this.httpOnly = httpOnly; + } + + public Boolean getSecure() { + return this.secure; + } + + public void setSecure(Boolean secure) { + this.secure = secure; + } + + public Integer getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Integer maxAge) { + this.maxAge = maxAge; + } + + } + + } + public static class Tomcat { /** @@ -662,4 +814,51 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord } + /** + * {@link ServletContextInitializer} to apply appropriate parts of the {@link Session} + * configuration. + */ + private static class SessionConfiguringInitializer implements + ServletContextInitializer { + + private final Session session; + + public SessionConfiguringInitializer(Session session) { + this.session = session; + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + if (this.session.getTrackingModes() != null) { + servletContext.setSessionTrackingModes(this.session.getTrackingModes()); + } + configureSessionCookie(servletContext.getSessionCookieConfig()); + } + + private void configureSessionCookie(SessionCookieConfig config) { + Cookie cookie = this.session.getCookie(); + if (cookie.getName() != null) { + config.setName(cookie.getName()); + } + if (cookie.getDomain() != null) { + config.setDomain(cookie.getDomain()); + } + if (cookie.getPath() != null) { + config.setPath(cookie.getPath()); + } + if (cookie.getComment() != null) { + config.setComment(cookie.getComment()); + } + if (cookie.getHttpOnly() != null) { + config.setHttpOnly(cookie.getHttpOnly()); + } + if (cookie.getSecure() != null) { + config.setSecure(cookie.getSecure()); + } + if (cookie.getMaxAge() != null) { + config.setMaxAge(cookie.getMaxAge()); + } + } + + } } diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index b09f535118..019a774d9b 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -157,6 +157,12 @@ "type": "java.lang.String", "description": "Spring MVC view suffix.", "deprecated": true + }, + { + "name": "server.session-timeout", + "type": "java.lang.Integer", + "description": "Session timeout in seconds.", + "deprecated": true } ],"hints": [ { 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 154e6b8b65..f1148ee98d 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 @@ -18,21 +18,34 @@ package org.springframework.boot.autoconfigure.web; import java.net.InetAddress; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; + import org.apache.catalina.Valve; import org.apache.catalina.valves.RemoteIpValve; +import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; import org.springframework.beans.MutablePropertyValues; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; +import org.springframework.boot.context.embedded.ServletContextInitializer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -48,6 +61,14 @@ public class ServerPropertiesTests { private final ServerProperties properties = new ServerProperties(); + @Captor + private ArgumentCaptor initializersCaptor; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + @Test public void testAddressBinding() throws Exception { RelaxedDataBinder binder = new RelaxedDataBinder(this.properties, "server"); @@ -123,6 +144,53 @@ public class ServerPropertiesTests { verify(factory).setDisplayName("TestName"); } + @Test + public void customizeSessionProperties() throws Exception { + Map map = new HashMap(); + map.put("server.session.timeout", "123"); + map.put("server.session.tracking-modes", "cookie,url"); + map.put("server.session.cookie.name", "testname"); + map.put("server.session.cookie.domain", "testdomain"); + map.put("server.session.cookie.path", "/testpath"); + map.put("server.session.cookie.comment", "testcomment"); + map.put("server.session.cookie.http-only", "true"); + map.put("server.session.cookie.secure", "true"); + map.put("server.session.cookie.max-age", "60"); + bindProperties(map); + ConfigurableEmbeddedServletContainer factory = mock(ConfigurableEmbeddedServletContainer.class); + ServletContext servletContext = mock(ServletContext.class); + SessionCookieConfig sessionCookieConfig = mock(SessionCookieConfig.class); + given(servletContext.getSessionCookieConfig()).willReturn(sessionCookieConfig); + this.properties.customize(factory); + triggerInitializers(factory, servletContext); + verify(factory).setSessionTimeout(123); + verify(servletContext).setSessionTrackingModes( + EnumSet.of(SessionTrackingMode.COOKIE, SessionTrackingMode.URL)); + verify(sessionCookieConfig).setName("testname"); + verify(sessionCookieConfig).setDomain("testdomain"); + verify(sessionCookieConfig).setPath("/testpath"); + verify(sessionCookieConfig).setComment("testcomment"); + verify(sessionCookieConfig).setHttpOnly(true); + verify(sessionCookieConfig).setSecure(true); + verify(sessionCookieConfig).setMaxAge(60); + } + + private void triggerInitializers(ConfigurableEmbeddedServletContainer container, + ServletContext servletContext) throws ServletException { + verify(container, atLeastOnce()).addInitializers( + this.initializersCaptor.capture()); + for (Object initializers : this.initializersCaptor.getAllValues()) { + if (initializers instanceof ServletContextInitializer) { + ((ServletContextInitializer) initializers).onStartup(servletContext); + } + else { + for (ServletContextInitializer initializer : (ServletContextInitializer[]) initializers) { + initializer.onStartup(servletContext); + } + } + } + } + @Test public void testCustomizeTomcatPort() throws Exception { ConfigurableEmbeddedServletContainer factory = mock(ConfigurableEmbeddedServletContainer.class); 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 11896b5d6e..242ed206e4 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -65,7 +65,6 @@ content into your application; rather pick only the properties that you need. # EMBEDDED SERVER CONFIGURATION ({sc-spring-boot-autoconfigure}/web/ServerProperties.{sc-ext}[ServerProperties]) server.port=8080 server.address= # bind to a specific NIC - server.session-timeout= # session timeout in seconds server.compression.enabled=false # if response compression is enabled server.compression.mime-types=text/html,text/xml,text/plain,text/css # comma-separated list of MIME types that should be compressed server.compression.min-response-size=2048 # minimum response size that is required for compression to be performed @@ -76,6 +75,15 @@ content into your application; rather pick only the properties that you need. server.jsp-servlet.registered=true # Whether or not the JSP servlet is registered server.servlet-path= # the servlet path, defaults to '/' server.display-name= # the display name of the application + server.session.timeout= # session timeout in seconds + server.session.tracking-modes= # tracking modes (one or more of "cookie" ,"url", "ssl") + server.session.cookie.name= + server.session.cookie.domain= + server.session.cookie.path= + server.session.cookie.comment= + server.session.cookie.http-only= + server.session.cookie.secure= + server.session.cookie.max-age= server.ssl.enabled=true # if SSL support is enabled server.ssl.client-auth= # want or need server.ssl.key-alias=