Browse Source
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.pull/1111/head
28 changed files with 1268 additions and 71 deletions
@ -0,0 +1,109 @@
@@ -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(); |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
/** |
||||
* Support for a user session. |
||||
*/ |
||||
package org.springframework.web.server.session; |
||||
@ -0,0 +1,154 @@
@@ -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 @@
@@ -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