Polish WebSession support code

This commit is contained in:
Rossen Stoyanchev 2017-07-14 22:10:31 +02:00
parent c802827f0f
commit bf712957f6
6 changed files with 90 additions and 69 deletions

View File

@ -87,8 +87,17 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver {
@Override @Override
public void setSessionId(ServerWebExchange exchange, String id) { public void setSessionId(ServerWebExchange exchange, String id) {
Assert.notNull(id, "'id' is required");
setSessionCookie(exchange, id, getCookieMaxAge());
}
@Override
public void expireSession(ServerWebExchange exchange) {
setSessionCookie(exchange, "", Duration.ofSeconds(0));
}
private void setSessionCookie(ServerWebExchange exchange, String id, Duration maxAge) {
String name = getCookieName(); String name = getCookieName();
Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0));
boolean secure = "https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()); boolean secure = "https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme());
MultiValueMap<String, ResponseCookie> cookieMap = exchange.getResponse().getCookies(); MultiValueMap<String, ResponseCookie> cookieMap = exchange.getResponse().getCookies();
cookieMap.set(name, ResponseCookie.from(name, id).maxAge(maxAge).httpOnly(true).secure(secure).build()); cookieMap.set(name, ResponseCookie.from(name, id).maxAge(maxAge).httpOnly(true).secure(secure).build());

View File

