diff --git a/spring-web/src/main/java/org/springframework/web/server/WebSession.java b/spring-web/src/main/java/org/springframework/web/server/WebSession.java index 8cbc0fdea9..6fe7e1cc2a 100644 --- a/spring-web/src/main/java/org/springframework/web/server/WebSession.java +++ b/spring-web/src/main/java/org/springframework/web/server/WebSession.java @@ -109,6 +109,12 @@ public interface WebSession { */ Mono changeSessionId(); + /** + * Invalidate the current session and clear session storage. + * @return completion notification (success or error) + */ + Mono invalidate(); + /** * Save the session persisting attributes (e.g. if stored remotely) and also * sending the session id to the client if the session is new. diff --git a/spring-web/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index bbeec287bb..f7bb74cdf2 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -90,26 +90,21 @@ public class DefaultWebSessionManager implements WebSessionManager { } private Mono save(ServerWebExchange exchange, WebSession session) { - if (session.isExpired()) { - return Mono.error(new IllegalStateException("Session='" + session.getId() + "' expired.")); - } - if (!session.isStarted()) { - if (hasNewSessionId(exchange, session)) { + List ids = getSessionIdResolver().resolveSessionIds(exchange); + + if (!session.isStarted() || session.isExpired()) { + if (!ids.isEmpty()) { + // Expired on retrieve or while processing request, or invalidated.. this.sessionIdResolver.expireSession(exchange); } return Mono.empty(); } - if (hasNewSessionId(exchange, session)) { + if (ids.isEmpty() || !session.getId().equals(ids.get(0))) { this.sessionIdResolver.setSessionId(exchange, session.getId()); } return session.save(); } - - private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) { - List ids = getSessionIdResolver().resolveSessionIds(exchange); - return ids.isEmpty() || !session.getId().equals(ids.get(0)); - } } diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index c6eb8e6c6d..6192d1304d 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -109,25 +109,22 @@ public class InMemoryWebSessionStore implements WebSessionStore { private class InMemoryWebSession implements WebSession { - private final AtomicReference id; + private final AtomicReference id = new AtomicReference<>(String.valueOf(idGenerator.generateId())); - private final Map attributes; + private final Map attributes = new ConcurrentHashMap<>(); private final Instant creationTime; private volatile Instant lastAccessTime; - private volatile Duration maxIdleTime; + private volatile Duration maxIdleTime = Duration.ofMinutes(30); - private volatile boolean started; + private final AtomicReference state = new AtomicReference<>(State.NEW); InMemoryWebSession() { - this.id = new AtomicReference<>(String.valueOf(idGenerator.generateId())); - this.attributes = new ConcurrentHashMap<>(); this.creationTime = Instant.now(getClock()); this.lastAccessTime = this.creationTime; - this.maxIdleTime = Duration.ofMinutes(30); } @@ -163,12 +160,12 @@ public class InMemoryWebSessionStore implements WebSessionStore { @Override public void start() { - this.started = true; + this.state.compareAndSet(State.NEW, State.STARTED); } @Override public boolean isStarted() { - return this.started || !getAttributes().isEmpty(); + return this.state.get().equals(State.STARTED) || !getAttributes().isEmpty(); } @Override @@ -185,21 +182,46 @@ public class InMemoryWebSessionStore implements WebSessionStore { return Mono.empty(); } + @Override + public Mono invalidate() { + this.state.set(State.EXPIRED); + getAttributes().clear(); + InMemoryWebSessionStore.this.sessions.remove(this.id.get()); + return Mono.empty(); + } + @Override public Mono save() { + if (!getAttributes().isEmpty()) { + this.state.compareAndSet(State.NEW, State.STARTED); + } InMemoryWebSessionStore.this.sessions.put(this.getId(), this); return Mono.empty(); } @Override public boolean isExpired() { - return (isStarted() && !this.maxIdleTime.isNegative() && - Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime)); + if (this.state.get().equals(State.EXPIRED)) { + return true; + } + if (checkExpired()) { + this.state.set(State.EXPIRED); + return true; + } + return false; + } + + private boolean checkExpired() { + return isStarted() && !this.maxIdleTime.isNegative() && + Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime); } private void updateLastAccessTime() { this.lastAccessTime = Instant.now(getClock()); } + } + private enum State { NEW, STARTED, EXPIRED } + } diff --git a/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index 0e0883d08b..51a08fd351 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -70,7 +70,6 @@ public class DefaultWebSessionManagerTests { @Before public void setUp() throws Exception { when(this.store.createWebSession()).thenReturn(Mono.just(this.createSession)); - when(this.store.updateLastAccessTime(any())).thenReturn(Mono.just(this.updateSession)); when(this.createSession.save()).thenReturn(Mono.empty()); when(this.updateSession.getId()).thenReturn("update-session-id"); diff --git a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java index ac49f905c5..d0bf5cce5a 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java @@ -89,4 +89,9 @@ public class InMemoryWebSessionStoreTests { Instant time2 = session2.getLastAccessTime(); assertTrue(time1.isBefore(time2)); } + + @Test + public void invalidate() throws Exception { + + } } \ No newline at end of file diff --git a/spring-web/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java index f862f0d904..102756f56d 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java @@ -20,7 +20,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.Clock; import java.time.Duration; -import java.time.Instant; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -136,14 +135,13 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe assertEquals(HttpStatus.OK, response.getStatusCode()); String id = extractSessionId(response.getHeaders()); assertNotNull(id); - assertEquals(1, this.handler.getSessionRequestCount()); // Now fast-forward by 31 minutes InMemoryWebSessionStore store = (InMemoryWebSessionStore) this.sessionManager.getSessionStore(); store.setClock(Clock.offset(store.getClock(), Duration.ofMinutes(31))); // Second request: session expires - URI uri = new URI("http://localhost:" + this.port + "/?expiredSession"); + URI uri = new URI("http://localhost:" + this.port + "/?expire"); request = RequestEntity.get(uri).header("Cookie", "SESSION=" + id).build(); response = this.restTemplate.exchange(request, Void.class); @@ -177,6 +175,28 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe assertEquals(2, this.handler.getSessionRequestCount()); } + @Test + public void invalidate() throws Exception { + + // First request: no session yet, new session created + RequestEntity request = RequestEntity.get(createUri()).build(); + ResponseEntity response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + String id = extractSessionId(response.getHeaders()); + assertNotNull(id); + + // Second request: invalidates session + URI uri = new URI("http://localhost:" + this.port + "/?invalidate"); + request = RequestEntity.get(uri).header("Cookie", "SESSION=" + id).build(); + response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + String value = response.getHeaders().getFirst("Set-Cookie"); + assertNotNull(value); + assertTrue("Actual value: " + value, value.contains("Max-Age=0")); + } + private String extractSessionId(HttpHeaders headers) { List headerValues = headers.get("Set-Cookie"); assertNotNull(headerValues); @@ -206,7 +226,7 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe @Override public Mono handle(ServerWebExchange exchange) { - if (exchange.getRequest().getQueryParams().containsKey("expiredSession")) { + if (exchange.getRequest().getQueryParams().containsKey("expire")) { return exchange.getSession().doOnNext(session -> { // Don't do anything, leave it expired... }).then(); @@ -215,6 +235,9 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe return exchange.getSession().flatMap(session -> session.changeSessionId().doOnSuccess(aVoid -> updateSessionAttribute(session))); } + else if (exchange.getRequest().getQueryParams().containsKey("invalidate")) { + return exchange.getSession().doOnNext(WebSession::invalidate).then(); + } else { return exchange.getSession().doOnSuccess(this::updateSessionAttribute).then(); }