Polish WebSession support code
This commit is contained in:
parent
c802827f0f
commit
bf712957f6
|
@ -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());
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue