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:
Rossen Stoyanchev 2016-01-13 15:41:52 -05:00
parent 3744549a3e
commit 407d11a58a
28 changed files with 1268 additions and 71 deletions

View File

@ -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.

View File

@ -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;
}
/**

View File

@ -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();
}
}
}

View File

@ -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());

View File

@ -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()));
}
}

View File

@ -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());

View File

@ -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());

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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 }
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,4 @@
/**
* Support for a user session.
*/
package org.springframework.web.server.session;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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());

View File

@ -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"));
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}