@ -30,8 +30,9 @@ import org.springframework.web.server.WebSession;
/** /**
* Default implementation of {@link WebSessionManager} with a cookie-based web * Default implementation of {@link WebSessionManager} delegating to a
* session id resolution strategy and simple in-memory session persistence. * {@link WebSessionIdResolver} for session id resolution and to a
* {@link WebSessionStore}
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
@ -46,12 +47,12 @@ public class DefaultWebSessionManager implements WebSessionManager {
/** /**
* Configure the session id resolution strategy to use. * Configure the id resolution strategy.
* <p>By default {@link CookieWebSessionIdResolver} is used. * <p>By default an instance of {@link CookieWebSessionIdResolver}.
* @param sessionIdResolver the resolver * @param sessionIdResolver the resolver to use
*/ */
public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) { public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) {
Assert.notNull(sessionIdResolver, "'sessionIdResolver' is required."); Assert.notNull(sessionIdResolver, "WebSessionIdResolver is required.");
this.sessionIdResolver = sessionIdResolver; this.sessionIdResolver = sessionIdResolver;
} }
@ -63,12 +64,12 @@ public class DefaultWebSessionManager implements WebSessionManager {
} }
/** /**
* Configure the session persistence strategy to use. * Configure the persistence strategy.
* <p>By default {@link InMemoryWebSessionStore} is used. * <p>By default an instance of {@link InMemoryWebSessionStore}.
* @param sessionStore the persistence strategy * @param sessionStore the persistence strategy to use
*/ */
public void setSessionStore(WebSessionStore sessionStore) { public void setSessionStore(WebSessionStore sessionStore) {
Assert.notNull(sessionStore, "'sessionStore' is required."); Assert.notNull(sessionStore, "WebSessionStore is required.");
this.sessionStore = sessionStore; this.sessionStore = sessionStore;
} }
@ -80,10 +81,12 @@ public class DefaultWebSessionManager implements WebSessionManager {
} }
/** /**
* Configure the {@link Clock} for access to current time. During tests you * Configure the {@link Clock} to use to set lastAccessTime on every created
* may use {code Clock.offset(clock, Duration.ofMinutes(-31))} to set the * session and to calculate if it is expired.
* clock back for example to test changes after sessions expire. * <p>This may be useful to align to different timezone or to set the clock
* <p>By default {@code Clock.system(ZoneId.of("GMT"))} is used. * back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))}
* in order to simulate session expiration.
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
* @param clock the clock to use * @param clock the clock to use
*/ */
public void setClock(Clock clock) { public void setClock(Clock clock) {
@ -92,7 +95,7 @@ public class DefaultWebSessionManager implements WebSessionManager {
} }
/** /**
* Return the configured clock for access to current time. * Return the configured clock for session lastAccessTime calculations.
*/ */
public Clock getClock() { public Clock getClock() {
return this.clock; return this.clock;
@ -102,48 +105,45 @@ public class DefaultWebSessionManager implements WebSessionManager {
@Override @Override
public Mono<WebSession> getSession(ServerWebExchange exchange) { public Mono<WebSession> getSession(ServerWebExchange exchange) {
return Mono.defer(() -> return Mono.defer(() ->
Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange)) retrieveSession(exchange)
.concatMap(this.sessionStore::retrieveSession) .flatMap(session -> removeSessionIfExpired(exchange, session))
.next() .switchIfEmpty(createSession())
.flatMap(session -> validateSession(exchange, session)) .doOnNext(session -> {
.switchIfEmpty(createSession(exchange)) if (session instanceof ConfigurableWebSession) {
.map(session -> extendSession(exchange, session))); ConfigurableWebSession configurable = (ConfigurableWebSession) session;
configurable.setSaveOperation(() -> saveSession(exchange, session));
configurable.setLastAccessTime(Instant.now(getClock()));
}
exchange.getResponse().beforeCommit(session::save);
}));
} }
protected Mono<WebSession> validateSession(ServerWebExchange exchange, WebSession session) { private Mono<WebSession> retrieveSession(ServerWebExchange exchange) {
return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange))
.concatMap(this.sessionStore::retrieveSession)
.next();
}
private Mono<WebSession> removeSessionIfExpired(ServerWebExchange exchange, WebSession session) {
if (session.isExpired()) { if (session.isExpired()) {
this.sessionIdResolver.setSessionId(exchange, ""); this.sessionIdResolver.setSessionId(exchange, "");
return this.sessionStore.removeSession(session.getId()).cast(WebSession.class); return this.sessionStore.removeSession(session.getId()).then(Mono.empty());
} }
else {
return Mono.just(session);
}
}
protected Mono<WebSession> createSession(ServerWebExchange exchange) {
String sessionId = UUID.randomUUID().toString();
WebSession session = new DefaultWebSession(sessionId, getClock());
return Mono.just(session); return Mono.just(session);
} }
protected WebSession extendSession(ServerWebExchange exchange, WebSession session) { private Mono<DefaultWebSession> createSession() {
if (session instanceof ConfigurableWebSession) { return Mono.fromSupplier(() ->
ConfigurableWebSession managed = (ConfigurableWebSession) session; new DefaultWebSession(UUID.randomUUID().toString(), getClock()));
managed.setSaveOperation(() -> saveSession(exchange, session));
managed.setLastAccessTime(Instant.now(getClock()));
}
exchange.getResponse().beforeCommit(session::save);
return session;
} }
protected Mono<Void> saveSession(ServerWebExchange exchange, WebSession session) { private Mono<Void> saveSession(ServerWebExchange exchange, WebSession session) {
if (session.isExpired()) { if (session.isExpired()) {
return Mono.error(new IllegalStateException( return Mono.error(new IllegalStateException(
"Sessions are checked for expiration and have their " + "Sessions are checked for expiration and have their " +
"access time updated when first accessed during request processing. " + "lastAccessTime updated when first accessed during request processing. " +
"However this session is expired meaning that maxIdleTime elapsed " + "However this session is expired meaning that maxIdleTime elapsed " +
"since then and before the call to session.save().")); "before the call to session.save()."));
} }
if (!session.isStarted()) { if (!session.isStarted()) {
@ -153,11 +153,16 @@ public class DefaultWebSessionManager implements WebSessionManager {
// Force explicit start // Force explicit start
session.start(); session.start();
List<String> requestedIds = getSessionIdResolver().resolveSessionIds(exchange); if (hasNewSessionId(exchange, session)) {
if (requestedIds.isEmpty() || !session.getId().equals(requestedIds.get(0))) {
this.sessionIdResolver.setSessionId(exchange, session.getId()); this.sessionIdResolver.setSessionId(exchange, session.getId());
} }
return this.sessionStore.storeSession(session);
return this.sessionStore.storeSession(session);
}
private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) {
List<String> ids = getSessionIdResolver().resolveSessionIds(exchange);
return ids.isEmpty() || !session.getId().equals(ids.get(0));
} }
} }

View File

@ -22,8 +22,8 @@ import org.springframework.web.server.ServerWebExchange;
/** /**
* Contract for session id resolution strategies. Allows for session id * Contract for session id resolution strategies. Allows for session id
* resolution through the request and for sending the session id to the * resolution through the request and for sending the session id or expiring
* client through the response. * the session through the response.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
@ -39,11 +39,16 @@ public interface WebSessionIdResolver {
List<String> resolveSessionIds(ServerWebExchange exchange); List<String> resolveSessionIds(ServerWebExchange exchange);
/** /**
* Send the given session id to the client or if the session id is "null" * Send the given session id to the client.
* instruct the client to end the current session.
* @param exchange the current exchange * @param exchange the current exchange
* @param sessionId the session id * @param sessionId the session id
*/ */
void setSessionId(ServerWebExchange exchange, String sessionId); void setSessionId(ServerWebExchange exchange, String sessionId);
/**
* Instruct the client to end the current session.
* @param exchange the current exchange
*/
void expireSession(ServerWebExchange exchange);
} }

View File

@ -21,13 +21,7 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession; import org.springframework.web.server.WebSession;
/** /**
* Main contract abstracting support for access to {@link WebSession} instances * Main class for for access to the {@link WebSession} for an HTTP request.
* associated with HTTP requests as well as the subsequent management such as
* persistence and others.
*
* <p>The {@link DefaultWebSessionManager} implementation in turn delegates to
* {@link WebSessionIdResolver} and {@link WebSessionStore} which abstract
* underlying concerns related to the management of web sessions.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
@ -39,10 +33,10 @@ public interface WebSessionManager {
/** /**
* Return the {@link WebSession} for the given exchange. Always guaranteed * Return the {@link WebSession} for the given exchange. Always guaranteed
* to return an instance either matching to the session id requested by the * to return an instance either matching to the session id requested by the
* client, or with a new session id either because the client did not * client, or a new session either because the client did not specify one
* specify one or because the underlying session had expired. * or because the underlying session expired.
* @param exchange the current exchange * @param exchange the current exchange
* @return {@code Mono} for async access to the session * @return promise for the WebSession
*/ */
Mono<WebSession> getSession(ServerWebExchange exchange); Mono<WebSession> getSession(ServerWebExchange exchange);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,23 +28,23 @@ import org.springframework.web.server.WebSession;
public interface WebSessionStore { public interface WebSessionStore {
/** /**
* Store the given session. * Store the given WebSession.
* @param session the session to store * @param session the session to store
* @return {@code Mono} for completion notification * @return a completion notification (success or error)
*/ */
Mono<Void> storeSession(WebSession session); Mono<Void> storeSession(WebSession session);
/** /**
* Load the session for the given session id. * Return the WebSession for the given id.
* @param sessionId the session to load * @param sessionId the session to load
* @return {@code Mono} for async access to the loaded session * @return the session, or an empty {@code Mono}.
*/ */
Mono<WebSession> retrieveSession(String sessionId); Mono<WebSession> retrieveSession(String sessionId);
/** /**
* Remove the session with the given id. * Remove the WebSession for the specified id.
* @param sessionId the session to remove * @param sessionId the id of the session to remove
* @return {@code Mono} for completion notification * @return a completion notification (success or error)
*/ */
Mono<Void> removeSession(String sessionId); Mono<Void> removeSession(String sessionId);

View File

@ -27,6 +27,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -139,6 +140,7 @@ public class DefaultWebSessionManagerTests {
private List<String> idsToResolve = new ArrayList<>(); private List<String> idsToResolve = new ArrayList<>();
@Nullable
private String id = null; private String id = null;
@ -146,6 +148,7 @@ public class DefaultWebSessionManagerTests {
this.idsToResolve = idsToResolve; this.idsToResolve = idsToResolve;
} }
@Nullable
public String getSavedId() { public String getSavedId() {
return this.id; return this.id;
} }
@ -159,6 +162,11 @@ public class DefaultWebSessionManagerTests {
public void setSessionId(ServerWebExchange exchange, String sessionId) { public void setSessionId(ServerWebExchange exchange, String sessionId) {
this.id = sessionId; this.id = sessionId;
} }
@Override
public void expireSession(ServerWebExchange exchange) {
this.id = null;
}
} }
} }