Skip to content

Commit a4e393e

Browse files
Introduce SessionIdGenerationStrategy
Closes gh-11
1 parent 2e12753 commit a4e393e

File tree

39 files changed

+1074
-30
lines changed

39 files changed

+1074
-30
lines changed

spring-session-core/src/main/java/org/springframework/session/MapSession.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,27 @@ public final class MapSession implements Session, Serializable {
7474
*/
7575
private Duration maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL;
7676

77+
private transient SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy
78+
.getInstance();
79+
7780
/**
7881
* Creates a new instance with a secure randomly generated identifier.
7982
*/
8083
public MapSession() {
8184
this(generateId());
8285
}
8386

87+
/**
88+
* Creates a new instance using the specified {@link SessionIdGenerationStrategy} to
89+
* generate the session id.
90+
* @param sessionIdGenerationStrategy the {@link SessionIdGenerationStrategy} to use.
91+
* @since 3.2
92+
*/
93+
public MapSession(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
94+
this(sessionIdGenerationStrategy.generate());
95+
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
96+
}
97+
8498
/**
8599
* Creates a new instance with the specified id. This is preferred to the default
86100
* constructor when the id is known to prevent unnecessary consumption on entropy
@@ -141,7 +155,7 @@ public String getOriginalId() {
141155

142156
@Override
143157
public String changeSessionId() {
144-
String changedId = generateId();
158+
String changedId = this.sessionIdGenerationStrategy.generate();
145159
setId(changedId);
146160
return changedId;
147161
}
@@ -232,6 +246,16 @@ private static String generateId() {
232246
return UUID.randomUUID().toString();
233247
}
234248

249+
/**
250+
* Sets the {@link SessionIdGenerationStrategy} to use when generating a new session
251+
* id.
252+
* @param sessionIdGenerationStrategy the {@link SessionIdGenerationStrategy} to use.
253+
* @since 3.2
254+
*/
255+
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
256+
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
257+
}
258+
235259
private static final long serialVersionUID = 7160779239673823561L;
236260

237261
}

