Add WebSession.invalidate()

Issue: SPR-15960
This commit is contained in:
Rossen Stoyanchev 2017-09-27 00:10:38 -04:00
parent 6da3518a66
commit 15cc44e6e8
6 changed files with 77 additions and 27 deletions

View File

@ -109,6 +109,12 @@ public interface WebSession {
*/ */
Mono<Void> changeSessionId(); Mono<Void> changeSessionId();
/**
* Invalidate the current session and clear session storage.
* @return completion notification (success or error)
*/
Mono<Void> invalidate();
/** /**
* Save the session persisting attributes (e.g. if stored remotely) and also * Save the session persisting attributes (e.g. if stored remotely) and also
* sending the session id to the client if the session is new. * sending the session id to the client if the session is new.

View File

@ -90,26 +90,21 @@ public class DefaultWebSessionManager implements WebSessionManager {
} }
private Mono<Void> save(ServerWebExchange exchange, WebSession session) { private Mono<Void> save(ServerWebExchange exchange, WebSession session) {
if (session.isExpired()) {
return Mono.error(new IllegalStateException("Session='" + session.getId() + "' expired."));
}
if (!session.isStarted()) { List<String> ids = getSessionIdResolver().resolveSessionIds(exchange);
if (hasNewSessionId(exchange, session)) {
if (!session.isStarted() || session.isExpired()) {
if (!ids.isEmpty()) {
// Expired on retrieve or while processing request, or invalidated..
this.sessionIdResolver.expireSession(exchange); this.sessionIdResolver.expireSession(exchange);
} }
return Mono.empty(); return Mono.empty();
} }
if (hasNewSessionId(exchange, session)) { if (ids.isEmpty() || !session.getId().equals(ids.get(0))) {
this.sessionIdResolver.setSessionId(exchange, session.getId()); this.sessionIdResolver.setSessionId(exchange, session.getId());
} }
return session.save(); return session.save();
} }
private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) {
List<String> ids = getSessionIdResolver().resolveSessionIds(exchange);
return ids.isEmpty() || !session.getId().equals(ids.get(0));
}
} }

View File

@ -109,25 +109,22 @@ public class InMemoryWebSessionStore implements WebSessionStore {
private class InMemoryWebSession implements WebSession { private class InMemoryWebSession implements WebSession {
private final AtomicReference<String> id; private final AtomicReference<String> id = new AtomicReference<>(String.valueOf(idGenerator.generateId()));
private final Map<String, Object> attributes; private final Map<String, Object> attributes = new ConcurrentHashMap<>();
private final Instant creationTime; private final Instant creationTime;
private volatile Instant lastAccessTime; private volatile Instant lastAccessTime;
private volatile Duration maxIdleTime; private volatile Duration maxIdleTime = Duration.ofMinutes(30);
private volatile boolean started; private final AtomicReference<State> state = new AtomicReference<>(State.NEW);
InMemoryWebSession() { InMemoryWebSession() {
this.id = new AtomicReference<>(String.valueOf(idGenerator.generateId()));
this.attributes = new ConcurrentHashMap<>();
this.creationTime = Instant.now(getClock()); this.creationTime = Instant.now(getClock());
this.lastAccessTime = this.creationTime; this.lastAccessTime = this.creationTime;
this.maxIdleTime = Duration.ofMinutes(30);
} }
@ -163,12 +160,12 @@ public class InMemoryWebSessionStore implements WebSessionStore {
@Override @Override
public void start() { public void start() {
this.started = true; this.state.compareAndSet(State.NEW, State.STARTED);
} }
@Override @Override
public boolean isStarted() { public boolean isStarted() {
return this.started || !getAttributes().isEmpty(); return this.state.get().equals(State.STARTED) || !getAttributes().isEmpty();
} }
@Override @Override
@ -185,21 +182,46 @@ public class InMemoryWebSessionStore implements WebSessionStore {
return Mono.empty(); return Mono.empty();
} }
@Override
public Mono<Void> invalidate() {
this.state.set(State.EXPIRED);
getAttributes().clear();
InMemoryWebSessionStore.this.sessions.remove(this.id.get());
return Mono.empty();
}
@Override @Override
public Mono<Void> save() { public Mono<Void> save() {
if (!getAttributes().isEmpty()) {
this.state.compareAndSet(State.NEW, State.STARTED);
}
InMemoryWebSessionStore.this.sessions.put(this.getId(), this); InMemoryWebSessionStore.this.sessions.put(this.getId(), this);
return Mono.empty(); return Mono.empty();
} }
@Override @Override
public boolean isExpired() { public boolean isExpired() {
return (isStarted() && !this.maxIdleTime.isNegative() && if (this.state.get().equals(State.EXPIRED)) {
Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime)); 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() { private void updateLastAccessTime() {
this.lastAccessTime = Instant.now(getClock()); this.lastAccessTime = Instant.now(getClock());
} }
} }
private enum State { NEW, STARTED, EXPIRED }
} }

View File

@ -70,7 +70,6 @@ public class DefaultWebSessionManagerTests {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
when(this.store.createWebSession()).thenReturn(Mono.just(this.createSession)); 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.createSession.save()).thenReturn(Mono.empty());
when(this.updateSession.getId()).thenReturn("update-session-id"); when(this.updateSession.getId()).thenReturn("update-session-id");

View File

@ -89,4 +89,9 @@ public class InMemoryWebSessionStoreTests {
Instant time2 = session2.getLastAccessTime(); Instant time2 = session2.getLastAccessTime();
assertTrue(time1.isBefore(time2)); assertTrue(time1.isBefore(time2));
} }
@Test
public void invalidate() throws Exception {
}
} }

View File

@ -20,7 +20,6 @@ import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Clock; import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -136,14 +135,13 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe
assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, response.getStatusCode());
String id = extractSessionId(response.getHeaders()); String id = extractSessionId(response.getHeaders());
assertNotNull(id); assertNotNull(id);
assertEquals(1, this.handler.getSessionRequestCount());
// Now fast-forward by 31 minutes // Now fast-forward by 31 minutes
InMemoryWebSessionStore store = (InMemoryWebSessionStore) this.sessionManager.getSessionStore(); InMemoryWebSessionStore store = (InMemoryWebSessionStore) this.sessionManager.getSessionStore();
store.setClock(Clock.offset(store.getClock(), Duration.ofMinutes(31))); store.setClock(Clock.offset(store.getClock(), Duration.ofMinutes(31)));
// Second request: session expires // 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(); request = RequestEntity.get(uri).header("Cookie", "SESSION=" + id).build();
response = this.restTemplate.exchange(request, Void.class); response = this.restTemplate.exchange(request, Void.class);
@ -177,6 +175,28 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe
assertEquals(2, this.handler.getSessionRequestCount()); assertEquals(2, this.handler.getSessionRequestCount());
} }
@Test
public void invalidate() throws Exception {
// First request: no session yet, new session created
RequestEntity<Void> request = RequestEntity.get(createUri()).build();
ResponseEntity<Void> 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) { private String extractSessionId(HttpHeaders headers) {
List<String> headerValues = headers.get("Set-Cookie"); List<String> headerValues = headers.get("Set-Cookie");
assertNotNull(headerValues); assertNotNull(headerValues);
@ -206,7 +226,7 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe
@Override @Override
public Mono<Void> handle(ServerWebExchange exchange) { public Mono<Void> handle(ServerWebExchange exchange) {
if (exchange.getRequest().getQueryParams().containsKey("expiredSession")) { if (exchange.getRequest().getQueryParams().containsKey("expire")) {
return exchange.getSession().doOnNext(session -> { return exchange.getSession().doOnNext(session -> {
// Don't do anything, leave it expired... // Don't do anything, leave it expired...
}).then(); }).then();
@ -215,6 +235,9 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe
return exchange.getSession().flatMap(session -> return exchange.getSession().flatMap(session ->
session.changeSessionId().doOnSuccess(aVoid -> updateSessionAttribute(session))); session.changeSessionId().doOnSuccess(aVoid -> updateSessionAttribute(session)));
} }
else if (exchange.getRequest().getQueryParams().containsKey("invalidate")) {
return exchange.getSession().doOnNext(WebSession::invalidate).then();
}
else { else {
return exchange.getSession().doOnSuccess(this::updateSessionAttribute).then(); return exchange.getSession().doOnSuccess(this::updateSessionAttribute).then();
} }