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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.time.Instant; |
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
/** |
||||||
|
* Extend {@link WebSession} with management operations meant for internal use |
||||||
|
* for example by implementations of {@link WebSessionManager}. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public interface ConfigurableWebSession extends WebSession { |
||||||
|
|
||||||
|
/** |
||||||
|
* Update the last access time for user-related session activity. |
||||||
|
* @param time the time of access |
||||||
|
*/ |
||||||
|
void setLastAccessTime(Instant time); |
||||||
|
|
||||||
|
/** |
||||||
|
* Set the operation to invoke when {@link WebSession#save()} is invoked. |
||||||
|
* @param saveOperation the save operation |
||||||
|
*/ |
||||||
|
void setSaveOperation(Supplier<Mono<Void>> saveOperation); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import org.springframework.http.HttpCookie; |
||||||
|
import org.springframework.http.HttpHeaders; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.web.server.WebServerExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* Cookie-based {@link WebSessionIdResolver}. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class CookieWebSessionIdResolver implements WebSessionIdResolver { |
||||||
|
|
||||||
|
private String cookieName = "SESSION"; |
||||||
|
|
||||||
|
private Duration cookieMaxAge = Duration.ofSeconds(-1); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Set the name of the cookie to use for the session id. |
||||||
|
* <p>By default set to "SESSION". |
||||||
|
* @param cookieName the cookie name |
||||||
|
*/ |
||||||
|
public void setCookieName(String cookieName) { |
||||||
|
Assert.hasText(cookieName, "'cookieName' must not be empty."); |
||||||
|
this.cookieName = cookieName; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured cookie name. |
||||||
|
*/ |
||||||
|
public String getCookieName() { |
||||||
|
return this.cookieName; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Set the value for the "Max-Age" attribute of the cookie that holds the |
||||||
|
* session id. For the range of values see {@link HttpCookie#getMaxAge()}. |
||||||
|
* <p>By default set to -1. |
||||||
|
* @param maxAge the maxAge duration value |
||||||
|
*/ |
||||||
|
public void setCookieMaxAge(Duration maxAge) { |
||||||
|
this.cookieMaxAge = maxAge; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured "Max-Age" attribute value for the session cookie. |
||||||
|
*/ |
||||||
|
public Duration getCookieMaxAge() { |
||||||
|
return this.cookieMaxAge; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<String> resolveSessionId(WebServerExchange exchange) { |
||||||
|
HttpHeaders headers = exchange.getRequest().getHeaders(); |
||||||
|
List<HttpCookie> cookies = headers.getCookies().get(getCookieName()); |
||||||
|
return (CollectionUtils.isEmpty(cookies) ? |
||||||
|
Optional.empty() : Optional.of(cookies.get(0).getValue())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setSessionId(WebServerExchange exchange, String id) { |
||||||
|
Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0)); |
||||||
|
HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build(); |
||||||
|
HttpHeaders headers = exchange.getResponse().getHeaders(); |
||||||
|
headers.getCookies().put(getCookieName(), Collections.singletonList(cookie)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,169 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.io.Serializable; |
||||||
|
import java.time.Clock; |
||||||
|
import java.time.Duration; |
||||||
|
import java.time.Instant; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
import java.util.concurrent.atomic.AtomicReference; |
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class DefaultWebSession implements ConfigurableWebSession, Serializable { |
||||||
|
|
||||||
|
private final String id; |
||||||
|
|
||||||
|
private final Map<String, Object> attributes; |
||||||
|
|
||||||
|
private final Clock clock; |
||||||
|
|
||||||
|
private final Instant creationTime; |
||||||
|
|
||||||
|
private volatile Instant lastAccessTime; |
||||||
|
|
||||||
|
private volatile Duration maxIdleTime; |
||||||
|
|
||||||
|
private AtomicReference<State> state = new AtomicReference<>(); |
||||||
|
|
||||||
|
private volatile transient Supplier<Mono<Void>> saveOperation; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor to create a new session. |
||||||
|
* @param id the session id |
||||||
|
* @param clock for access to current time |
||||||
|
*/ |
||||||
|
public DefaultWebSession(String id, Clock clock) { |
||||||
|
Assert.notNull(id, "'id' is required."); |
||||||
|
Assert.notNull(clock, "'clock' is required."); |
||||||
|
this.id = id; |
||||||
|
this.clock = clock; |
||||||
|
this.attributes = new ConcurrentHashMap<>(); |
||||||
|
this.creationTime = Instant.now(clock); |
||||||
|
this.lastAccessTime = this.creationTime; |
||||||
|
this.maxIdleTime = Duration.ofMinutes(30); |
||||||
|
this.state.set(State.NEW); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor to load existing session. |
||||||
|
* @param id the session id |
||||||
|
* @param attributes the attributes of the session |
||||||
|
* @param clock for access to current time |
||||||
|
* @param creationTime the creation time |
||||||
|
* @param lastAccessTime the last access time |
||||||
|
* @param maxIdleTime the configured maximum session idle time |
||||||
|
*/ |
||||||
|
public DefaultWebSession(String id, Map<String, Object> attributes, Clock clock, |
||||||
|
Instant creationTime, Instant lastAccessTime, Duration maxIdleTime) { |
||||||
|
|
||||||
|
Assert.notNull(id, "'id' is required."); |
||||||
|
Assert.notNull(clock, "'clock' is required."); |
||||||
|
this.id = id; |
||||||
|
this.attributes = new ConcurrentHashMap<>(attributes); |
||||||
|
this.clock = clock; |
||||||
|
this.creationTime = creationTime; |
||||||
|
this.lastAccessTime = lastAccessTime; |
||||||
|
this.maxIdleTime = maxIdleTime; |
||||||
|
this.state.set(State.STARTED); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public String getId() { |
||||||
|
return this.id; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, Object> getAttributes() { |
||||||
|
return this.attributes; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Instant getCreationTime() { |
||||||
|
return this.creationTime; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setLastAccessTime(Instant lastAccessTime) { |
||||||
|
this.lastAccessTime = lastAccessTime; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Instant getLastAccessTime() { |
||||||
|
return this.lastAccessTime; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* <p>By default this is set to 30 minutes. |
||||||
|
* @param maxIdleTime the max idle time |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public void setMaxIdleTime(Duration maxIdleTime) { |
||||||
|
this.maxIdleTime = maxIdleTime; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Duration getMaxIdleTime() { |
||||||
|
return this.maxIdleTime; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setSaveOperation(Supplier<Mono<Void>> saveOperation) { |
||||||
|
Assert.notNull(saveOperation, "'saveOperation' is required."); |
||||||
|
this.saveOperation = saveOperation; |
||||||
|
} |
||||||
|
|
||||||
|
protected Supplier<Mono<Void>> getSaveOperation() { |
||||||
|
return this.saveOperation; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void start() { |
||||||
|
this.state.compareAndSet(State.NEW, State.STARTED); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isStarted() { |
||||||
|
State value = this.state.get(); |
||||||
|
return (State.STARTED.equals(value) || (State.NEW.equals(value) && !getAttributes().isEmpty())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> save() { |
||||||
|
return this.saveOperation.get(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isExpired() { |
||||||
|
return (isStarted() && !this.maxIdleTime.isNegative() && |
||||||
|
Instant.now(this.clock).minus(this.maxIdleTime).isAfter(this.lastAccessTime)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private enum State { NEW, STARTED } |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,157 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.time.Clock; |
||||||
|
import java.time.Instant; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.UUID; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.web.server.WebServerExchange; |
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Default implementation of {@link WebSessionManager} with a cookie-based web |
||||||
|
* session id resolution strategy and simple in-memory session persistence. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class DefaultWebSessionManager implements WebSessionManager { |
||||||
|
|
||||||
|
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver(); |
||||||
|
|
||||||
|
private WebSessionStore sessionStore = new InMemoryWebSessionStore(); |
||||||
|
|
||||||
|
private Clock clock = Clock.systemDefaultZone(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Configure the session id resolution strategy to use. |
||||||
|
* <p>By default {@link CookieWebSessionIdResolver} is used. |
||||||
|
* @param sessionIdResolver the resolver |
||||||
|
*/ |
||||||
|
public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) { |
||||||
|
Assert.notNull(sessionIdResolver, "'sessionIdResolver' is required."); |
||||||
|
this.sessionIdResolver = sessionIdResolver; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured {@link WebSessionIdResolver}. |
||||||
|
*/ |
||||||
|
public WebSessionIdResolver getSessionIdResolver() { |
||||||
|
return this.sessionIdResolver; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure the session persistence strategy to use. |
||||||
|
* <p>By default {@link InMemoryWebSessionStore} is used. |
||||||
|
* @param sessionStore the persistence strategy |
||||||
|
*/ |
||||||
|
public void setSessionStore(WebSessionStore sessionStore) { |
||||||
|
Assert.notNull(sessionStore, "'sessionStore' is required."); |
||||||
|
this.sessionStore = sessionStore; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured {@link WebSessionStore}. |
||||||
|
*/ |
||||||
|
public WebSessionStore getSessionStore() { |
||||||
|
return this.sessionStore; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure the {@link Clock} for access to current time. During tests you |
||||||
|
* may use {code Clock.offset(clock, Duration.ofMinutes(-31))} to set the |
||||||
|
* clock back for example to test changes after sessions expire. |
||||||
|
* <p>By default {@link Clock#systemDefaultZone()} is used. |
||||||
|
* @param clock the clock to use |
||||||
|
*/ |
||||||
|
public void setClock(Clock clock) { |
||||||
|
Assert.notNull(clock, "'clock' is required."); |
||||||
|
this.clock = clock; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured clock for access to current time. |
||||||
|
*/ |
||||||
|
public Clock getClock() { |
||||||
|
return this.clock; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<WebSession> getSession(WebServerExchange exchange) { |
||||||
|
return Mono.fromCallable(() -> getSessionIdResolver().resolveSessionId(exchange)) |
||||||
|
.where(Optional::isPresent) |
||||||
|
.map(Optional::get) |
||||||
|
.then(this.sessionStore::retrieveSession) |
||||||
|
.then(session -> validateSession(exchange, session)) |
||||||
|
.otherwiseIfEmpty(createSession(exchange)) |
||||||
|
.map(session -> extendSession(exchange, session)); |
||||||
|
} |
||||||
|
|
||||||
|
protected Mono<WebSession> validateSession(WebServerExchange exchange, WebSession session) { |
||||||
|
if (session.isExpired()) { |
||||||
|
this.sessionIdResolver.setSessionId(exchange, ""); |
||||||
|
return this.sessionStore.removeSession(session.getId()).after(Mono::empty); |
||||||
|
} |
||||||
|
else { |
||||||
|
return Mono.just(session); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected Mono<WebSession> createSession(WebServerExchange exchange) { |
||||||
|
String sessionId = UUID.randomUUID().toString(); |
||||||
|
WebSession session = new DefaultWebSession(sessionId, getClock()); |
||||||
|
return Mono.just(session); |
||||||
|
} |
||||||
|
|
||||||
|
protected WebSession extendSession(WebServerExchange exchange, WebSession session) { |
||||||
|
if (session instanceof ConfigurableWebSession) { |
||||||
|
ConfigurableWebSession managed = (ConfigurableWebSession) session; |
||||||
|
managed.setSaveOperation(() -> saveSession(exchange, session)); |
||||||
|
managed.setLastAccessTime(Instant.now(getClock())); |
||||||
|
} |
||||||
|
exchange.getResponse().beforeCommit(session::save); |
||||||
|
return session; |
||||||
|
} |
||||||
|
|
||||||
|
protected Mono<Void> saveSession(WebServerExchange exchange, WebSession session) { |
||||||
|
|
||||||
|
Assert.isTrue(!session.isExpired(), "Sessions are checked for expiration and have their " + |
||||||
|
"access time updated when first accessed during request processing. " + |
||||||
|
"However this session is expired meaning that maxIdleTime elapsed " + |
||||||
|
"since then and before the call to session.save()."); |
||||||
|
|
||||||
|
if (!session.isStarted()) { |
||||||
|
return Mono.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
// Force explicit start
|
||||||
|
session.start(); |
||||||
|
|
||||||
|
Optional<String> requestedId = getSessionIdResolver().resolveSessionId(exchange); |
||||||
|
if (!requestedId.isPresent() || !session.getId().equals(requestedId.get())) { |
||||||
|
this.sessionIdResolver.setSessionId(exchange, session.getId()); |
||||||
|
} |
||||||
|
return this.sessionStore.storeSession(session); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
/** |
||||||
|
* Simple Map-based storage for {@link WebSession} instances. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class InMemoryWebSessionStore implements WebSessionStore { |
||||||
|
|
||||||
|
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> storeSession(WebSession session) { |
||||||
|
this.sessions.put(session.getId(), session); |
||||||
|
return Mono.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<WebSession> retrieveSession(String id) { |
||||||
|
return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> removeSession(String id) { |
||||||
|
this.sessions.remove(id); |
||||||
|
return Mono.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import org.springframework.web.server.WebServerExchange; |
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Contract for session id resolution strategies. Allows for session id |
||||||
|
* resolution through the request and for sending the session id to the |
||||||
|
* client through the response. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @see CookieWebSessionIdResolver |
||||||
|
*/ |
||||||
|
public interface WebSessionIdResolver { |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve the session id associated with the request. |
||||||
|
* @param exchange the current exchange |
||||||
|
* @return the session id if present |
||||||
|
*/ |
||||||
|
Optional<String> resolveSessionId(WebServerExchange exchange); |
||||||
|
|
||||||
|
/** |
||||||
|
* Send the given session id to the client or if the session id is "null" |
||||||
|
* instruct the client to end the current session. |
||||||
|
* @param exchange the current exchange |
||||||
|
* @param sessionId the session id |
||||||
|
*/ |
||||||
|
void setSessionId(WebServerExchange exchange, String sessionId); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.web.server.WebServerExchange; |
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
/** |
||||||
|
* Main contract abstracting support for access to {@link WebSession} instances |
||||||
|
* associated with HTTP requests as well as the subsequent management such as |
||||||
|
* persistence and others. |
||||||
|
* |
||||||
|
* <p>The {@link DefaultWebSessionManager} implementation in turn delegates to |
||||||
|
* {@link WebSessionIdResolver} and {@link WebSessionStore} which abstract |
||||||
|
* underlying concerns related to the management of web sessions. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @see WebSessionIdResolver |
||||||
|
* @see WebSessionStore |
||||||
|
*/ |
||||||
|
public interface WebSessionManager { |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the {@link WebSession} for the given exchange. Always guaranteed |
||||||
|
* to return an instance either matching to the session id requested by the |
||||||
|
* client, or with a new session id either because the client did not |
||||||
|
* specify one or because the underlying session had expired. |
||||||
|
* @param exchange the current exchange |
||||||
|
* @return {@code Mono} for async access to the session |
||||||
|
*/ |
||||||
|
Mono<WebSession> getSession(WebServerExchange exchange); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
/** |
||||||
|
* Strategy for {@link WebSession} persistence. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
public interface WebSessionStore { |
||||||
|
|
||||||
|
/** |
||||||
|
* Store the given session. |
||||||
|
* @param session the session to store |
||||||
|
* @return {@code Mono} for completion notification |
||||||
|
*/ |
||||||
|
Mono<Void> storeSession(WebSession session); |
||||||
|
|
||||||
|
/** |
||||||
|
* Load the session for the given session id. |
||||||
|
* @param sessionId the session to load |
||||||
|
* @return {@code Mono} for async access to the loaded session |
||||||
|
*/ |
||||||
|
Mono<WebSession> retrieveSession(String sessionId); |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove the session with the given id. |
||||||
|
* @param sessionId the session to remove |
||||||
|
* @return {@code Mono} for completion notification |
||||||
|
*/ |
||||||
|
Mono<Void> removeSession(String sessionId); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
/** |
||||||
|
* Support for a user session. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
@ -0,0 +1,154 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
import java.time.Clock; |
||||||
|
import java.time.Duration; |
||||||
|
import java.time.Instant; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
import org.springframework.http.server.reactive.MockServerHttpRequest; |
||||||
|
import org.springframework.http.server.reactive.MockServerHttpResponse; |
||||||
|
import org.springframework.web.server.DefaultWebServerExchange; |
||||||
|
import org.springframework.web.server.WebServerExchange; |
||||||
|
import org.springframework.web.server.WebSession; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertFalse; |
||||||
|
import static org.junit.Assert.assertNotNull; |
||||||
|
import static org.junit.Assert.assertNotSame; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
import static org.junit.Assert.assertSame; |
||||||
|
import static org.junit.Assert.assertTrue; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class DefaultWebSessionManagerTests { |
||||||
|
|
||||||
|
private DefaultWebSessionManager manager; |
||||||
|
|
||||||
|
private TestWebSessionIdResolver idResolver; |
||||||
|
|
||||||
|
private DefaultWebServerExchange exchange; |
||||||
|
|
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() throws Exception { |
||||||
|
this.idResolver = new TestWebSessionIdResolver(); |
||||||
|
this.manager = new DefaultWebSessionManager(); |
||||||
|
this.manager.setSessionIdResolver(this.idResolver); |
||||||
|
|
||||||
|
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); |
||||||
|
MockServerHttpResponse response = new MockServerHttpResponse(); |
||||||
|
this.exchange = new DefaultWebServerExchange(request, response, this.manager); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void getSessionPassive() throws Exception { |
||||||
|
this.idResolver.setIdToResolve(Optional.empty()); |
||||||
|
WebSession session = this.manager.getSession(this.exchange).get(); |
||||||
|
|
||||||
|
assertNotNull(session); |
||||||
|
assertFalse(session.isStarted()); |
||||||
|
assertFalse(session.isExpired()); |
||||||
|
|
||||||
|
session.save(); |
||||||
|
|
||||||
|
assertFalse(this.idResolver.getId().isPresent()); |
||||||
|
assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void getSessionForceCreate() throws Exception { |
||||||
|
this.idResolver.setIdToResolve(Optional.empty()); |
||||||
|
WebSession session = this.manager.getSession(this.exchange).get(); |
||||||
|
session.start(); |
||||||
|
session.save(); |
||||||
|
|
||||||
|
String id = session.getId(); |
||||||
|
assertTrue(this.idResolver.getId().isPresent()); |
||||||
|
assertEquals(id, this.idResolver.getId().get()); |
||||||
|
assertSame(session, this.manager.getSessionStore().retrieveSession(id).get()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void getSessionAddAttribute() throws Exception { |
||||||
|
this.idResolver.setIdToResolve(Optional.empty()); |
||||||
|
WebSession session = this.manager.getSession(this.exchange).get(); |
||||||
|
session.getAttributes().put("foo", "bar"); |
||||||
|
session.save(); |
||||||
|
|
||||||
|
assertTrue(this.idResolver.getId().isPresent()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void getSessionExisting() throws Exception { |
||||||
|
DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone()); |
||||||
|
this.manager.getSessionStore().storeSession(existing); |
||||||
|
|
||||||
|
this.idResolver.setIdToResolve(Optional.of("1")); |
||||||
|
WebSession actual = this.manager.getSession(this.exchange).get(); |
||||||
|
assertSame(existing, actual); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void getSessionExistingExpired() throws Exception { |
||||||
|
Clock clock = Clock.systemDefaultZone(); |
||||||
|
DefaultWebSession existing = new DefaultWebSession("1", clock); |
||||||
|
existing.start(); |
||||||
|
existing.setLastAccessTime(Instant.now(clock).minus(Duration.ofMinutes(31))); |
||||||
|
this.manager.getSessionStore().storeSession(existing); |
||||||
|
|
||||||
|
this.idResolver.setIdToResolve(Optional.of("1")); |
||||||
|
WebSession actual = this.manager.getSession(this.exchange).get(); |
||||||
|
assertNotSame(existing, actual); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class TestWebSessionIdResolver implements WebSessionIdResolver { |
||||||
|
|
||||||
|
private Optional<String> idToResolve = Optional.empty(); |
||||||
|
|
||||||
|
private Optional<String> id = Optional.empty(); |
||||||
|
|
||||||
|
|
||||||
|
public void setIdToResolve(Optional<String> idToResolve) { |
||||||
|
this.idToResolve = idToResolve; |
||||||
|
} |
||||||
|
|
||||||
|
public Optional<String> getId() { |
||||||
|
return this.id; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<String> resolveSessionId(WebServerExchange exchange) { |
||||||
|
return this.idToResolve; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setSessionId(WebServerExchange exchange, Optional<String> sessionId) { |
||||||
|
this.id = sessionId; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,167 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2016 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.server.session; |
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.time.Clock; |
||||||
|
import java.time.Duration; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
import org.springframework.http.RequestEntity; |
||||||
|
import org.springframework.http.ResponseEntity; |
||||||
|
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; |
||||||
|
import org.springframework.http.server.reactive.HttpHandler; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.web.client.RestTemplate; |
||||||
|
import org.springframework.web.server.WebHandler; |
||||||
|
import org.springframework.web.server.WebServerExchange; |
||||||
|
import org.springframework.web.server.WebToHttpHandlerBuilder; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertNotNull; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTests { |
||||||
|
|
||||||
|
private RestTemplate restTemplate; |
||||||
|
|
||||||
|
private DefaultWebSessionManager sessionManager; |
||||||
|
|
||||||
|
private TestWebHandler handler; |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void setup() throws Exception { |
||||||
|
super.setup(); |
||||||
|
this.restTemplate = new RestTemplate(); |
||||||
|
} |
||||||
|
|
||||||
|
protected URI createUri(String pathAndQuery) throws URISyntaxException { |
||||||
|
boolean prefix = !StringUtils.hasText(pathAndQuery) || !pathAndQuery.startsWith("/"); |
||||||
|
pathAndQuery = (prefix ? "/" + pathAndQuery : pathAndQuery); |
||||||
|
return new URI("http://localhost:" + port + pathAndQuery); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected HttpHandler createHttpHandler() { |
||||||
|
this.sessionManager = new DefaultWebSessionManager(); |
||||||
|
this.handler = new TestWebHandler(); |
||||||
|
return WebToHttpHandlerBuilder.webHandler(this.handler).sessionManager(this.sessionManager).build(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void createSession() throws Exception { |
||||||
|
RequestEntity<Void> request = RequestEntity.get(createUri("/")).build(); |
||||||
|
ResponseEntity<Void> response = this.restTemplate.exchange(request, Void.class); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode()); |
||||||
|
String id = extractSessionId(response.getHeaders()); |
||||||
|
assertNotNull(id); |
||||||
|
assertEquals(1, this.handler.getCount()); |
||||||
|
|
||||||
|
request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); |
||||||
|
response = this.restTemplate.exchange(request, Void.class); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode()); |
||||||
|
assertNull(response.getHeaders().get("Set-Cookie")); |
||||||
|
assertEquals(2, this.handler.getCount()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void expiredSession() throws Exception { |
||||||
|
RequestEntity<Void> request = RequestEntity.get(createUri("/")).build(); |
||||||
|
ResponseEntity<Void> response = this.restTemplate.exchange(request, Void.class); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode()); |
||||||
|
String id = extractSessionId(response.getHeaders()); |
||||||
|
assertNotNull(id); |
||||||
|
assertEquals(1, this.handler.getCount()); |
||||||
|
|
||||||
|
// Set clock back 31 minutes
|
||||||
|
Clock clock = this.sessionManager.getClock(); |
||||||
|
this.sessionManager.setClock(Clock.offset(clock, Duration.ofMinutes(-31))); |
||||||
|
|
||||||
|
// Access again to update lastAccessTime
|
||||||
|
request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); |
||||||
|
response = this.restTemplate.exchange(request, Void.class); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode()); |
||||||
|
assertNull(response.getHeaders().get("Set-Cookie")); |
||||||
|
assertEquals(2, this.handler.getCount()); |
||||||
|
|
||||||
|
// Now it should be expired
|
||||||
|
request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); |
||||||
|
response = this.restTemplate.exchange(request, Void.class); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode()); |
||||||
|
id = extractSessionId(response.getHeaders()); |
||||||
|
assertNotNull("Expected new session id", id); |
||||||
|
assertEquals("Expected new session attribute", 1, this.handler.getCount()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// No client side HttpCookie support yet
|
||||||
|
|
||||||
|
private String extractSessionId(HttpHeaders headers) { |
||||||
|
List<String> headerValues = headers.get("Set-Cookie"); |
||||||
|
assertNotNull(headerValues); |
||||||
|
assertEquals(1, headerValues.size()); |
||||||
|
|
||||||
|
List<String> data = new ArrayList<>(); |
||||||
|
for (String s : headerValues.get(0).split(";")){ |
||||||
|
if (s.startsWith("SESSION=")) { |
||||||
|
return s.substring("SESSION=".length()); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private static class TestWebHandler implements WebHandler { |
||||||
|
|
||||||
|
private AtomicInteger currentValue = new AtomicInteger(); |
||||||
|
|
||||||
|
|
||||||
|
public int getCount() { |
||||||
|
return this.currentValue.get(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> handle(WebServerExchange exchange) { |
||||||
|
return exchange.getSession().map(session -> { |
||||||
|
Map<String, Object> map = session.getAttributes(); |
||||||
|
int value = (map.get("counter") != null ? (int) map.get("counter") : 0); |
||||||
|
value++; |
||||||
|
map.put("counter", value); |
||||||
|
this.currentValue.set(value); |
||||||
|
return session; |
||||||
|
}).after(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue