diff --git a/spring-session-core/spring-session-core.gradle b/spring-session-core/spring-session-core.gradle
index e7413ce9..8c78724f 100644
--- a/spring-session-core/spring-session-core.gradle
+++ b/spring-session-core/spring-session-core.gradle
@@ -7,14 +7,16 @@ dependencies {
optional "io.projectreactor:reactor-core"
optional "javax.servlet:javax.servlet-api"
+ optional "org.springframework.security:spring-security-core"
+ optional "org.springframework.security:spring-security-web"
optional "org.springframework:spring-context"
optional "org.springframework:spring-jdbc"
optional "org.springframework:spring-messaging"
optional "org.springframework:spring-web"
+ optional "org.springframework:spring-webflux"
optional "org.springframework:spring-websocket"
- optional "org.springframework.security:spring-security-core"
- optional "org.springframework.security:spring-security-web"
+ testCompile "io.projectreactor:reactor-test"
testCompile "junit:junit"
testCompile "org.mockito:mockito-core"
testCompile "edu.umd.cs.mtc:multithreadedtc"
diff --git a/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionManager.java b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionManager.java
new file mode 100644
index 00000000..599f7b9f
--- /dev/null
+++ b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionManager.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2014-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.session.web.server.session;
+
+import org.springframework.session.ReactorSessionRepository;
+import org.springframework.session.Session;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebSession;
+import org.springframework.web.server.session.CookieWebSessionIdResolver;
+import org.springframework.web.server.session.WebSessionIdResolver;
+import org.springframework.web.server.session.WebSessionManager;
+import org.springframework.web.server.session.WebSessionStore;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.List;
+
+/**
+ * @author Rob Winch
+ * @since 2.0
+ */
+public class SpringSessionWebSessionManager implements WebSessionManager {
+ private final SpringSessionWebSessionStore extends Session> sessionStore;
+
+ private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
+
+ private Clock clock = Clock.system(ZoneId.of("GMT"));
+
+ public SpringSessionWebSessionManager(ReactorSessionRepository extends Session> sessionRepository) {
+ sessionStore
+ = new SpringSessionWebSessionStore<>(sessionRepository);
+ }
+
+ /**
+ * Configure the id resolution strategy.
+ *
By default an instance of {@link CookieWebSessionIdResolver}.
+ * @param sessionIdResolver the resolver to use
+ */
+ public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) {
+ Assert.notNull(sessionIdResolver, "WebSessionIdResolver is required.");
+ this.sessionIdResolver = sessionIdResolver;
+ }
+
+ /**
+ * Return the configured {@link WebSessionIdResolver}.
+ */
+ private WebSessionIdResolver getSessionIdResolver() {
+ return this.sessionIdResolver;
+ }
+
+ /**
+ * Return the configured {@link WebSessionStore}.
+ */
+ private WebSessionStore getSessionStore() {
+ return this.sessionStore;
+ }
+
+ /**
+ * Configure the {@link Clock} to use to set lastAccessTime on every created
+ * session and to calculate if it is expired.
+ *
This may be useful to align to different timezone or to set the clock
+ * back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))}
+ * in order to simulate session expiration.
+ *
By default this is {@code Clock.system(ZoneId.of("GMT"))}.
+ * @param clock the clock to use
+ */
+ public void setClock(Clock clock) {
+ Assert.notNull(clock, "'clock' is required.");
+ this.clock = clock;
+ }
+
+ /**
+ * Return the configured clock for session lastAccessTime calculations.
+ */
+ private Clock getClock() {
+ return this.clock;
+ }
+
+ @Override
+ public Mono getSession(ServerWebExchange exchange) {
+ return Mono.defer(() ->
+ retrieveSession(exchange)
+ .flatMap(session -> removeSessionIfExpired(exchange, session))
+ .flatMap(session -> {
+ Instant lastAccessTime = Instant.now(getClock());
+ return this.sessionStore.setLastAccessedTime(session, lastAccessTime);
+ })
+ .switchIfEmpty(createSession(exchange))
+ .doOnNext(session -> exchange.getResponse().beforeCommit(session::save)));
+ }
+
+ private Mono retrieveSession(ServerWebExchange exchange) {
+ return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange))
+ .concatMap(this.sessionStore::retrieveSession)
+ .cast(WebSession.class)
+ .next();
+ }
+
+ private Mono removeSessionIfExpired(ServerWebExchange exchange, WebSession session) {
+ if (session.isExpired()) {
+ this.sessionIdResolver.expireSession(exchange);
+ return this.sessionStore.removeSession(session.getId()).then(Mono.empty());
+ }
+ return Mono.just(session);
+ }
+
+ private Mono saveSession(ServerWebExchange exchange, WebSession session) {
+ if (session.isExpired()) {
+ return Mono.error(new IllegalStateException(
+ "Sessions are checked for expiration and have their " +
+ "lastAccessTime updated when first accessed during request processing. " +
+ "However this session is expired meaning that maxIdleTime elapsed " +
+ "before the call to session.save()."));
+ }
+
+ if (!session.isStarted()) {
+ return Mono.empty();
+ }
+
+ // Force explicit start
+ session.start();
+
+ if (hasNewSessionId(exchange, session)) {
+ this.sessionIdResolver.setSessionId(exchange, session.getId());
+ }
+
+ return this.sessionStore.storeSession(session);
+ }
+
+ private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) {
+ List ids = getSessionIdResolver().resolveSessionIds(exchange);
+ return ids.isEmpty() || !session.getId().equals(ids.get(0));
+ }
+
+ private Mono createSession(ServerWebExchange exchange) {
+ return this.sessionStore.createSession();
+ }
+}
diff --git a/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionStore.java b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionStore.java
new file mode 100644
index 00000000..427060d5
--- /dev/null
+++ b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionStore.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2014-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.session.web.server.session;
+
+import org.springframework.lang.Nullable;
+import org.springframework.session.ReactorSessionRepository;
+import org.springframework.session.Session;
+import org.springframework.util.Assert;
+import org.springframework.web.server.WebSession;
+import org.springframework.web.server.session.WebSessionStore;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * @author Rob Winch
+ * @since 2.0
+ */
+class SpringSessionWebSessionStore implements WebSessionStore {
+ private final ReactorSessionRepository sessions;
+
+ SpringSessionWebSessionStore(ReactorSessionRepository sessions) {
+ Assert.notNull(sessions, "sessions cannot be null");
+ this.sessions = sessions;
+ }
+
+ public Mono createSession() {
+ return this.sessions.createSession().map(this::createSession);
+ }
+
+ public Mono setLastAccessedTime(WebSession session,
+ Instant lastAccessedTime) {
+ SpringSessionWebSession springSessionWebSession = (SpringSessionWebSession) session;
+ springSessionWebSession.session.setLastAccessedTime(lastAccessedTime);
+ return Mono.just(session);
+ }
+
+ @Override
+ public Mono storeSession(WebSession session) {
+ @SuppressWarnings("unchecked")
+ SpringSessionWebSession springWebSession = (SpringSessionWebSession) session;
+ return this.sessions.save(springWebSession.session);
+ }
+
+ @Override
+ public Mono retrieveSession(String sessionId) {
+ return this.sessions.findById(sessionId).map(this::existingSession);
+ }
+
+ @Override
+ public Mono changeSessionId(String s, WebSession webSession) {
+ return storeSession(webSession);
+ }
+
+ private SpringSessionWebSession createSession(S session) {
+ return new SpringSessionWebSession(session, State.NEW);
+ }
+
+ private SpringSessionWebSession existingSession(S session) {
+ return new SpringSessionWebSession(session, State.STARTED);
+ }
+
+ @Override
+ public Mono removeSession(String sessionId) {
+ return sessions.delete(sessionId);
+ }
+
+ private class SpringSessionWebSession implements WebSession {
+ private final S session;
+
+ private final Map attributes;
+
+ private AtomicReference state = new AtomicReference<>();
+
+ private volatile transient Supplier> saveOperation = Mono::empty;
+
+ SpringSessionWebSession(S session, State state) {
+ Assert.notNull(session, "session cannot be null");
+ this.session = session;
+ this.attributes = new SpringSessionMap(session);
+ this.state.set(state);
+ }
+
+ @Override
+ public String getId() {
+ return session.getId();
+ }
+
+ @Override
+ public Mono changeSessionId() {
+ return Mono.defer(() -> {
+ session.changeSessionId();
+ return save();
+ });
+ }
+
+ @Override
+ public Map getAttributes() {
+ return this.attributes;
+ }
+
+ @Override
+ public void start() {
+ this.state.compareAndSet(State.NEW, State.STARTED);
+ }
+
+ @Override
+ public boolean isStarted() {
+ State value = this.state.get();
+ return (State.STARTED.equals(value)
+ || (State.NEW.equals(value) && !getAttributes().isEmpty()));
+ }
+
+ @Override
+ public Mono save() {
+ return this.saveOperation.get();
+ }
+
+ @Override
+ public boolean isExpired() {
+ return this.session.isExpired();
+ }
+
+ @Override
+ public Instant getCreationTime() {
+ return this.session.getCreationTime();
+ }
+
+ @Override
+ public Instant getLastAccessTime() {
+ return this.session.getLastAccessedTime();
+ }
+
+ @Override
+ public void setMaxIdleTime(Duration maxIdleTime) {
+ this.session.setMaxInactiveInterval(maxIdleTime);
+ }
+
+ @Override
+ public Duration getMaxIdleTime() {
+ return this.session.getMaxInactiveInterval();
+ }
+ }
+
+ private enum State {
+ NEW, STARTED
+ }
+
+ static class SpringSessionMap implements Map {
+ private final Session session;
+ private final Collection