parent
6da3518a66
commit
15cc44e6e8
|
@ -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.
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue