Server-side session support
This commit adds initial support for a maintaining a server-side session with attributes across HTTP requests. The WebSession abstraction can be accessed via WebServerExchange from a WebFilter or the target WebHandler. The session sub-package contains additional abstractions for creating and managing sessions providing a basis for extensibility (e.g. Spring Session). Those include WebSessionManager, SessionIdStrategy, and SessionStore along with a cookie-based session id strategy and an in-memory session store in use by default. Note that the current API does not provide a way to invalidate or re-create the session from server side code.
This commit is contained in:
parent
3744549a3e
commit
407d11a58a
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package org.springframework.http;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
|
@ -34,7 +36,7 @@ public final class HttpCookie {
|
|||
|
||||
private final String value;
|
||||
|
||||
private final int maxAge;
|
||||
private final Duration maxAge;
|
||||
|
||||
private final String domain;
|
||||
|
||||
|
@ -46,17 +48,17 @@ public final class HttpCookie {
|
|||
|
||||
|
||||
private HttpCookie(String name, String value) {
|
||||
this(name, value, -1, null, null, false, false);
|
||||
this(name, value, Duration.ofSeconds(-1), null, null, false, false);
|
||||
}
|
||||
|
||||
private HttpCookie(String name, String value, int maxAge, String domain, String path,
|
||||
private HttpCookie(String name, String value, Duration maxAge, String domain, String path,
|
||||
boolean secure, boolean httpOnly) {
|
||||
|
||||
Assert.hasLength(name, "'name' is required and must not be empty.");
|
||||
Assert.hasLength(value, "'value' is required and must not be empty.");
|
||||
Assert.notNull(maxAge);
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.maxAge = (maxAge > -1 ? maxAge : -1);
|
||||
this.value = (value != null ? value : "");
|
||||
this.maxAge = maxAge;
|
||||
this.domain = domain;
|
||||
this.path = path;
|
||||
this.secure = secure;
|
||||
|
@ -85,7 +87,7 @@ public final class HttpCookie {
|
|||
* A negative value means no "Max-Age" attribute in which case the cookie
|
||||
* is removed when the browser is closed.
|
||||
*/
|
||||
public int getMaxAge() {
|
||||
public Duration getMaxAge() {
|
||||
return this.maxAge;
|
||||
}
|
||||
|
||||
|
@ -162,7 +164,7 @@ public final class HttpCookie {
|
|||
|
||||
return new HttpCookieBuilder() {
|
||||
|
||||
private int maxAge = -1;
|
||||
private Duration maxAge = Duration.ofSeconds(-1);
|
||||
|
||||
private String domain;
|
||||
|
||||
|
@ -174,7 +176,7 @@ public final class HttpCookie {
|
|||
|
||||
|
||||
@Override
|
||||
public HttpCookieBuilder maxAge(int maxAge) {
|
||||
public HttpCookieBuilder maxAge(Duration maxAge) {
|
||||
this.maxAge = maxAge;
|
||||
return this;
|
||||
}
|
||||
|
@ -217,14 +219,14 @@ public final class HttpCookie {
|
|||
public interface HttpCookieBuilder {
|
||||
|
||||
/**
|
||||
* Set the cookie "Max-Age" attribute in seconds.
|
||||
* Set the cookie "Max-Age" attribute.
|
||||
*
|
||||
* <p>A positive value indicates when the cookie should expire relative
|
||||
* to the current time. A value of 0 means the cookie should expire
|
||||
* immediately. A negative value results in no "Max-Age" attribute in
|
||||
* which case the cookie is removed when the browser is closed.
|
||||
*/
|
||||
HttpCookieBuilder maxAge(int maxAge);
|
||||
HttpCookieBuilder maxAge(Duration maxAge);
|
||||
|
||||
/**
|
||||
* Set the cookie "Path" attribute.
|
||||
|
|
|
@ -66,25 +66,23 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
|
|||
}
|
||||
|
||||
private Mono<Void> applyBeforeCommit() {
|
||||
return Stream.defer(() -> {
|
||||
Mono<Void> mono = Mono.empty();
|
||||
if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
|
||||
for (Supplier<? extends Mono<Void>> action : this.beforeCommitActions) {
|
||||
mono = mono.after(() -> action.get());
|
||||
}
|
||||
mono = mono.otherwise(ex -> {
|
||||
// Ignore errors from beforeCommit actions
|
||||
return Mono.empty();
|
||||
});
|
||||
mono = mono.after(() -> {
|
||||
this.state.set(State.COMITTED);
|
||||
writeHeaders();
|
||||
writeCookies();
|
||||
return Mono.empty();
|
||||
});
|
||||
Mono<Void> mono = Mono.empty();
|
||||
if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
|
||||
for (Supplier<? extends Mono<Void>> action : this.beforeCommitActions) {
|
||||
mono = mono.after(() -> action.get());
|
||||
}
|
||||
return mono;
|
||||
}).after();
|
||||
mono = mono.otherwise(ex -> {
|
||||
// Ignore errors from beforeCommit actions
|
||||
return Mono.empty();
|
||||
});
|
||||
mono = mono.after(() -> {
|
||||
this.state.set(State.COMITTED);
|
||||
writeHeaders();
|
||||
writeCookies();
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package org.springframework.http.server.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
@ -73,55 +75,59 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
|
|||
protected void writeCookies() {
|
||||
for (String name : getHeaders().getCookies().keySet()) {
|
||||
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
|
||||
Cookie cookie = new ReactorCookie(name, httpCookie);
|
||||
this.channel.addResponseCookie(name, cookie);
|
||||
Cookie reactorCookie = new ReactorCookie(httpCookie);
|
||||
this.channel.addResponseCookie(name, reactorCookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* At present Reactor does not provide a {@link Cookie} implementation.
|
||||
*/
|
||||
private final static class ReactorCookie extends Cookie {
|
||||
|
||||
final HttpCookie httpCookie;
|
||||
final String name;
|
||||
private final HttpCookie httpCookie;
|
||||
|
||||
public ReactorCookie(String name, HttpCookie httpCookie) {
|
||||
this.name = name;
|
||||
|
||||
public ReactorCookie(HttpCookie httpCookie) {
|
||||
this.httpCookie = httpCookie;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
return this.httpCookie.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String value() {
|
||||
return httpCookie.getValue();
|
||||
return this.httpCookie.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean httpOnly() {
|
||||
return httpCookie.isHttpOnly();
|
||||
return this.httpCookie.isHttpOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long maxAge() {
|
||||
return httpCookie.getMaxAge() > -1 ? httpCookie.getMaxAge() : -1;
|
||||
Duration maxAge = this.httpCookie.getMaxAge();
|
||||
return (!maxAge.isNegative() ? maxAge.getSeconds() : -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String domain() {
|
||||
return httpCookie.getDomain();
|
||||
return this.httpCookie.getDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return httpCookie.getPath();
|
||||
return this.httpCookie.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean secure() {
|
||||
return httpCookie.isSecure();
|
||||
return this.httpCookie.isSecure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,8 +82,8 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
|
|||
for (String name : getHeaders().getCookies().keySet()) {
|
||||
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
|
||||
Cookie cookie = new DefaultCookie(name, httpCookie.getValue());
|
||||
if (httpCookie.getMaxAge() > -1) {
|
||||
cookie.setMaxAge(httpCookie.getMaxAge());
|
||||
if (!httpCookie.getMaxAge().isNegative()) {
|
||||
cookie.setMaxAge(httpCookie.getMaxAge().getSeconds());
|
||||
}
|
||||
cookie.setDomain(httpCookie.getDomain());
|
||||
cookie.setPath(httpCookie.getPath());
|
||||
|
|
|
@ -113,14 +113,17 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest {
|
|||
|
||||
@Override
|
||||
protected void initCookies(Map<String, List<HttpCookie>> map) {
|
||||
for (Cookie cookie : this.request.getCookies()) {
|
||||
String name = cookie.getName();
|
||||
List<HttpCookie> list = map.get(name);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
map.put(name, list);
|
||||
Cookie[] cookies = this.request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
String name = cookie.getName();
|
||||
List<HttpCookie> list = map.get(name);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
map.put(name, list);
|
||||
}
|
||||
list.add(HttpCookie.clientCookie(name, cookie.getValue()));
|
||||
}
|
||||
list.add(HttpCookie.clientCookie(name, cookie.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -91,8 +91,8 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse {
|
|||
for (String name : getHeaders().getCookies().keySet()) {
|
||||
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
|
||||
Cookie cookie = new Cookie(name, httpCookie.getValue());
|
||||
if (httpCookie.getMaxAge() > -1) {
|
||||
cookie.setMaxAge(httpCookie.getMaxAge());
|
||||
if (!httpCookie.getMaxAge().isNegative()) {
|
||||
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
|
||||
}
|
||||
if (httpCookie.getDomain() != null) {
|
||||
cookie.setDomain(httpCookie.getDomain());
|
||||
|
|
|
@ -83,8 +83,8 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse {
|
|||
for (String name : getHeaders().getCookies().keySet()) {
|
||||
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
|
||||
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
|
||||
if (httpCookie.getMaxAge() > -1) {
|
||||
cookie.setMaxAge(httpCookie.getMaxAge());
|
||||
if (!httpCookie.getMaxAge().isNegative()) {
|
||||
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
|
||||
}
|
||||
cookie.setDomain(httpCookie.getDomain());
|
||||
cookie.setPath(httpCookie.getPath());
|
||||
|
|
|
@ -18,9 +18,14 @@ package org.springframework.web.server;
|
|||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import reactor.core.publisher.FluxProcessor;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Processors;
|
||||
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebServerExchange}.
|
||||
|
@ -33,14 +38,26 @@ public class DefaultWebServerExchange implements WebServerExchange {
|
|||
|
||||
private final ServerHttpResponse response;
|
||||
|
||||
private final WebSessionManager sessionManager;
|
||||
|
||||
|
||||
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
|
||||
|
||||
private final Object createSessionLock = new Object();
|
||||
|
||||
private Mono<WebSession> sessionMono;
|
||||
|
||||
|
||||
|
||||
public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSessionManager sessionManager) {
|
||||
|
||||
public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
Assert.notNull(request, "'request' is required.");
|
||||
Assert.notNull(response, "'response' is required.");
|
||||
Assert.notNull(response, "'sessionManager' is required.");
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
|
||||
|
@ -59,4 +76,17 @@ public class DefaultWebServerExchange implements WebServerExchange {
|
|||
return this.attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> getSession() {
|
||||
if (this.sessionMono == null) {
|
||||
synchronized (this.createSessionLock) {
|
||||
if (this.sessionMono == null) {
|
||||
FluxProcessor<WebSession, WebSession> replay = Processors.replay(1);
|
||||
this.sessionMono = this.sessionManager.getSession(this).subscribeWith(replay).next();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.sessionMono;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ package org.springframework.web.server;
|
|||
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
|
||||
|
@ -30,18 +32,23 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
|
|||
public interface WebServerExchange {
|
||||
|
||||
/**
|
||||
* @return the current HTTP request
|
||||
* Return the current HTTP request.
|
||||
*/
|
||||
ServerHttpRequest getRequest();
|
||||
|
||||
/**
|
||||
* @return the current HTTP response
|
||||
* Return the current HTTP response.
|
||||
*/
|
||||
ServerHttpResponse getResponse();
|
||||
|
||||
/**
|
||||
* @return mutable map of request attributes for the current exchange
|
||||
* Return a mutable map of request attributes for the current exchange.
|
||||
*/
|
||||
Map<String, Object> getAttributes();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Mono<WebSession> getSession();
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2002-2015 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.web.server;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Main contract for using a server-side session that provides access to session
|
||||
* attributes across HTTP requests.
|
||||
*
|
||||
* <p>The creation of a {@code WebSession} instance does not automatically start
|
||||
* a session thus causing the session id to be sent to the client (typically via
|
||||
* a cookie). A session starts implicitly when session attributes are added.
|
||||
* A session may also be created explicitly via {@link #start()}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public interface WebSession {
|
||||
|
||||
/**
|
||||
* Return a unique session identifier.
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Return a map that holds session attributes.
|
||||
*/
|
||||
Map<String, Object> getAttributes();
|
||||
|
||||
/**
|
||||
* Force the creation of a session causing the session id to be sent when
|
||||
* {@link #save()} is called.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Whether a session with the client has been started explicitly via
|
||||
* {@link #start()} or implicitly by adding session attributes.
|
||||
* If "false" then the session id is not sent to the client and the
|
||||
* {@link #save()} method is essentially a no-op.
|
||||
*/
|
||||
boolean isStarted();
|
||||
|
||||
/**
|
||||
* Save the session persisting attributes (e.g. if stored remotely) and also
|
||||
* sending the session id to the client if the session is new.
|
||||
* <p>Note that a session must be started explicitly via {@link #start()} or
|
||||
* implicitly by adding attributes or otherwise this method has no effect.
|
||||
* @return {@code Mono} to indicate completion with success or error
|
||||
* <p>Typically this method should be automatically invoked just before the
|
||||
* response is committed so applications don't have to by default.
|
||||
*/
|
||||
Mono<Void> save();
|
||||
|
||||
/**
|
||||
* Return {@code true} if the session expired after {@link #getMaxIdleTime()
|
||||
* maxIdleTime} elapsed.
|
||||
* <p>Typically expiration checks should be automatically made when a session
|
||||
* is accessed, a new {@code WebSession} instance created if necessary, at
|
||||
* the start of request processing so that applications don't have to worry
|
||||
* about expired session by default.
|
||||
*/
|
||||
boolean isExpired();
|
||||
|
||||
/**
|
||||
* Return the time when the session was created.
|
||||
*/
|
||||
Instant getCreationTime();
|
||||
|
||||
/**
|
||||
* Return the last time of session access as a result of user activity such
|
||||
* as an HTTP request. Together with {@link #getMaxIdleTime()
|
||||
* maxIdleTimeInSeconds} this helps to determine when a session is
|
||||
* {@link #isExpired() expired}.
|
||||
*/
|
||||
Instant getLastAccessTime();
|
||||
|
||||
/**
|
||||
* Configure the max amount of time that may elapse after the
|
||||
* {@link #getLastAccessTime() lastAccessTime} before a session is considered
|
||||
* expired. A negative value indicates the session should not expire.
|
||||
*/
|
||||
void setMaxIdleTime(Duration maxIdleTime);
|
||||
|
||||
/**
|
||||
* Return the maximum time after the {@link #getLastAccessTime()
|
||||
* lastAccessTime} before a session expires. A negative time indicates the
|
||||
* session doesn't expire.
|
||||
*/
|
||||
Duration getMaxIdleTime();
|
||||
|
||||
}
|
|
@ -23,6 +23,9 @@ import org.springframework.http.HttpStatus;
|
|||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
/**
|
||||
* Adapt {@link WebHandler} to {@link HttpHandler} also creating the
|
||||
|
@ -35,11 +38,31 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http
|
|||
private static Log logger = LogFactory.getLog(WebToHttpHandlerAdapter.class);
|
||||
|
||||
|
||||
private WebSessionManager sessionManager = new DefaultWebSessionManager();
|
||||
|
||||
|
||||
public WebToHttpHandlerAdapter(WebHandler delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param sessionManager
|
||||
*/
|
||||
public void setSessionManager(WebSessionManager sessionManager) {
|
||||
Assert.notNull(sessionManager, "'sessionManager' must not be null.");
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link WebSessionManager}.
|
||||
*/
|
||||
public WebSessionManager getSessionManager() {
|
||||
return this.sessionManager;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
WebServerExchange exchange = createWebServerExchange(request, response);
|
||||
|
@ -55,7 +78,7 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http
|
|||
}
|
||||
|
||||
protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
return new DefaultWebServerExchange(request, response);
|
||||
return new DefaultWebServerExchange(request, response, this.sessionManager);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.List;
|
|||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
/**
|
||||
* Assist with building an
|
||||
|
@ -43,6 +44,8 @@ public class WebToHttpHandlerBuilder {
|
|||
|
||||
private final List<WebExceptionHandler> exceptionHandlers = new ArrayList<>();
|
||||
|
||||
private WebSessionManager sessionManager;
|
||||
|
||||
|
||||
private WebToHttpHandlerBuilder(WebHandler targetHandler) {
|
||||
Assert.notNull(targetHandler, "'targetHandler' must not be null");
|
||||
|
@ -68,17 +71,26 @@ public class WebToHttpHandlerBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public WebToHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) {
|
||||
this.sessionManager = sessionManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
public WebToHttpHandlerAdapter build() {
|
||||
WebHandler webHandler = this.targetHandler;
|
||||
WebHandler handler = this.targetHandler;
|
||||
if (!this.exceptionHandlers.isEmpty()) {
|
||||
WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()];
|
||||
webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array));
|
||||
handler = new ExceptionHandlingWebHandler(handler, this.exceptionHandlers.toArray(array));
|
||||
}
|
||||
if (!this.filters.isEmpty()) {
|
||||
WebFilter[] array = new WebFilter[this.filters.size()];
|
||||
webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array));
|
||||
handler = new FilteringWebHandler(handler, this.filters.toArray(array));
|
||||
}
|
||||
return new WebToHttpHandlerAdapter(webHandler);
|
||||
WebToHttpHandlerAdapter adapter = new WebToHttpHandlerAdapter(handler);
|
||||
if (this.sessionManager != null) {
|
||||
adapter.setSessionManager(this.sessionManager);
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Extend {@link WebSession} with management operations meant for internal use
|
||||
* for example by implementations of {@link WebSessionManager}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public interface ConfigurableWebSession extends WebSession {
|
||||
|
||||
/**
|
||||
* Update the last access time for user-related session activity.
|
||||
* @param time the time of access
|
||||
*/
|
||||
void setLastAccessTime(Instant time);
|
||||
|
||||
/**
|
||||
* Set the operation to invoke when {@link WebSession#save()} is invoked.
|
||||
* @param saveOperation the save operation
|
||||
*/
|
||||
void setSaveOperation(Supplier<Mono<Void>> saveOperation);
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
|
||||
/**
|
||||
* Cookie-based {@link WebSessionIdResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class CookieWebSessionIdResolver implements WebSessionIdResolver {
|
||||
|
||||
private String cookieName = "SESSION";
|
||||
|
||||
private Duration cookieMaxAge = Duration.ofSeconds(-1);
|
||||
|
||||
|
||||
/**
|
||||
* Set the name of the cookie to use for the session id.
|
||||
* <p>By default set to "SESSION".
|
||||
* @param cookieName the cookie name
|
||||
*/
|
||||
public void setCookieName(String cookieName) {
|
||||
Assert.hasText(cookieName, "'cookieName' must not be empty.");
|
||||
this.cookieName = cookieName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured cookie name.
|
||||
*/
|
||||
public String getCookieName() {
|
||||
return this.cookieName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the "Max-Age" attribute of the cookie that holds the
|
||||
* session id. For the range of values see {@link HttpCookie#getMaxAge()}.
|
||||
* <p>By default set to -1.
|
||||
* @param maxAge the maxAge duration value
|
||||
*/
|
||||
public void setCookieMaxAge(Duration maxAge) {
|
||||
this.cookieMaxAge = maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured "Max-Age" attribute value for the session cookie.
|
||||
*/
|
||||
public Duration getCookieMaxAge() {
|
||||
return this.cookieMaxAge;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Optional<String> resolveSessionId(WebServerExchange exchange) {
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
List<HttpCookie> cookies = headers.getCookies().get(getCookieName());
|
||||
return (CollectionUtils.isEmpty(cookies) ?
|
||||
Optional.empty() : Optional.of(cookies.get(0).getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSessionId(WebServerExchange exchange, String id) {
|
||||
Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0));
|
||||
HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build();
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
headers.getCookies().put(getCookieName(), Collections.singletonList(cookie));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class DefaultWebSession implements ConfigurableWebSession, Serializable {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private final Instant creationTime;
|
||||
|
||||
private volatile Instant lastAccessTime;
|
||||
|
||||
private volatile Duration maxIdleTime;
|
||||
|
||||
private AtomicReference<State> state = new AtomicReference<>();
|
||||
|
||||
private volatile transient Supplier<Mono<Void>> saveOperation;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor to create a new session.
|
||||
* @param id the session id
|
||||
* @param clock for access to current time
|
||||
*/
|
||||
public DefaultWebSession(String id, Clock clock) {
|
||||
Assert.notNull(id, "'id' is required.");
|
||||
Assert.notNull(clock, "'clock' is required.");
|
||||
this.id = id;
|
||||
this.clock = clock;
|
||||
this.attributes = new ConcurrentHashMap<>();
|
||||
this.creationTime = Instant.now(clock);
|
||||
this.lastAccessTime = this.creationTime;
|
||||
this.maxIdleTime = Duration.ofMinutes(30);
|
||||
this.state.set(State.NEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to load existing session.
|
||||
* @param id the session id
|
||||
* @param attributes the attributes of the session
|
||||
* @param clock for access to current time
|
||||
* @param creationTime the creation time
|
||||
* @param lastAccessTime the last access time
|
||||
* @param maxIdleTime the configured maximum session idle time
|
||||
*/
|
||||
public DefaultWebSession(String id, Map<String, Object> attributes, Clock clock,
|
||||
Instant creationTime, Instant lastAccessTime, Duration maxIdleTime) {
|
||||
|
||||
Assert.notNull(id, "'id' is required.");
|
||||
Assert.notNull(clock, "'clock' is required.");
|
||||
this.id = id;
|
||||
this.attributes = new ConcurrentHashMap<>(attributes);
|
||||
this.clock = clock;
|
||||
this.creationTime = creationTime;
|
||||
this.lastAccessTime = lastAccessTime;
|
||||
this.maxIdleTime = maxIdleTime;
|
||||
this.state.set(State.STARTED);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.creationTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessTime(Instant lastAccessTime) {
|
||||
this.lastAccessTime = lastAccessTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessTime() {
|
||||
return this.lastAccessTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>By default this is set to 30 minutes.
|
||||
* @param maxIdleTime the max idle time
|
||||
*/
|
||||
@Override
|
||||
public void setMaxIdleTime(Duration maxIdleTime) {
|
||||
this.maxIdleTime = maxIdleTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxIdleTime() {
|
||||
return this.maxIdleTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSaveOperation(Supplier<Mono<Void>> saveOperation) {
|
||||
Assert.notNull(saveOperation, "'saveOperation' is required.");
|
||||
this.saveOperation = saveOperation;
|
||||
}
|
||||
|
||||
protected Supplier<Mono<Void>> getSaveOperation() {
|
||||
return this.saveOperation;
|
||||
}
|
||||
|
||||
|
||||
@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<Void> save() {
|
||||
return this.saveOperation.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return (isStarted() && !this.maxIdleTime.isNegative() &&
|
||||
Instant.now(this.clock).minus(this.maxIdleTime).isAfter(this.lastAccessTime));
|
||||
}
|
||||
|
||||
|
||||
private enum State { NEW, STARTED }
|
||||
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebSessionManager} with a cookie-based web
|
||||
* session id resolution strategy and simple in-memory session persistence.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class DefaultWebSessionManager implements WebSessionManager {
|
||||
|
||||
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
|
||||
|
||||
private WebSessionStore sessionStore = new InMemoryWebSessionStore();
|
||||
|
||||
private Clock clock = Clock.systemDefaultZone();
|
||||
|
||||
|
||||
/**
|
||||
* Configure the session id resolution strategy to use.
|
||||
* <p>By default {@link CookieWebSessionIdResolver} is used.
|
||||
* @param sessionIdResolver the resolver
|
||||
*/
|
||||
public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) {
|
||||
Assert.notNull(sessionIdResolver, "'sessionIdResolver' is required.");
|
||||
this.sessionIdResolver = sessionIdResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link WebSessionIdResolver}.
|
||||
*/
|
||||
public WebSessionIdResolver getSessionIdResolver() {
|
||||
return this.sessionIdResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the session persistence strategy to use.
|
||||
* <p>By default {@link InMemoryWebSessionStore} is used.
|
||||
* @param sessionStore the persistence strategy
|
||||
*/
|
||||
public void setSessionStore(WebSessionStore sessionStore) {
|
||||
Assert.notNull(sessionStore, "'sessionStore' is required.");
|
||||
this.sessionStore = sessionStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link WebSessionStore}.
|
||||
*/
|
||||
public WebSessionStore getSessionStore() {
|
||||
return this.sessionStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link Clock} for access to current time. During tests you
|
||||
* may use {code Clock.offset(clock, Duration.ofMinutes(-31))} to set the
|
||||
* clock back for example to test changes after sessions expire.
|
||||
* <p>By default {@link Clock#systemDefaultZone()} is used.
|
||||
* @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 access to current time.
|
||||
*/
|
||||
public Clock getClock() {
|
||||
return this.clock;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> getSession(WebServerExchange exchange) {
|
||||
return Mono.fromCallable(() -> getSessionIdResolver().resolveSessionId(exchange))
|
||||
.where(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.then(this.sessionStore::retrieveSession)
|
||||
.then(session -> validateSession(exchange, session))
|
||||
.otherwiseIfEmpty(createSession(exchange))
|
||||
.map(session -> extendSession(exchange, session));
|
||||
}
|
||||
|
||||
protected Mono<WebSession> validateSession(WebServerExchange exchange, WebSession session) {
|
||||
if (session.isExpired()) {
|
||||
this.sessionIdResolver.setSessionId(exchange, "");
|
||||
return this.sessionStore.removeSession(session.getId()).after(Mono::empty);
|
||||
}
|
||||
else {
|
||||
return Mono.just(session);
|
||||
}
|
||||
}
|
||||
|
||||
protected Mono<WebSession> createSession(WebServerExchange exchange) {
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
WebSession session = new DefaultWebSession(sessionId, getClock());
|
||||
return Mono.just(session);
|
||||
}
|
||||
|
||||
protected WebSession extendSession(WebServerExchange exchange, WebSession session) {
|
||||
if (session instanceof ConfigurableWebSession) {
|
||||
ConfigurableWebSession managed = (ConfigurableWebSession) session;
|
||||
managed.setSaveOperation(() -> saveSession(exchange, session));
|
||||
managed.setLastAccessTime(Instant.now(getClock()));
|
||||
}
|
||||
exchange.getResponse().beforeCommit(session::save);
|
||||
return session;
|
||||
}
|
||||
|
||||
protected Mono<Void> saveSession(WebServerExchange exchange, WebSession session) {
|
||||
|
||||
Assert.isTrue(!session.isExpired(), "Sessions are checked for expiration and have their " +
|
||||
"access time updated when first accessed during request processing. " +
|
||||
"However this session is expired meaning that maxIdleTime elapsed " +
|
||||
"since then and before the call to session.save().");
|
||||
|
||||
if (!session.isStarted()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// Force explicit start
|
||||
session.start();
|
||||
|
||||
Optional<String> requestedId = getSessionIdResolver().resolveSessionId(exchange);
|
||||
if (!requestedId.isPresent() || !session.getId().equals(requestedId.get())) {
|
||||
this.sessionIdResolver.setSessionId(exchange, session.getId());
|
||||
}
|
||||
return this.sessionStore.storeSession(session);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Simple Map-based storage for {@link WebSession} instances.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class InMemoryWebSessionStore implements WebSessionStore {
|
||||
|
||||
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> storeSession(WebSession session) {
|
||||
this.sessions.put(session.getId(), session);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> retrieveSession(String id) {
|
||||
return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeSession(String id) {
|
||||
this.sessions.remove(id);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
|
||||
/**
|
||||
* Contract for session id resolution strategies. Allows for session id
|
||||
* resolution through the request and for sending the session id to the
|
||||
* client through the response.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @see CookieWebSessionIdResolver
|
||||
*/
|
||||
public interface WebSessionIdResolver {
|
||||
|
||||
/**
|
||||
* Resolve the session id associated with the request.
|
||||
* @param exchange the current exchange
|
||||
* @return the session id if present
|
||||
*/
|
||||
Optional<String> resolveSessionId(WebServerExchange exchange);
|
||||
|
||||
/**
|
||||
* Send the given session id to the client or if the session id is "null"
|
||||
* instruct the client to end the current session.
|
||||
* @param exchange the current exchange
|
||||
* @param sessionId the session id
|
||||
*/
|
||||
void setSessionId(WebServerExchange exchange, String sessionId);
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Main contract abstracting support for access to {@link WebSession} instances
|
||||
* 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
|
||||
* @see WebSessionIdResolver
|
||||
* @see WebSessionStore
|
||||
*/
|
||||
public interface WebSessionManager {
|
||||
|
||||
/**
|
||||
* Return the {@link WebSession} for the given exchange. Always guaranteed
|
||||
* 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
|
||||
* specify one or because the underlying session had expired.
|
||||
* @param exchange the current exchange
|
||||
* @return {@code Mono} for async access to the session
|
||||
*/
|
||||
Mono<WebSession> getSession(WebServerExchange exchange);
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Strategy for {@link WebSession} persistence.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3
|
||||
*/
|
||||
public interface WebSessionStore {
|
||||
|
||||
/**
|
||||
* Store the given session.
|
||||
* @param session the session to store
|
||||
* @return {@code Mono} for completion notification
|
||||
*/
|
||||
Mono<Void> storeSession(WebSession session);
|
||||
|
||||
/**
|
||||
* Load the session for the given session id.
|
||||
* @param sessionId the session to load
|
||||
* @return {@code Mono} for async access to the loaded session
|
||||
*/
|
||||
Mono<WebSession> retrieveSession(String sessionId);
|
||||
|
||||
/**
|
||||
* Remove the session with the given id.
|
||||
* @param sessionId the session to remove
|
||||
* @return {@code Mono} for completion notification
|
||||
*/
|
||||
Mono<Void> removeSession(String sessionId);
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Support for a user session.
|
||||
*/
|
||||
package org.springframework.web.server.session;
|
|
@ -57,9 +57,11 @@ import org.springframework.web.server.WebFilter;
|
|||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.startsWith;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Test the effect of exceptions at different stages of request processing by
|
||||
|
@ -91,9 +93,11 @@ public class DispatcherHandlerErrorTests {
|
|||
this.dispatcherHandler = new DispatcherHandler();
|
||||
this.dispatcherHandler.setApplicationContext(appContext);
|
||||
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/"));
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultWebServerExchange(this.request, this.response);
|
||||
this.exchange = new DefaultWebServerExchange(this.request, this.response, sessionManager);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -31,10 +31,12 @@ import org.springframework.http.server.reactive.MockServerHttpResponse;
|
|||
import org.springframework.web.ResponseStatusException;
|
||||
import org.springframework.web.server.DefaultWebServerExchange;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
|
@ -52,8 +54,9 @@ public class ResponseStatusExceptionHandlerTests {
|
|||
public void setUp() throws Exception {
|
||||
this.handler = new ResponseStatusExceptionHandler();
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultWebServerExchange(request, this.response);
|
||||
this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.springframework.web.reactive.HandlerResult;
|
|||
import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver;
|
||||
import org.springframework.web.server.DefaultWebServerExchange;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Matchers.any;
|
||||
|
@ -56,8 +57,9 @@ public class InvocableHandlerMethodTests {
|
|||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
this.request = mock(ServerHttpRequest.class);
|
||||
this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class));
|
||||
this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class), sessionManager);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -36,10 +36,12 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
|||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.server.DefaultWebServerExchange;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
|
@ -62,7 +64,9 @@ public class RequestMappingHandlerMappingTests {
|
|||
@Test
|
||||
public void path() throws Exception {
|
||||
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo"));
|
||||
WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse());
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager);
|
||||
Publisher<?> handlerPublisher = this.mapping.getHandler(exchange);
|
||||
HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher);
|
||||
assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod());
|
||||
|
@ -71,13 +75,15 @@ public class RequestMappingHandlerMappingTests {
|
|||
@Test
|
||||
public void method() throws Exception {
|
||||
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo"));
|
||||
WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse());
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager);
|
||||
Publisher<?> handlerPublisher = this.mapping.getHandler(exchange);
|
||||
HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher);
|
||||
assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod());
|
||||
|
||||
request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo"));
|
||||
exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse());
|
||||
exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse(), sessionManager);
|
||||
handlerPublisher = this.mapping.getHandler(exchange);
|
||||
handlerMethod = toHandlerMethod(handlerPublisher);
|
||||
assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod());
|
||||
|
|
|
@ -26,8 +26,10 @@ import org.springframework.http.HttpMethod;
|
|||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
|
@ -45,9 +47,10 @@ public class ExceptionHandlingHttpHandlerTests {
|
|||
@Before
|
||||
public void setUp() throws Exception {
|
||||
URI uri = new URI("http://localhost:8080");
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, uri);
|
||||
this.response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultWebServerExchange(request, this.response);
|
||||
this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager);
|
||||
this.targetHandler = new StubWebHandler(new IllegalStateException("boo"));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.web.server.DefaultWebServerExchange;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class DefaultWebSessionManagerTests {
|
||||
|
||||
private DefaultWebSessionManager manager;
|
||||
|
||||
private TestWebSessionIdResolver idResolver;
|
||||
|
||||
private DefaultWebServerExchange exchange;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.idResolver = new TestWebSessionIdResolver();
|
||||
this.manager = new DefaultWebSessionManager();
|
||||
this.manager.setSessionIdResolver(this.idResolver);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultWebServerExchange(request, response, this.manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void getSessionPassive() throws Exception {
|
||||
this.idResolver.setIdToResolve(Optional.empty());
|
||||
WebSession session = this.manager.getSession(this.exchange).get();
|
||||
|
||||
assertNotNull(session);
|
||||
assertFalse(session.isStarted());
|
||||
assertFalse(session.isExpired());
|
||||
|
||||
session.save();
|
||||
|
||||
assertFalse(this.idResolver.getId().isPresent());
|
||||
assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionForceCreate() throws Exception {
|
||||
this.idResolver.setIdToResolve(Optional.empty());
|
||||
WebSession session = this.manager.getSession(this.exchange).get();
|
||||
session.start();
|
||||
session.save();
|
||||
|
||||
String id = session.getId();
|
||||
assertTrue(this.idResolver.getId().isPresent());
|
||||
assertEquals(id, this.idResolver.getId().get());
|
||||
assertSame(session, this.manager.getSessionStore().retrieveSession(id).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionAddAttribute() throws Exception {
|
||||
this.idResolver.setIdToResolve(Optional.empty());
|
||||
WebSession session = this.manager.getSession(this.exchange).get();
|
||||
session.getAttributes().put("foo", "bar");
|
||||
session.save();
|
||||
|
||||
assertTrue(this.idResolver.getId().isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionExisting() throws Exception {
|
||||
DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone());
|
||||
this.manager.getSessionStore().storeSession(existing);
|
||||
|
||||
this.idResolver.setIdToResolve(Optional.of("1"));
|
||||
WebSession actual = this.manager.getSession(this.exchange).get();
|
||||
assertSame(existing, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionExistingExpired() throws Exception {
|
||||
Clock clock = Clock.systemDefaultZone();
|
||||
DefaultWebSession existing = new DefaultWebSession("1", clock);
|
||||
existing.start();
|
||||
existing.setLastAccessTime(Instant.now(clock).minus(Duration.ofMinutes(31)));
|
||||
this.manager.getSessionStore().storeSession(existing);
|
||||
|
||||
this.idResolver.setIdToResolve(Optional.of("1"));
|
||||
WebSession actual = this.manager.getSession(this.exchange).get();
|
||||
assertNotSame(existing, actual);
|
||||
}
|
||||
|
||||
|
||||
private static class TestWebSessionIdResolver implements WebSessionIdResolver {
|
||||
|
||||
private Optional<String> idToResolve = Optional.empty();
|
||||
|
||||
private Optional<String> id = Optional.empty();
|
||||
|
||||
|
||||
public void setIdToResolve(Optional<String> idToResolve) {
|
||||
this.idToResolve = idToResolve;
|
||||
}
|
||||
|
||||
public Optional<String> getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> resolveSessionId(WebServerExchange exchange) {
|
||||
return this.idToResolve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSessionId(WebServerExchange exchange, Optional<String> sessionId) {
|
||||
this.id = sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright 2002-2016 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.web.server.session;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.WebServerExchange;
|
||||
import org.springframework.web.server.WebToHttpHandlerBuilder;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTests {
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private DefaultWebSessionManager sessionManager;
|
||||
|
||||
private TestWebHandler handler;
|
||||
|
||||
|
||||
@Override
|
||||
public void setup() throws Exception {
|
||||
super.setup();
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
protected URI createUri(String pathAndQuery) throws URISyntaxException {
|
||||
boolean prefix = !StringUtils.hasText(pathAndQuery) || !pathAndQuery.startsWith("/");
|
||||
pathAndQuery = (prefix ? "/" + pathAndQuery : pathAndQuery);
|
||||
return new URI("http://localhost:" + port + pathAndQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpHandler createHttpHandler() {
|
||||
this.sessionManager = new DefaultWebSessionManager();
|
||||
this.handler = new TestWebHandler();
|
||||
return WebToHttpHandlerBuilder.webHandler(this.handler).sessionManager(this.sessionManager).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSession() throws Exception {
|
||||
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);
|
||||
assertEquals(1, this.handler.getCount());
|
||||
|
||||
request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build();
|
||||
response = this.restTemplate.exchange(request, Void.class);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNull(response.getHeaders().get("Set-Cookie"));
|
||||
assertEquals(2, this.handler.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expiredSession() throws Exception {
|
||||
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);
|
||||
assertEquals(1, this.handler.getCount());
|
||||
|
||||
// Set clock back 31 minutes
|
||||
Clock clock = this.sessionManager.getClock();
|
||||
this.sessionManager.setClock(Clock.offset(clock, Duration.ofMinutes(-31)));
|
||||
|
||||
// Access again to update lastAccessTime
|
||||
request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build();
|
||||
response = this.restTemplate.exchange(request, Void.class);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNull(response.getHeaders().get("Set-Cookie"));
|
||||
assertEquals(2, this.handler.getCount());
|
||||
|
||||
// Now it should be expired
|
||||
request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build();
|
||||
response = this.restTemplate.exchange(request, Void.class);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
id = extractSessionId(response.getHeaders());
|
||||
assertNotNull("Expected new session id", id);
|
||||
assertEquals("Expected new session attribute", 1, this.handler.getCount());
|
||||
}
|
||||
|
||||
|
||||
// No client side HttpCookie support yet
|
||||
|
||||
private String extractSessionId(HttpHeaders headers) {
|
||||
List<String> headerValues = headers.get("Set-Cookie");
|
||||
assertNotNull(headerValues);
|
||||
assertEquals(1, headerValues.size());
|
||||
|
||||
List<String> data = new ArrayList<>();
|
||||
for (String s : headerValues.get(0).split(";")){
|
||||
if (s.startsWith("SESSION=")) {
|
||||
return s.substring("SESSION=".length());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class TestWebHandler implements WebHandler {
|
||||
|
||||
private AtomicInteger currentValue = new AtomicInteger();
|
||||
|
||||
|
||||
public int getCount() {
|
||||
return this.currentValue.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebServerExchange exchange) {
|
||||
return exchange.getSession().map(session -> {
|
||||
Map<String, Object> map = session.getAttributes();
|
||||
int value = (map.get("counter") != null ? (int) map.get("counter") : 0);
|
||||
value++;
|
||||
map.put("counter", value);
|
||||
this.currentValue.set(value);
|
||||
return session;
|
||||
}).after();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue