diff --git a/spring-boot-actuator-autoconfigure/pom.xml b/spring-boot-actuator-autoconfigure/pom.xml index 87a1f031de1f..8e85717eb562 100644 --- a/spring-boot-actuator-autoconfigure/pom.xml +++ b/spring-boot-actuator-autoconfigure/pom.xml @@ -311,6 +311,11 @@ spring-security-web true + + org.springframework.session + spring-session-core + true + org.springframework.boot diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java new file mode 100644 index 000000000000..ccf83b92e6cd --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2017 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.boot.actuate.autoconfigure.session; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.session.SessionsEndpoint; +import org.springframework.boot.actuate.session.SessionsWebEndpointExtension; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link SessionsEndpoint}. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +@Configuration +@ConditionalOnClass(FindByIndexNameSessionRepository.class) +@AutoConfigureAfter(SessionAutoConfiguration.class) +public class SessionsEndpointAutoConfiguration { + + @Bean + @ConditionalOnBean(FindByIndexNameSessionRepository.class) + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public SessionsEndpoint sessionEndpoint( + FindByIndexNameSessionRepository sessionRepository) { + return new SessionsEndpoint(sessionRepository); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + @ConditionalOnBean(SessionsEndpoint.class) + public SessionsWebEndpointExtension sessionsWebEndpointExtension( + SessionsEndpoint sessionsEndpoint) { + return new SessionsWebEndpointExtension(sessionsEndpoint); + } + +} diff --git a/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/package-info.java b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/package-info.java new file mode 100644 index 000000000000..90eec90bb75e --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Auto-configuration for actuator Spring Sessions concerns. + */ +package org.springframework.boot.actuate.autoconfigure.session; diff --git a/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 3d365c5bb98a..fb947772a080 100644 --- a/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -29,6 +29,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.redis.RedisHealthIndicatorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.solr.SolrHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration,\ diff --git a/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java b/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java new file mode 100644 index 000000000000..29149186aec6 --- /dev/null +++ b/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2017 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.boot.actuate.autoconfigure.session; + +import org.junit.Test; + +import org.springframework.boot.actuate.session.SessionsEndpoint; +import org.springframework.boot.actuate.session.SessionsWebEndpointExtension; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.FindByIndexNameSessionRepository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SessionsEndpointAutoConfiguration}. + * + * @author Vedran Pavic + */ +public class SessionsEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) + .withUserConfiguration(SessionConfiguration.class); + + @Test + public void runShouldHaveEndpointBean() { + this.contextRunner.run( + (context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class)); + } + + @Test + public void runShouldHaveWebExtensionBean() { + this.contextRunner.run((context) -> assertThat(context) + .hasSingleBean(SessionsWebEndpointExtension.class)); + } + + @Test + public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointOrExtensionBean() + throws Exception { + this.contextRunner.withPropertyValues("endpoints.sessions.enabled:false").run( + (context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class)); + } + + @Configuration + static class SessionConfiguration { + + @Bean + public FindByIndexNameSessionRepository sessionRepository() { + return mock(FindByIndexNameSessionRepository.class); + } + + } + +} diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 23a3afb7018a..6f42b7e5c70d 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -199,6 +199,11 @@ spring-security-web true + + org.springframework.session + spring-session-core + true + org.springframework.boot diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java new file mode 100644 index 000000000000..6e1debe35c42 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-2017 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.boot.actuate.session; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; + +/** + * {@link Endpoint} to expose a user's {@link Session}s. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +@Endpoint(id = "sessions") +public class SessionsEndpoint { + + private final FindByIndexNameSessionRepository sessionRepository; + + /** + * Create a new {@link SessionsEndpoint} instance. + * @param sessionRepository the session repository + */ + public SessionsEndpoint( + FindByIndexNameSessionRepository sessionRepository) { + this.sessionRepository = sessionRepository; + } + + @ReadOperation + public SessionsReport sessionsForUsername(String username) { + Map sessions = this.sessionRepository + .findByIndexNameAndIndexValue( + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, + username); + return new SessionsReport(sessions); + } + + @ReadOperation + public SessionDescriptor getSession(@Selector String sessionId) { + Session session = this.sessionRepository.findById(sessionId); + return new SessionDescriptor(session); + } + + @DeleteOperation + public void deleteSession(@Selector String sessionId) { + this.sessionRepository.deleteById(sessionId); + } + + /** + * A report of user's {@link Session sessions}. Primarily intended for serialization + * to JSON. + */ + public static final class SessionsReport { + + private final List sessions; + + public SessionsReport(Map sessions) { + this.sessions = sessions.entrySet().stream() + .map(s -> new SessionDescriptor(s.getValue())) + .collect(Collectors.toList()); + } + + public List getSessions() { + return this.sessions; + } + + } + + /** + * A description of user's {@link Session session}. Primarily intended for + * serialization to JSON. + */ + public static final class SessionDescriptor { + + private final String id; + + private final Set attributeNames; + + private final long creationTime; + + private final long lastAccessedTime; + + private final long maxInactiveInterval; + + private final boolean expired; + + public SessionDescriptor(Session session) { + this.id = session.getId(); + this.attributeNames = session.getAttributeNames(); + this.creationTime = session.getCreationTime().toEpochMilli(); + this.lastAccessedTime = session.getLastAccessedTime().toEpochMilli(); + this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds(); + this.expired = session.isExpired(); + } + + public String getId() { + return this.id; + } + + public Set getAttributeNames() { + return this.attributeNames; + } + + public long getCreationTime() { + return this.creationTime; + } + + public long getLastAccessedTime() { + return this.lastAccessedTime; + } + + public long getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + public boolean isExpired() { + return this.expired; + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsWebEndpointExtension.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsWebEndpointExtension.java new file mode 100644 index 000000000000..fbed52985124 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsWebEndpointExtension.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2017 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.boot.actuate.session; + +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointExtension; +import org.springframework.boot.actuate.session.SessionsEndpoint.SessionsReport; + +/** + * {@link WebEndpointExtension} for the {@link SessionsEndpoint}. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +@WebEndpointExtension(endpoint = SessionsEndpoint.class) +public class SessionsWebEndpointExtension { + + private final SessionsEndpoint delegate; + + public SessionsWebEndpointExtension(SessionsEndpoint delegate) { + this.delegate = delegate; + } + + @ReadOperation + public WebEndpointResponse sessionsForUsername(String username) { + if (username == null) { + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + SessionsReport sessions = this.delegate.sessionsForUsername(username); + return new WebEndpointResponse<>(sessions); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/package-info.java new file mode 100644 index 000000000000..f5b7df1b8888 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Actuator support for Spring Session. + */ +package org.springframework.boot.actuate.session; diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java new file mode 100644 index 000000000000..67d16750b0a9 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2017 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.boot.actuate.session; + +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.springframework.boot.actuate.session.SessionsEndpoint.SessionDescriptor; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.MapSession; +import org.springframework.session.Session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link SessionsEndpoint}. + * + * @author Vedran Pavic + */ +public class SessionsEndpointTests { + + private static final Session session = new MapSession(); + + private final FindByIndexNameSessionRepository repository = mock( + FindByIndexNameSessionRepository.class); + + @SuppressWarnings("unchecked") + private final SessionsEndpoint endpoint = new SessionsEndpoint(this.repository); + + @Test + public void sessionsForUsername() { + given(this.repository.findByIndexNameAndIndexValue( + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "user")) + .willReturn(Collections.singletonMap(session.getId(), session)); + List result = this.endpoint.sessionsForUsername("user") + .getSessions(); + assertThat(result).hasSize(1); + assertThat(result.get(0).getId()).isEqualTo(session.getId()); + assertThat(result.get(0).getAttributeNames()) + .isEqualTo(session.getAttributeNames()); + assertThat(result.get(0).getCreationTime()) + .isEqualTo(session.getCreationTime().toEpochMilli()); + assertThat(result.get(0).getLastAccessedTime()) + .isEqualTo(session.getLastAccessedTime().toEpochMilli()); + assertThat(result.get(0).getMaxInactiveInterval()) + .isEqualTo(session.getMaxInactiveInterval().getSeconds()); + assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired()); + } + + @Test + public void getSession() { + given(this.repository.findById(session.getId())).willReturn(session); + SessionDescriptor result = this.endpoint.getSession(session.getId()); + assertThat(result.getId()).isEqualTo(session.getId()); + assertThat(result.getAttributeNames()).isEqualTo(session.getAttributeNames()); + assertThat(result.getCreationTime()) + .isEqualTo(session.getCreationTime().toEpochMilli()); + assertThat(result.getLastAccessedTime()) + .isEqualTo(session.getLastAccessedTime().toEpochMilli()); + assertThat(result.getMaxInactiveInterval()) + .isEqualTo(session.getMaxInactiveInterval().getSeconds()); + assertThat(result.isExpired()).isEqualTo(session.isExpired()); + } + + @Test + public void deleteSession() { + this.endpoint.deleteSession(session.getId()); + verify(this.repository).deleteById(session.getId()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java new file mode 100644 index 000000000000..382f8b07c26e --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2017 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.boot.actuate.session; + +import java.util.Collections; + +import net.minidev.json.JSONArray; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointRunners; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.MapSession; +import org.springframework.session.Session; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link SessionsEndpoint} exposed by Jersey, Spring MVC, and + * WebFlux. + * + * @author Vedran Pavic + */ +@RunWith(WebEndpointRunners.class) +public class SessionsEndpointWebIntegrationTests { + + private static final Session session = new MapSession(); + + private static final FindByIndexNameSessionRepository repository = mock( + FindByIndexNameSessionRepository.class); + + private static WebTestClient client; + + @Test + public void sessionsForUsernameWithoutUsernameParam() throws Exception { + client.get().uri((builder) -> builder.path("/application/sessions").build()) + .exchange().expectStatus().isBadRequest(); + } + + @Test + public void sessionsForUsernameNoResults() throws Exception { + given(repository.findByIndexNameAndIndexValue( + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "user")) + .willReturn(Collections.emptyMap()); + client.get() + .uri((builder) -> builder.path("/application/sessions") + .queryParam("username", "user").build()) + .exchange().expectStatus().isOk().expectBody().jsonPath("sessions") + .isEmpty(); + } + + @Test + public void sessionsForUsernameFound() throws Exception { + given(repository.findByIndexNameAndIndexValue( + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "user")) + .willReturn(Collections.singletonMap(session.getId(), session)); + client.get() + .uri((builder) -> builder.path("/application/sessions") + .queryParam("username", "user").build()) + .exchange().expectStatus().isOk().expectBody().jsonPath("sessions.[*].id") + .isEqualTo(new JSONArray().appendElement(session.getId())); + } + + @Configuration + protected static class TestConfiguration { + + @Bean + @SuppressWarnings("unchecked") + public SessionsEndpoint sessionsEndpoint() { + return new SessionsEndpoint(repository); + } + + @Bean + public SessionsWebEndpointExtension sessionsWebEndpointExtension( + SessionsEndpoint delegate) { + return new SessionsWebEndpointExtension(delegate); + } + + } + +} 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 8c0e997ac3f4..3f5b891b5379 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -1168,6 +1168,12 @@ content into your application; rather pick only the properties that you need. endpoints.prometheus.enabled= # Enable the metrics endpoint. endpoints.prometheus.web.enabled= # Expose the metrics endpoint as a Web endpoint. + # SESSIONS ENDPOINT ({sc-spring-boot-actuator}/session/SessionsEndpoint.{sc-ext}[SessionsEndpoint]) + endpoints.sessions.cache.time-to-live=0 # Maximum time in milliseconds that a response can be cached. + endpoints.sessions.enabled= # Enable the sessions endpoint. + endpoints.sessions.jmx.enabled= # Expose the sessions endpoint as a JMX MBean. + endpoints.sessions.web.enabled= # Expose the sessions endpoint as a Web endpoint. + # SHUTDOWN ENDPOINT ({sc-spring-boot-actuator}/context/ShutdownEndpoint.{sc-ext}[ShutdownEndpoint]) endpoints.shutdown.cache.time-to-live=0 # Maximum time in milliseconds that a response can be cached. endpoints.shutdown.enabled=false # Enable the shutdown endpoint. diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 01b3afaaabbe..88134617dbef 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -106,6 +106,10 @@ The following technology agnostic endpoints are available: |`mappings` |Displays a collated list of all `@RequestMapping` paths. +|`sessions` +|Allows retrieval and deletion of user's sessions from Spring Session backed session +store. + |`shutdown` |Allows the application to be gracefully shutdown (not enabled by default). diff --git a/spring-boot-samples/spring-boot-sample-session/pom.xml b/spring-boot-samples/spring-boot-sample-session/pom.xml index 70b62242f870..4793836a44f3 100644 --- a/spring-boot-samples/spring-boot-sample-session/pom.xml +++ b/spring-boot-samples/spring-boot-sample-session/pom.xml @@ -24,6 +24,14 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.boot diff --git a/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/HelloRestController.java b/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/HelloRestController.java index 81eab2ff43e2..0432f422a959 100644 --- a/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/HelloRestController.java +++ b/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/HelloRestController.java @@ -16,8 +16,6 @@ package sample.session; -import java.util.UUID; - import javax.servlet.http.HttpSession; import org.springframework.web.bind.annotation.GetMapping; @@ -28,12 +26,7 @@ public class HelloRestController { @GetMapping("/") String uid(HttpSession session) { - UUID uid = (UUID) session.getAttribute("uid"); - if (uid == null) { - uid = UUID.randomUUID(); - } - session.setAttribute("uid", uid); - return uid.toString(); + return session.getId(); } } diff --git a/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/SampleSessionApplication.java b/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/SampleSessionApplication.java index a79c96ea6b53..8a3a5bcc9e9c 100644 --- a/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/SampleSessionApplication.java +++ b/spring-boot-samples/spring-boot-sample-session/src/main/java/sample/session/SampleSessionApplication.java @@ -18,10 +18,22 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @SpringBootApplication public class SampleSessionApplication { + @Bean + public UserDetailsService userDetailsService() { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser( + User.withUsername("user").password("password").roles("USER").build()); + return manager; + } + public static void main(String[] args) throws Exception { SpringApplication.run(SampleSessionApplication.class); } diff --git a/spring-boot-samples/spring-boot-sample-session/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-session/src/main/resources/application.properties index 1c91264a7414..b975771025b6 100644 --- a/spring-boot-samples/spring-boot-sample-session/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-session/src/main/resources/application.properties @@ -1 +1 @@ -server.session.timeout=5 +endpoints.sessions.web.enabled=true diff --git a/spring-boot-samples/spring-boot-sample-session/src/test/java/sample/session/SampleSessionApplicationTests.java b/spring-boot-samples/spring-boot-sample-session/src/test/java/sample/session/SampleSessionApplicationTests.java index c842714c129d..abeb690a2d42 100644 --- a/spring-boot-samples/spring-boot-sample-session/src/test/java/sample/session/SampleSessionApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-session/src/test/java/sample/session/SampleSessionApplicationTests.java @@ -17,6 +17,7 @@ package sample.session; import java.net.URI; +import java.util.Base64; import org.junit.Test; @@ -35,6 +36,7 @@ * Tests for {@link SampleSessionApplication}. * * @author Andy Wilkinson + * @author Vedran Pavic */ public class SampleSessionApplicationTests { @@ -42,29 +44,33 @@ public class SampleSessionApplicationTests { public void sessionExpiry() throws Exception { ConfigurableApplicationContext context = new SpringApplicationBuilder() .sources(SampleSessionApplication.class) - .properties("server.port:0") - .initializers(new ServerPortInfoApplicationContextInitializer()) - .run(); + .properties("server.port:0", "server.session.timeout:1") + .initializers(new ServerPortInfoApplicationContextInitializer()).run(); String port = context.getEnvironment().getProperty("local.server.port"); URI uri = URI.create("http://localhost:" + port + "/"); RestTemplate restTemplate = new RestTemplate(); - ResponseEntity response = restTemplate.getForEntity(uri, String.class); - String uuid1 = response.getBody(); HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("Authorization", "Basic " + + Base64.getEncoder().encodeToString("user:password".getBytes())); + + ResponseEntity response = restTemplate.exchange( + new RequestEntity<>(requestHeaders, HttpMethod.GET, uri), String.class); + String sessionId1 = response.getBody(); + requestHeaders.clear(); requestHeaders.set("Cookie", response.getHeaders().getFirst("Set-Cookie")); RequestEntity request = new RequestEntity<>(requestHeaders, HttpMethod.GET, uri); - String uuid2 = restTemplate.exchange(request, String.class).getBody(); - assertThat(uuid1).isEqualTo(uuid2); + String sessionId2 = restTemplate.exchange(request, String.class).getBody(); + assertThat(sessionId1).isEqualTo(sessionId2); - Thread.sleep(5000); + Thread.sleep(1000); - String uuid3 = restTemplate.exchange(request, String.class).getBody(); - assertThat(uuid2).isNotEqualTo(uuid3); + String loginPage = restTemplate.exchange(request, String.class).getBody(); + assertThat(loginPage).containsIgnoringCase("login"); } }