Skip to content

Commit 15cc44e

Browse files
committed
Add WebSession.invalidate()
Issue: SPR-15960
1 parent 6da3518 commit 15cc44e

File tree

6 files changed

+77
-27
lines changed

6 files changed

+77
-27
lines changed

spring-web/src/main/java/org/springframework/web/server/WebSession.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ default <T> T getAttributeOrDefault(String name, T defaultValue) {
109109
*/
110110
Mono<Void> changeSessionId();
111111

112+
/**
113+
* Invalidate the current session and clear session storage.
114+
* @return completion notification (success or error)
115+
*/
116+
Mono<Void> invalidate();
117+
112118
/**
113119
* Save the session persisting attributes (e.g. if stored remotely) and also
114120
* sending the session id to the client if the session is new.

spring-web/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,21 @@ private Mono<WebSession> retrieveSession(ServerWebExchange exchange) {
9090
}
9191

9292
private Mono<Void> save(ServerWebExchange exchange, WebSession session) {
93-
if (session.isExpired()) {
94-
return Mono.error(new IllegalStateException("Session='" + session.getId() + "' expired."));
95-
}
9693

97-
if (!session.isStarted()) {
98-
if (hasNewSessionId(exchange, session)) {
94+
List<String> ids = getSessionIdResolver().resolveSessionIds(exchange);
95+
96+
if (!session.isStarted() || session.isExpired()) {
97+
if (!ids.isEmpty()) {
98+
// Expired on retrieve or while processing request, or invalidated..
9999
this.sessionIdResolver.expireSession(exchange);
100100
}
101101
return Mono.empty();
102102
}
103103

104-
if (hasNewSessionId(exchange, session)) {
104+
if (ids.isEmpty() || !session.getId().equals(ids.get(0))) {
105105
this.sessionIdResolver.setSessionId(exchange, session.getId());
106106
}
107107

108108
return session.save();
109109
}
110-
111-
private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) {
112-
List<String> ids = getSessionIdResolver().resolveSessionIds(exchange);
113-
return ids.isEmpty() || !session.getId().equals(ids.get(0));
114-
}
115110
}

spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,25 +109,22 @@ public Mono<WebSession> updateLastAccessTime(WebSession webSession) {
109109

110110
private class InMemoryWebSession implements WebSession {
111111

112-
private final AtomicReference<String> id;
112+
private final AtomicReference<String> id = new AtomicReference<>(String.valueOf(idGenerator.generateId()));
113113

114-
private final Map<String, Object> attributes;
114+
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
115115

116116
private final Instant creationTime;
117117

118118
private volatile Instant lastAccessTime;
119119

120-
private volatile Duration maxIdleTime;
120+
private volatile Duration maxIdleTime = Duration.ofMinutes(30);
121121

122-
private volatile boolean started;
122+
private final AtomicReference<State> state = new AtomicReference<>(State.NEW);
123123

124124

125125
InMemoryWebSession() {
126-
this.id = new AtomicReference<>(String.valueOf(idGenerator.generateId()));
127-
this.attributes = new ConcurrentHashMap<>();
128126
this.creationTime = Instant.now(getClock());
129127
this.lastAccessTime = this.creationTime;
130-
this.maxIdleTime = Duration.ofMinutes(30);
131128
}
132129

133130

@@ -163,12 +160,12 @@ public Duration getMaxIdleTime() {
163160

164161
@Override
165162
public void start() {
166-
this.started = true;
163+
this.state.compareAndSet(State.NEW, State.STARTED);
167164
}
168165

169166
@Override
170167
public boolean isStarted() {
171-
return this.started || !getAttributes().isEmpty();
168+
return this.state.get().equals(State.STARTED) || !getAttributes().isEmpty();
172169
}
173170

174171
@Override
@@ -185,21 +182,46 @@ public Mono<Void> changeSessionId() {
185182
return Mono.empty();
186183
}
187184

185+
@Override
186+
public Mono<Void> invalidate() {
187+
this.state.set(State.EXPIRED);
188+
getAttributes().clear();
189+
InMemoryWebSessionStore.this.sessions.remove(this.id.get());
190+
return Mono.empty();
191+
}
192+
188193
@Override
189194
public Mono<Void> save() {
195+
if (!getAttributes().isEmpty()) {
196+
this.state.compareAndSet(State.NEW, State.STARTED);
197+
}
190198
InMemoryWebSessionStore.this.sessions.put(this.getId(), this);
191199
return Mono.empty();
192200
}
193201

194202
@Override
195203
public boolean isExpired() {
196-
return (isStarted() && !this.maxIdleTime.isNegative() &&
197-
Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime));
204+
if (this.state.get().equals(State.EXPIRED)) {
205+
return true;
206+
}
207+
if (checkExpired()) {
208+
this.state.set(State.EXPIRED);
209+
return true;
210+
}
211+
return false;
212+
}
213+
214+
private boolean checkExpired() {
215+
return isStarted() && !this.maxIdleTime.isNegative() &&
216+
Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime);
198217
}
199218

200219
private void updateLastAccessTime() {
201220
this.lastAccessTime = Instant.now(getClock());
202221
}
222+
203223
}
204224

225+
private enum State { NEW, STARTED, EXPIRED }
226+
205227
}

spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ public class DefaultWebSessionManagerTests {
7070
@Before
7171
public void setUp() throws Exception {
7272
when(this.store.createWebSession()).thenReturn(Mono.just(this.createSession));
73-
when(this.store.updateLastAccessTime(any())).thenReturn(Mono.just(this.updateSession));
7473
when(this.createSession.save()).thenReturn(Mono.empty());
7574
when(this.updateSession.getId()).thenReturn("update-session-id");
7675

spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ public void lastAccessTimeIsUpdatedOnRetrieve() throws Exception {
8989
Instant time2 = session2.getLastAccessTime();
9090
assertTrue(time1.isBefore(time2));
9191
}
92+
93+
@Test
94+
public void invalidate() throws Exception {
95+
96+
}
9297
}

spring-web/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.net.URISyntaxException;
2121
import java.time.Clock;
2222
import java.time.Duration;
23-
import java.time.Instant;
2423
import java.util.List;
2524
import java.util.concurrent.atomic.AtomicInteger;
2625

@@ -136,14 +135,13 @@ public void expiredSessionEnds() throws Exception {
136135
assertEquals(HttpStatus.OK, response.getStatusCode());
137136
String id = extractSessionId(response.getHeaders());
138137
assertNotNull(id);
139-
assertEquals(1, this.handler.getSessionRequestCount());
140138

141139
// Now fast-forward by 31 minutes
142140
InMemoryWebSessionStore store = (InMemoryWebSessionStore) this.sessionManager.getSessionStore();
143141
store.setClock(Clock.offset(store.getClock(), Duration.ofMinutes(31)));
144142

145143
// Second request: session expires
146-
URI uri = new URI("http://localhost:" + this.port + "/?expiredSession");
144+
URI uri = new URI("http://localhost:" + this.port + "/?expire");
147145
request = RequestEntity.get(uri).header("Cookie", "SESSION=" + id).build();
148146
response = this.restTemplate.exchange(request, Void.class);
149147

@@ -177,6 +175,28 @@ public void changeSessionId() throws Exception {
177175
assertEquals(2, this.handler.getSessionRequestCount());
178176
}
179177

178+
@Test
179+
public void invalidate() throws Exception {
180+
181+
// First request: no session yet, new session created
182+
RequestEntity<Void> request = RequestEntity.get(createUri()).build();
183+
ResponseEntity<Void> response = this.restTemplate.exchange(request, Void.class);
184+
185+
assertEquals(HttpStatus.OK, response.getStatusCode());
186+
String id = extractSessionId(response.getHeaders());
187+
assertNotNull(id);
188+
189+
// Second request: invalidates session
190+
URI uri = new URI("http://localhost:" + this.port + "/?invalidate");
191+
request = RequestEntity.get(uri).header("Cookie", "SESSION=" + id).build();
192+
response = this.restTemplate.exchange(request, Void.class);
193+
194+
assertEquals(HttpStatus.OK, response.getStatusCode());
195+
String value = response.getHeaders().getFirst("Set-Cookie");
196+
assertNotNull(value);
197+
assertTrue("Actual value: " + value, value.contains("Max-Age=0"));
198+
}
199+
180200
private String extractSessionId(HttpHeaders headers) {
181201
List<String> headerValues = headers.get("Set-Cookie");
182202
assertNotNull(headerValues);
@@ -206,7 +226,7 @@ public int getSessionRequestCount() {
206226

207227
@Override
208228
public Mono<Void> handle(ServerWebExchange exchange) {
209-
if (exchange.getRequest().getQueryParams().containsKey("expiredSession")) {
229+
if (exchange.getRequest().getQueryParams().containsKey("expire")) {
210230
return exchange.getSession().doOnNext(session -> {
211231
// Don't do anything, leave it expired...
212232
}).then();
@@ -215,6 +235,9 @@ else if (exchange.getRequest().getQueryParams().containsKey("changeId")) {
215235
return exchange.getSession().flatMap(session ->
216236
session.changeSessionId().doOnSuccess(aVoid -> updateSessionAttribute(session)));
217237
}
238+
else if (exchange.getRequest().getQueryParams().containsKey("invalidate")) {
239+
return exchange.getSession().doOnNext(WebSession::invalidate).then();
240+
}
218241
else {
219242
return exchange.getSession().doOnSuccess(this::updateSessionAttribute).then();
220243
}

0 commit comments

Comments
 (0)