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 extends Session> 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 extends Session> sessionRepository;
+
+ /**
+ * Create a new {@link SessionsEndpoint} instance.
+ * @param sessionRepository the session repository
+ */
+ public SessionsEndpoint(
+ FindByIndexNameSessionRepository extends Session> 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");
}
}