spring-session-core/src/main/java/org/springframework/session/MapSessionRepository.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public class MapSessionRepository implements SessionRepository<MapSession> {
4343

4444
private final Map<String, Session> sessions;
4545

46+
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
47+
4648
/**
4749
* Creates a new instance backed by the provided {@link java.util.Map}. This allows
4850
* injecting a distributed {@link java.util.Map}.
@@ -71,7 +73,9 @@ public void save(MapSession session) {
7173
if (!session.getId().equals(session.getOriginalId())) {
7274
this.sessions.remove(session.getOriginalId());
7375
}
74-
this.sessions.put(session.getId(), new MapSession(session));
76+
MapSession saved = new MapSession(session);
77+
saved.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy);
78+
this.sessions.put(session.getId(), saved);
7579
}
7680

7781
@Override
@@ -84,7 +88,9 @@ public MapSession findById(String id) {
8488
deleteById(saved.getId());
8589
return null;
8690
}
87-
return new MapSession(saved);
91+
MapSession result = new MapSession(saved);
92+
result.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy);
93+
return result;
8894
}
8995

9096
@Override
@@ -94,9 +100,14 @@ public void deleteById(String id) {
94100

95101
@Override
96102
public MapSession createSession() {
97-
MapSession result = new MapSession();
103+
MapSession result = new MapSession(this.sessionIdGenerationStrategy);
98104
result.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
99105
return result;
100106
}
101107

108+
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
109+
Assert.notNull(sessionIdGenerationStrategy, "sessionIdGenerationStrategy cannot be null");
110+
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
111+
}
112+
102113
}

spring-session-core/src/main/java/org/springframework/session/ReactiveMapSessionRepository.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public class ReactiveMapSessionRepository implements ReactiveSessionRepository<M
4545

4646
private final Map<String, Session> sessions;
4747

48+
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
49+
4850
/**
4951
* Creates a new instance backed by the provided {@link Map}. This allows injecting a
5052
* distributed {@link Map}.
@@ -84,6 +86,7 @@ public Mono<MapSession> findById(String id) {
8486
return Mono.defer(() -> Mono.justOrEmpty(this.sessions.get(id))
8587
.filter((session) -> !session.isExpired())
8688
.map(MapSession::new)
89+
.doOnNext((session) -> session.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy))
8790
.switchIfEmpty(deleteById(id).then(Mono.empty())));
8891
// @formatter:on
8992
}
@@ -96,10 +99,21 @@ public Mono<Void> deleteById(String id) {
9699
@Override
97100
public Mono<MapSession> createSession() {
98101
return Mono.defer(() -> {
99-
MapSession result = new MapSession();
102+
MapSession result = new MapSession(this.sessionIdGenerationStrategy);
100103
result.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
101104
return Mono.just(result);
102105
});
103106
}
104107

108+
/**
109+
* Sets the {@link SessionIdGenerationStrategy} to use.
110+
* @param sessionIdGenerationStrategy the non-null {@link SessionIdGenerationStrategy}
111+
* to use
112+
* @since 3.2
113+
*/
114+
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
115+
Assert.notNull(sessionIdGenerationStrategy, "sessionIdGenerationStrategy cannot be null");
116+
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
117+
}
118+
105119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2014-2023 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+
* https://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.session;
18+
19+
import org.springframework.lang.NonNull;
20+
21+
/**
22+
* An interface for specifying a strategy for generating session identifiers.
23+
*
24+
* @author Marcus da Coregio
25+
* @since 3.2
26+
*/
27+
public interface SessionIdGenerationStrategy {
28+
29+
@NonNull
30+
String generate();
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2014-2023 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+
* https://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.session;
18+
19+
import java.util.UUID;
20+
21+
import org.springframework.lang.NonNull;
22+
23+
/**
24+
* A {@link SessionIdGenerationStrategy} that generates a random UUID to be used as the
25+
* session id.
26+
*
27+
* @author Marcus da Coregio
28+
* @since 3.2
29+
*/
30+
public final class UuidSessionIdGenerationStrategy implements SessionIdGenerationStrategy {
31+
32+
private static final UuidSessionIdGenerationStrategy INSTANCE = new UuidSessionIdGenerationStrategy();
33+
34+
private UuidSessionIdGenerationStrategy() {
35+
}
36+
37+
@Override
38+
@NonNull
39+
public String generate() {
40+
return UUID.randomUUID().toString();
41+
}
42+
43+
/**
44+
* Returns the singleton instance of {@link UuidSessionIdGenerationStrategy}.
45+
* @return the singleton instance of {@link UuidSessionIdGenerationStrategy}
46+
*/
47+
public static UuidSessionIdGenerationStrategy getInstance() {
48+
return INSTANCE;
49+
}
50+
51+
}

spring-session-core/src/test/java/org/springframework/session/MapSessionTests.java

+49
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.time.Duration;
2020
import java.time.Instant;
2121
import java.util.Set;
22+
import java.util.UUID;
2223

2324
import org.junit.jupiter.api.BeforeEach;
2425
import org.junit.jupiter.api.Test;
@@ -42,6 +43,19 @@ void constructorNullSession() {
4243
.withMessage("session cannot be null");
4344
}
4445

46+
@Test
47+
void constructorWhenSessionIdGenerationStrategyThenUsesStrategy() {
48+
MapSession session = new MapSession(new FixedSessionIdGenerationStrategy("my-id"));
49+
assertThat(session.getId()).isEqualTo("my-id");
50+
}
51+
52+
@Test
53+
void constructorWhenDefaultThenUuid() {
54+
String id = this.session.getId();
55+
UUID uuid = UUID.fromString(id);
56+
assertThat(uuid).isNotNull();
57+
}
58+
4559
@Test
4660
void getAttributeWhenNullThenNull() {
4761
String result = this.session.getAttribute("attrName");
@@ -143,6 +157,41 @@ void getAttributeNamesAndRemove() {
143157
assertThat(this.session.getAttributeNames()).isEmpty();
144158
}
145159

160+
@Test
161+
void changeSessionIdWhenSessionIdStrategyThenUsesStrategy() {
162+
MapSession session = new MapSession(new IncrementalSessionIdGenerationStrategy());
163+
String idBeforeChange = session.getId();
164+
String idAfterChange = session.changeSessionId();
165+
assertThat(idBeforeChange).isEqualTo("1");
166+
assertThat(idAfterChange).isEqualTo("2");
167+
}
168+
169+
static class FixedSessionIdGenerationStrategy implements SessionIdGenerationStrategy {
170+
171+
private final String id;
172+
173+
FixedSessionIdGenerationStrategy(String id) {
174+
this.id = id;
175+
}
176+
177+
@Override
178+
public String generate() {
179+
return this.id;
180+
}
181+
182+
}
183+
184+
static class IncrementalSessionIdGenerationStrategy implements SessionIdGenerationStrategy {
185+
186+
private int counter = 1;
187+
188+
@Override
189+
public String generate() {
190+
return String.valueOf(this.counter++);
191+
}
192+
193+
}
194+
146195
static class CustomSession implements Session {
147196

148197
@Override

spring-session-core/src/test/java/org/springframework/session/ReactiveMapSessionRepositoryTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,31 @@ void getAttributeNamesAndRemove() {
152152
assertThat(session.getAttributeNames()).isEmpty();
153153
}
154154

155+
@Test
156+
void createSessionWhenSessionIdGenerationStrategyThenUses() {
157+
this.repository.setSessionIdGenerationStrategy(() -> "test");
158+
MapSession session = this.repository.createSession().block();
159+
assertThat(session.getId()).isEqualTo("test");
160+
assertThat(session.changeSessionId()).isEqualTo("test");
161+
}
162+
163+
@Test
164+
void setSessionIdGenerationStrategyWhenNullThenThrowsException() {
165+
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSessionIdGenerationStrategy(null))
166+
.withMessage("sessionIdGenerationStrategy cannot be null");
167+
}
168+
169+
@Test
170+
void findByIdWhenChangeSessionIdThenUsesSessionIdGenerationStrategy() {
171+
this.repository.setSessionIdGenerationStrategy(() -> "test");
172+
173+
MapSession session = this.repository.createSession().block();
174+
this.repository.save(session).block();
175+
176+
MapSession savedSession = this.repository.findById("test").block();
177+
178+
assertThat(savedSession.getId()).isEqualTo("test");
179+
assertThat(savedSession.changeSessionId()).isEqualTo("test");
180+
}
181+
155182
}

spring-session-data-mongodb/src/main/java/org/springframework/session/data/mongo/MongoIndexedSessionRepository.java

+23-5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import org.springframework.lang.Nullable;
3737
import org.springframework.session.FindByIndexNameSessionRepository;
3838
import org.springframework.session.MapSession;
39+
import org.springframework.session.SessionIdGenerationStrategy;
40+
import org.springframework.session.UuidSessionIdGenerationStrategy;
3941
import org.springframework.session.events.SessionCreatedEvent;
4042
import org.springframework.session.events.SessionDeletedEvent;
4143
import org.springframework.session.events.SessionExpiredEvent;
@@ -81,14 +83,16 @@ public class MongoIndexedSessionRepository
8183

8284
private ApplicationEventPublisher eventPublisher;
8385

86+
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
87+
8488
public MongoIndexedSessionRepository(MongoOperations mongoOperations) {
8589
this.mongoOperations = mongoOperations;
8690
}
8791

8892
@Override
8993
public MongoSession createSession() {
9094

91-
MongoSession session = new MongoSession();
95+
MongoSession session = new MongoSession(this.sessionIdGenerationStrategy);
9296

9397
session.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
9498

@@ -116,10 +120,13 @@ public MongoSession findById(String id) {
116120

117121
MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, sessionWrapper);
118122

119-
if (session != null && session.isExpired()) {
120-
publishEvent(new SessionExpiredEvent(this, session));
121-
deleteById(id);
122-
return null;
123+
if (session != null) {
124+
if (session.isExpired()) {
125+
publishEvent(new SessionExpiredEvent(this, session));
126+
deleteById(id);
127+
return null;
128+
}
129+
session.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy);
123130
}
124131

125132
return session;
@@ -140,6 +147,7 @@ public Map<String, MongoSession> findByIndexNameAndIndexValue(String indexName,
140147
.map((query) -> this.mongoOperations.find(query, Document.class, this.collectionName))
141148
.orElse(Collections.emptyList()).stream()
142149
.map((dbSession) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, dbSession))
150+
.peek((session) -> session.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy))
143151
.collect(Collectors.toMap(MongoSession::getId, (mapSession) -> mapSession));
144152
}
145153

@@ -216,4 +224,14 @@ public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSe
216224
this.mongoSessionConverter = mongoSessionConverter;
217225
}
218226

227+
/**
228+
* Set the {@link SessionIdGenerationStrategy} to use to generate session ids.
229+
* @param sessionIdGenerationStrategy the {@link SessionIdGenerationStrategy} to use
230+
* @since 3.2
231+
*/
232+
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
233+
Assert.notNull(sessionIdGenerationStrategy, "sessionIdGenerationStrategy cannot be null");
234+
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
235+
}
236+
219237
}

0 commit comments

Comments
 (0)