Skip to content

Commit b82bfd9

Browse files
committed
Add actuator endpoint for finding and deleting sessions
1 parent 49a62a7 commit b82bfd9

File tree

15 files changed

+716
-5
lines changed

15 files changed

+716
-5
lines changed

spring-boot-actuator-docs/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@
9898
<artifactId>spring-restdocs-mockmvc</artifactId>
9999
<scope>provided</scope>
100100
</dependency>
101+
<dependency>
102+
<groupId>org.springframework.session</groupId>
103+
<artifactId>spring-session</artifactId>
104+
<scope>provided</scope>
105+
</dependency>
101106
<dependency>
102107
<groupId>org.yaml</groupId>
103108
<artifactId>snakeyaml</artifactId>

spring-boot-actuator-docs/src/main/asciidoc/index.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,44 @@ include::{generated}/partial-logfile/http-response.adoc[]
7777

7878

7979

80+
=== /sessions
81+
This endpoint allows retrieving the sessions from Spring Session backed session store, and
82+
deleting them.
83+
Sessions can be retrieved by username, or by session id, and deleted by session id.
84+
85+
86+
==== Finding sessions for given username
87+
88+
Example cURL request:
89+
include::{generated}/sessions/curl-request.adoc[]
90+
91+
Example HTTP request:
92+
include::{generated}/sessions/http-request.adoc[]
93+
94+
Example HTTP response:
95+
include::{generated}/sessions/http-response.adoc[]
96+
97+
==== Getting a single session
98+
99+
Example cURL request:
100+
include::{generated}/sessions/get-session/curl-request.adoc[]
101+
102+
Example HTTP request:
103+
include::{generated}/sessions/get-session/http-request.adoc[]
104+
105+
Example HTTP response:
106+
include::{generated}/sessions/get-session/http-response.adoc[]
107+
108+
==== Deleting the session
109+
110+
Example cURL request:
111+
include::{generated}/sessions/delete-session/curl-request.adoc[]
112+
113+
Example HTTP request:
114+
include::{generated}/sessions/delete-session/http-request.adoc[]
115+
116+
117+
80118
=== /docs
81119
This endpoint (if available) contains HTML documentation for the other endpoints. Its path
82120
can be "/docs" (if there is an existing home page) or "/" (otherwise, including if the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.hypermedia;
18+
19+
import java.util.Collections;
20+
import java.util.Map;
21+
import java.util.Set;
22+
23+
import org.springframework.session.ExpiringSession;
24+
import org.springframework.session.FindByIndexNameSessionRepository;
25+
26+
/**
27+
* {@link FindByIndexNameSessionRepository} implementation for documentation purposes.
28+
*
29+
* @author Vedran Pavic
30+
*/
31+
public class DocumentationSessionRepository
32+
implements FindByIndexNameSessionRepository<ExpiringSession> {
33+
34+
static final String MOCK_SESSION_ID = "6993c49c-6d29-4117-9242-802b8185470e";
35+
36+
static final String MOCK_USER = "user";
37+
38+
private static final Map<String, ExpiringSession> sessions = Collections
39+
.singletonMap(MOCK_SESSION_ID, new DocumentationSession());
40+
41+
@Override
42+
public ExpiringSession createSession() {
43+
return null;
44+
}
45+
46+
@Override
47+
public void save(ExpiringSession expiringSession) {
48+
49+
}
50+
51+
@Override
52+
public ExpiringSession getSession(String id) {
53+
return sessions.get(id);
54+
}
55+
56+
@Override
57+
public void delete(String id) {
58+
59+
}
60+
61+
@Override
62+
public Map<String, ExpiringSession> findByIndexNameAndIndexValue(String indexName,
63+
String indexValue) {
64+
if (PRINCIPAL_NAME_INDEX_NAME.equals(indexName) && MOCK_USER.equals(indexValue)) {
65+
return sessions;
66+
}
67+
return Collections.emptyMap();
68+
}
69+
70+
private static class DocumentationSession implements ExpiringSession {
71+
72+
@Override
73+
public long getCreationTime() {
74+
return 1487617610119L;
75+
}
76+
77+
@Override
78+
public long getLastAccessedTime() {
79+
return 1487617610119L;
80+
}
81+
82+
@Override
83+
public void setLastAccessedTime(long lastAccessedTime) {
84+
85+
}
86+
87+
@Override
88+
public int getMaxInactiveIntervalInSeconds() {
89+
return 1800;
90+
}
91+
92+
@Override
93+
public void setMaxInactiveIntervalInSeconds(int interval) {
94+
95+
}
96+
97+
@Override
98+
public boolean isExpired() {
99+
return false;
100+
}
101+
102+
@Override
103+
public String getId() {
104+
return MOCK_SESSION_ID;
105+
}
106+
107+
@Override
108+
public <T> T getAttribute(String attributeName) {
109+
return null;
110+
}
111+
112+
@Override
113+
public Set<String> getAttributeNames() {
114+
return Collections.singleton("principal");
115+
}
116+
117+
@Override
118+
public void setAttribute(String attributeName, Object attributeValue) {
119+
120+
}
121+
122+
@Override
123+
public void removeAttribute(String attributeName) {
124+
125+
}
126+
127+
}
128+
129+
}

spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.springframework.util.StringUtils;
5959

6060
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
61+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
6162
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6263
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
6364
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -67,7 +68,7 @@
6768
@WebAppConfiguration
6869
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
6970
"endpoints.health.sensitive=true", "endpoints.actuator.enabled=false",
70-
"management.security.enabled=false" })
71+
"endpoints.sessions.enabled=true", "management.security.enabled=false" })
7172
@DirtiesContext
7273
@AutoConfigureRestDocs(EndpointDocumentation.RESTDOCS_OUTPUT_DIR)
7374
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
@@ -78,8 +79,8 @@ public class EndpointDocumentation {
7879
static final File LOG_FILE = new File("target/logs/spring.log");
7980

8081
private static final Set<String> SKIPPED = Collections
81-
.<String>unmodifiableSet(new HashSet<>(
82-
Arrays.asList("/docs", "/logfile", "/heapdump", "/auditevents")));
82+
.<String>unmodifiableSet(new HashSet<>(Arrays.asList("/docs", "/logfile",
83+
"/heapdump", "/auditevents", "/sessions")));
8384

8485
@Autowired
8586
private MvcEndpoints mvcEndpoints;
@@ -159,6 +160,34 @@ public void auditEventsByPrincipalAndType() throws Exception {
159160
.andDo(document("auditevents/filter-by-principal-and-type"));
160161
}
161162

163+
@Test
164+
public void findSessionsByUsername() throws Exception {
165+
this.mockMvc
166+
.perform(get("/application/sessions")
167+
.param("username", DocumentationSessionRepository.MOCK_USER)
168+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
169+
.andExpect(status().isOk()).andDo(document("sessions"));
170+
}
171+
172+
@Test
173+
public void getSession() throws Exception {
174+
this.mockMvc
175+
.perform(get("/application/sessions/"
176+
+ DocumentationSessionRepository.MOCK_SESSION_ID)
177+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
178+
.andExpect(status().isOk()).andDo(document("sessions/get-session"));
179+
}
180+
181+
@Test
182+
public void deleteSession() throws Exception {
183+
this.mockMvc
184+
.perform(delete("/application/sessions/"
185+
+ DocumentationSessionRepository.MOCK_SESSION_ID)
186+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
187+
.andExpect(status().isNoContent())
188+
.andDo(document("sessions/delete-session"));
189+
}
190+
162191
@Test
163192
public void endpoints() throws Exception {
164193
final File docs = new File("src/main/asciidoc");

spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public EnvironmentEndpoint environmentEndpoint() {
5353
return new LimitedEnvironmentEndpoint();
5454
}
5555

56+
@Bean
57+
public DocumentationSessionRepository sessionRepository() {
58+
return new DocumentationSessionRepository();
59+
}
60+
5661
public static void main(String[] args) {
5762
SpringApplication.run(SpringBootHypermediaApplication.class, args);
5863
}

spring-boot-actuator/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@
226226
<artifactId>spring-security-config</artifactId>
227227
<optional>true</optional>
228228
</dependency>
229+
<dependency>
230+
<groupId>org.springframework.session</groupId>
231+
<artifactId>spring-session</artifactId>
232+
<optional>true</optional>
233+
</dependency>
229234
<dependency>
230235
<groupId>org.apache.tomcat.embed</groupId>
231236
<artifactId>tomcat-embed-core</artifactId>

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
import org.springframework.boot.actuate.endpoint.Endpoint;
2828
import org.springframework.boot.actuate.endpoint.jmx.AuditEventsJmxEndpoint;
2929
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter;
30+
import org.springframework.boot.actuate.endpoint.jmx.SessionsJmxEndpoint;
3031
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
3132
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3233
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
3334
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
3435
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3536
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
37+
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
3638
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
3739
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
3840
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -41,6 +43,8 @@
4143
import org.springframework.context.annotation.Conditional;
4244
import org.springframework.context.annotation.Configuration;
4345
import org.springframework.core.type.AnnotatedTypeMetadata;
46+
import org.springframework.session.ExpiringSession;
47+
import org.springframework.session.FindByIndexNameSessionRepository;
4448
import org.springframework.util.StringUtils;
4549

4650
/**
@@ -50,6 +54,7 @@
5054
* @author Christian Dupuis
5155
* @author Andy Wilkinson
5256
* @author Madhura Bhave
57+
* @author Vedran Pavic
5358
*/
5459
@Configuration
5560
@Conditional(JmxEnabledCondition.class)
@@ -95,6 +100,21 @@ public AuditEventsJmxEndpoint auditEventsEndpoint(
95100
return new AuditEventsJmxEndpoint(this.objectMapper, auditEventRepository);
96101
}
97102

103+
@Configuration
104+
@ConditionalOnSingleCandidate(FindByIndexNameSessionRepository.class)
105+
@ConditionalOnEnabledEndpoint(value = "sessions", enabledByDefault = false)
106+
protected class SessionsJmxEndpointConfiguration {
107+
108+
@Bean
109+
public SessionsJmxEndpoint sessionsEndpoint(
110+
FindByIndexNameSessionRepository<? extends ExpiringSession> sessionRepository) {
111+
return new SessionsJmxEndpoint(
112+
EndpointMBeanExportAutoConfiguration.this.objectMapper,
113+
sessionRepository);
114+
}
115+
116+
}
117+
98118
/**
99119
* Condition to check that spring.jmx and endpoints.jmx are enabled.
100120
*/

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,23 @@
4141
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
4242
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpointSecurityInterceptor;
4343
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
44+
import org.springframework.boot.actuate.endpoint.mvc.SessionsMvcEndpoint;
4445
import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint;
4546
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
4647
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
4748
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
4849
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
50+
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
4951
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
5052
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5153
import org.springframework.context.annotation.Bean;
5254
import org.springframework.context.annotation.ConditionContext;
5355
import org.springframework.context.annotation.Conditional;
56+
import org.springframework.context.annotation.Configuration;
5457
import org.springframework.core.env.Environment;
5558
import org.springframework.core.type.AnnotatedTypeMetadata;
59+
import org.springframework.session.ExpiringSession;
60+
import org.springframework.session.FindByIndexNameSessionRepository;
5661
import org.springframework.util.CollectionUtils;
5762
import org.springframework.util.StringUtils;
5863
import org.springframework.web.cors.CorsConfiguration;
@@ -208,6 +213,19 @@ public AuditEventsMvcEndpoint auditEventMvcEndpoint(
208213
return new AuditEventsMvcEndpoint(auditEventRepository);
209214
}
210215

216+
@Configuration
217+
@ConditionalOnSingleCandidate(FindByIndexNameSessionRepository.class)
218+
@ConditionalOnEnabledEndpoint(value = "sessions", enabledByDefault = false)
219+
protected static class SessionsMvcEndpointConfiguration {
220+
221+
@Bean
222+
public SessionsMvcEndpoint sessionsMvcEndpoint(
223+
FindByIndexNameSessionRepository<? extends ExpiringSession> sessionRepository) {
224+
return new SessionsMvcEndpoint(sessionRepository);
225+
}
226+
227+
}
228+
211229
private static class LogFileCondition extends SpringBootCondition {
212230

213231
@Override

0 commit comments

Comments
 (0)