Skip to content

Commit e2ee23b

Browse files
committed
WebSession supports changeSessionId
Issue: SPR-15571
1 parent 70252a7 commit e2ee23b

File tree

7 files changed

+152
-55
lines changed

7 files changed

+152
-55
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -101,6 +101,14 @@ default <T> T getAttributeOrDefault(String name, T defaultValue) {
101101
*/
102102
boolean isStarted();
103103

104+
/**
105+
* Generate a new id for the session and update the underlying session
106+
* storage to reflect the new id. After a successful call {@link #getId()}
107+
* reflects the new session id.
108+
* @return completion notification (success or error)
109+
*/
110+
Mono<Void> changeSessionId();
111+
104112
/**
105113
* Save the session persisting attributes (e.g. if stored remotely) and also
106114
* sending the session id to the client if the session is new.

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
import java.util.Map;
2222
import java.util.concurrent.ConcurrentHashMap;
2323
import java.util.concurrent.atomic.AtomicReference;
24+
import java.util.function.BiFunction;
2425
import java.util.function.Function;
2526

2627
import reactor.core.publisher.Mono;
2728

2829
import org.springframework.util.Assert;
30+
import org.springframework.util.IdGenerator;
2931
import org.springframework.web.server.WebSession;
3032

3133
/**
@@ -36,12 +38,16 @@
3638
*/
3739
class DefaultWebSession implements WebSession {
3840

39-
private final String id;
41+
private final AtomicReference<String> id;
42+
43+
private final IdGenerator idGenerator;
4044

4145
private final Map<String, Object> attributes;
4246

4347
private final Clock clock;
4448

49+
private final BiFunction<String, WebSession, Mono<Void>> changeIdOperation;
50+
4551
private final Function<WebSession, Mono<Void>> saveOperation;
4652

4753
private final Instant creationTime;
@@ -55,14 +61,22 @@ class DefaultWebSession implements WebSession {
5561

5662
/**
5763
* Constructor for creating a brand, new session.
58-
* @param id the session id
64+
* @param idGenerator the session id generator
5965
* @param clock for access to current time
6066
*/
61-
DefaultWebSession(String id, Clock clock, Function<WebSession, Mono<Void>> saveOperation) {
62-
Assert.notNull(id, "'id' is required.");
67+
DefaultWebSession(IdGenerator idGenerator, Clock clock,
68+
BiFunction<String, WebSession, Mono<Void>> changeIdOperation,
69+
Function<WebSession, Mono<Void>> saveOperation) {
70+
71+
Assert.notNull(idGenerator, "'idGenerator' is required.");
6372
Assert.notNull(clock, "'clock' is required.");
64-
this.id = id;
73+
Assert.notNull(changeIdOperation, "'changeIdOperation' is required.");
74+
Assert.notNull(saveOperation, "'saveOperation' is required.");
75+
76+
this.id = new AtomicReference<>(String.valueOf(idGenerator.generateId()));
77+
this.idGenerator = idGenerator;
6578
this.clock = clock;
79+
this.changeIdOperation = changeIdOperation;
6680
this.saveOperation = saveOperation;
6781
this.attributes = new ConcurrentHashMap<>();
6882
this.creationTime = Instant.now(clock);
@@ -81,12 +95,14 @@ class DefaultWebSession implements WebSession {
8195
Function<WebSession, Mono<Void>> saveOperation) {
8296

8397
this.id = existingSession.id;
98+
this.idGenerator = existingSession.idGenerator;
8499
this.attributes = existingSession.attributes;
85100
this.clock = existingSession.clock;
101+
this.changeIdOperation = existingSession.changeIdOperation;
102+
this.saveOperation = saveOperation;
86103
this.creationTime = existingSession.creationTime;
87104
this.lastAccessTime = lastAccessTime;
88105
this.maxIdleTime = existingSession.maxIdleTime;
89-
this.saveOperation = saveOperation;
90106
this.state = existingSession.state;
91107
}
92108

@@ -95,19 +111,21 @@ class DefaultWebSession implements WebSession {
95111
*/
96112
DefaultWebSession(DefaultWebSession existingSession, Instant lastAccessTime) {
97113
this.id = existingSession.id;
114+
this.idGenerator = existingSession.idGenerator;
98115
this.attributes = existingSession.attributes;
99116
this.clock = existingSession.clock;
117+
this.changeIdOperation = existingSession.changeIdOperation;
118+
this.saveOperation = existingSession.saveOperation;
100119
this.creationTime = existingSession.creationTime;
101120
this.lastAccessTime = lastAccessTime;
102121
this.maxIdleTime = existingSession.maxIdleTime;
103-
this.saveOperation = existingSession.saveOperation;
104122
this.state = existingSession.state;
105123
}
106124

107125

108126
@Override
109127
public String getId() {
110-
return this.id;
128+
return this.id.get();
111129
}
112130

113131
@Override
@@ -151,6 +169,14 @@ public boolean isStarted() {
151169
return (State.STARTED.equals(value) || (State.NEW.equals(value) && !getAttributes().isEmpty()));
152170
}
153171

172+
@Override
173+
public Mono<Void> changeSessionId() {
174+
String oldId = this.id.get();
175+
String newId = String.valueOf(this.idGenerator.generateId());
176+
this.id.set(newId);
177+
return this.changeIdOperation.apply(oldId, this).doOnError(ex -> this.id.set(oldId));
178+
}
179+
154180
@Override
155181
public Mono<Void> save() {
156182
return this.saveOperation.apply(this);

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,12 +19,13 @@
1919
import java.time.Instant;
2020
import java.time.ZoneId;
2121
import java.util.List;
22-
import java.util.UUID;
2322

2423
import reactor.core.publisher.Flux;
2524
import reactor.core.publisher.Mono;
2625

2726
import org.springframework.util.Assert;
27+
import org.springframework.util.IdGenerator;
28+
import org.springframework.util.JdkIdGenerator;
2829
import org.springframework.web.server.ServerWebExchange;
2930
import org.springframework.web.server.WebSession;
3031

@@ -39,6 +40,9 @@
3940
*/
4041
public class DefaultWebSessionManager implements WebSessionManager {
4142

43+
private static final IdGenerator idGenerator = new JdkIdGenerator();
44+
45+
4246
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
4347

4448
private WebSessionStore sessionStore = new InMemoryWebSessionStore();
@@ -159,10 +163,10 @@ private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session)
159163
}
160164

161165
private Mono<DefaultWebSession> createSession(ServerWebExchange exchange) {
162-
return Mono.fromSupplier(() -> {
163-
String id = UUID.randomUUID().toString();
164-
return new DefaultWebSession(id, getClock(), sess -> saveSession(exchange, sess));
165-
});
166+
return Mono.fromSupplier(() ->
167+
new DefaultWebSession(idGenerator, getClock(),
168+
(oldId, session) -> this.sessionStore.changeSessionId(oldId, session),
169+
session -> saveSession(exchange, session)));
166170
}
167171

168172
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,6 +44,13 @@ public Mono<WebSession> retrieveSession(String id) {
4444
return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty());
4545
}
4646

47+
@Override
48+
public Mono<Void> changeSessionId(String oldId, WebSession session) {
49+
this.sessions.remove(oldId);
50+
this.sessions.put(session.getId(), session);
51+
return Mono.empty();
52+
}
53+
4754
@Override
4855
public Mono<Void> removeSession(String id) {
4956
this.sessions.remove(id);

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ public interface WebSessionStore {
4141
*/
4242
Mono<WebSession> retrieveSession(String sessionId);
4343

44+
/**
45+
* Update WebSession data storage to reflect a change in session id.
46+
* <p>Note that the same can be achieved via a combination of
47+
* {@link #removeSession} + {@link #storeSession}. The purpose of this method
48+
* is to allow a more efficient replacement of the session id mapping
49+
* without replacing and storing the session with all of its data.
50+
* @param oldId the previous session id
51+
* @param session the session reflecting the changed session id
52+
* @return completion notification (success or error)
53+
*/
54+
Mono<Void> changeSessionId(String oldId, WebSession session);
55+
4456
/**
4557
* Remove the WebSession for the specified id.
4658
* @param sessionId the id of the session to remove

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import java.time.Clock;
1919
import java.time.Duration;
2020
import java.time.Instant;
21+
import java.time.ZoneId;
2122
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.Collections;
@@ -31,6 +32,8 @@
3132
import org.springframework.lang.Nullable;
3233
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
3334
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
35+
import org.springframework.util.IdGenerator;
36+
import org.springframework.util.JdkIdGenerator;
3437
import org.springframework.web.server.ServerWebExchange;
3538
import org.springframework.web.server.WebSession;
3639
import org.springframework.web.server.adapter.DefaultServerWebExchange;
@@ -44,10 +47,16 @@
4447
import static org.junit.Assert.assertSame;
4548

4649
/**
50+
* Unit tests for {@link DefaultWebSessionManager}.
4751
* @author Rossen Stoyanchev
4852
*/
4953
public class DefaultWebSessionManagerTests {
5054

55+
private static final Clock CLOCK = Clock.system(ZoneId.of("GMT"));
56+
57+
private static final IdGenerator idGenerator = new JdkIdGenerator();
58+
59+
5160
private DefaultWebSessionManager manager;
5261

5362
private TestWebSessionIdResolver idResolver;
@@ -105,9 +114,10 @@ public void startSessionImplicitly() throws Exception {
105114

106115
@Test
107116
public void existingSession() throws Exception {
108-
DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone(), s -> Mono.empty());
117+
DefaultWebSession existing = createDefaultWebSession();
118+
String id = existing.getId();
109119
this.manager.getSessionStore().storeSession(existing);
110-
this.idResolver.setIdsToResolve(Collections.singletonList("1"));
120+
this.idResolver.setIdsToResolve(Collections.singletonList(id));
111121

112122
WebSession actual = this.manager.getSession(this.exchange).block();
113123
assertNotNull(actual);
@@ -116,10 +126,9 @@ public void existingSession() throws Exception {
116126

117127
@Test
118128
public void existingSessionIsExpired() throws Exception {
119-
Clock clock = Clock.systemDefaultZone();
120-
DefaultWebSession existing = new DefaultWebSession("1", clock, s -> Mono.empty());
129+
DefaultWebSession existing = createDefaultWebSession();
121130
existing.start();
122-
Instant lastAccessTime = Instant.now(clock).minus(Duration.ofMinutes(31));
131+
Instant lastAccessTime = Instant.now(CLOCK).minus(Duration.ofMinutes(31));
123132
existing = new DefaultWebSession(existing, lastAccessTime, s -> Mono.empty());
124133
this.manager.getSessionStore().storeSession(existing);
125134
this.idResolver.setIdsToResolve(Collections.singletonList("1"));
@@ -129,16 +138,21 @@ public void existingSessionIsExpired() throws Exception {
129138
}
130139

131140
@Test
132-
public void multipleSessions() throws Exception {
133-
DefaultWebSession existing = new DefaultWebSession("3", Clock.systemDefaultZone(), s -> Mono.empty());
141+
public void multipleSessionIds() throws Exception {
142+
DefaultWebSession existing = createDefaultWebSession();
143+
String id = existing.getId();
134144
this.manager.getSessionStore().storeSession(existing);
135-
this.idResolver.setIdsToResolve(Arrays.asList("1", "2", "3"));
145+
this.idResolver.setIdsToResolve(Arrays.asList("neither-this", "nor-that", id));
136146

137147
WebSession actual = this.manager.getSession(this.exchange).block();
138148
assertNotNull(actual);
139149
assertEquals(existing.getId(), actual.getId());
140150
}
141151

152+
private DefaultWebSession createDefaultWebSession() {
153+
return new DefaultWebSession(idGenerator, CLOCK, (s, session) -> Mono.empty(), s -> Mono.empty());
154+
}
155+
142156

143157
private static class TestWebSessionIdResolver implements WebSessionIdResolver {
144158

0 commit comments

Comments
 (0)