diff --git a/bom/application/pom.xml b/bom/application/pom.xml index e73b4f767bce2..93e77daf18a1a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1218,6 +1218,16 @@ quarkus-infinispan-client-deployment ${project.version} + + io.quarkus + quarkus-infinispan-client-sessions + ${project.version} + + + io.quarkus + quarkus-infinispan-client-sessions-deployment + ${project.version} + io.quarkus quarkus-jaeger @@ -1835,6 +1845,11 @@ quarkus-vertx-http ${project.version} + + io.quarkus + quarkus-vertx-http-sessions-spi + ${project.version} + io.quarkus quarkus-vertx-http-dev-console-spi @@ -5941,6 +5956,11 @@ quarkus-redis-client ${project.version} + + io.quarkus + quarkus-redis-client-sessions + ${project.version} + io.quarkus quarkus-redis-cache @@ -5952,6 +5972,11 @@ quarkus-redis-client-deployment ${project.version} + + io.quarkus + quarkus-redis-client-sessions-deployment + ${project.version} + io.quarkus quarkus-redis-cache-deployment diff --git a/extensions/infinispan-client/pom.xml b/extensions/infinispan-client/pom.xml index 098c9701f3126..1c3b950c87bee 100644 --- a/extensions/infinispan-client/pom.xml +++ b/extensions/infinispan-client/pom.xml @@ -16,5 +16,8 @@ deployment runtime + + sessions/deployment + sessions/runtime diff --git a/extensions/infinispan-client/sessions/deployment/pom.xml b/extensions/infinispan-client/sessions/deployment/pom.xml new file mode 100644 index 0000000000000..d00d5484e4326 --- /dev/null +++ b/extensions/infinispan-client/sessions/deployment/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.quarkus + quarkus-infinispan-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-infinispan-client-sessions-deployment + + Quarkus - Infinispan Client - Vert.x Web Sessions - Deployment + + + io.quarkus + quarkus-infinispan-client-sessions + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-infinispan-client-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java b/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java new file mode 100644 index 0000000000000..b6d8315e6f63b --- /dev/null +++ b/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java @@ -0,0 +1,35 @@ +package io.quarkus.infinispan.sessions.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.infinispan.client.deployment.InfinispanClientNameBuildItem; +import io.quarkus.infinispan.client.runtime.InfinispanClientUtil; +import io.quarkus.infinispan.sessions.runtime.InfinispanSessionsRecorder; +import io.quarkus.vertx.http.sessions.spi.SessionStoreKind; +import io.quarkus.vertx.http.sessions.spi.SessionStoreRequestBuildItem; +import io.quarkus.vertx.http.sessions.spi.SessionStoreResponseBuildItem; + +public class InfinispanSessionsProcessor { + private static final String FEATURE = "infinispan-sessions"; + + @BuildStep + public FeatureBuildItem featureBuildItem() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void infinispanSessions(SessionStoreRequestBuildItem request, + BuildProducer response, + BuildProducer infinispanRequest, + InfinispanSessionsRecorder recorder) { + if (request.is(SessionStoreKind.INFINISPAN)) { + response.produce(new SessionStoreResponseBuildItem(recorder.create())); + infinispanRequest.produce(new InfinispanClientNameBuildItem( + request.clientName(InfinispanClientUtil.DEFAULT_INFINISPAN_CLIENT_NAME))); + } + } +} diff --git a/extensions/infinispan-client/sessions/runtime/pom.xml b/extensions/infinispan-client/sessions/runtime/pom.xml new file mode 100644 index 0000000000000..49a89bcc72fe6 --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + io.quarkus + quarkus-infinispan-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-infinispan-client-sessions + + Quarkus - Infinispan Client - Vert.x Web Sessions - Runtime + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-infinispan-client + + + io.quarkus + quarkus-vertx-http-sessions-spi + + + io.vertx + vertx-web-sstore-infinispan + + + org.infinispan + infinispan-client-hotrod + + + io.reactivex.rxjava3 + rxjava + + + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java b/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java new file mode 100644 index 0000000000000..068e221e0b585 --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java @@ -0,0 +1,47 @@ +package io.quarkus.infinispan.sessions.runtime; + +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.Map; + +import io.quarkus.infinispan.client.runtime.InfinispanClientUtil; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + +import org.infinispan.client.hotrod.RemoteCacheManager; + +import io.quarkus.arc.Arc; +import io.quarkus.infinispan.client.InfinispanClientName; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.http.sessions.spi.SessionStoreProvider; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.sstore.SessionStore; +import io.vertx.ext.web.sstore.infinispan.InfinispanSessionStore; + +@Recorder +public class InfinispanSessionsRecorder { + public SessionStoreProvider create() { + return new SessionStoreProvider() { + @Override + public SessionStore create(Vertx vertx, Map config) { + String clientName = (String) config.get("clientName"); + String cacheName = (String) config.get("cacheName"); + Duration retryTimeout = (Duration) config.get("retryTimeout"); + Annotation qualifier = clientName != null + ? InfinispanClientName.Literal.of(clientName) + : Default.Literal.INSTANCE; + Instance bean = Arc.container().select(RemoteCacheManager.class, qualifier); + if (bean.isResolvable()) { + RemoteCacheManager client = bean.get(); + JsonObject options = new JsonObject() + .put("cacheName", cacheName) + .put("retryTimeout", retryTimeout.toMillis()); + return InfinispanSessionStore.create(vertx, options, client); + } + throw new IllegalStateException("Unknown Infinispan client: " + + (clientName != null ? clientName : InfinispanClientUtil.DEFAULT_INFINISPAN_CLIENT_NAME)); + } + }; + } +} diff --git a/extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..de097cb66c050 --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Infinispan Client - Vert.x Web Sessions" +metadata: + keywords: + - "infinispan" + - "vertx" + - "sessions" + guide: "https://quarkus.io/guides/http-reference#vertx-web-sessions" + categories: + - "web" + status: "stable" diff --git a/extensions/redis-client/pom.xml b/extensions/redis-client/pom.xml index 458b2563a75bf..ea296877884df 100644 --- a/extensions/redis-client/pom.xml +++ b/extensions/redis-client/pom.xml @@ -19,6 +19,9 @@ deployment runtime + + sessions/deployment + sessions/runtime diff --git a/extensions/redis-client/sessions/deployment/pom.xml b/extensions/redis-client/sessions/deployment/pom.xml new file mode 100644 index 0000000000000..c4c33e636f1d5 --- /dev/null +++ b/extensions/redis-client/sessions/deployment/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.quarkus + quarkus-redis-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-redis-client-sessions-deployment + + Quarkus - Redis Client - Vert.x Web Sessions - Deployment + + + io.quarkus + quarkus-redis-client-sessions + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-redis-client-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java b/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java new file mode 100644 index 0000000000000..d51331ef21467 --- /dev/null +++ b/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java @@ -0,0 +1,35 @@ +package io.quarkus.redis.sessions.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.redis.deployment.client.RequestedRedisClientBuildItem; +import io.quarkus.redis.runtime.client.config.RedisConfig; +import io.quarkus.redis.sessions.runtime.RedisSessionsRecorder; +import io.quarkus.vertx.http.sessions.spi.SessionStoreKind; +import io.quarkus.vertx.http.sessions.spi.SessionStoreRequestBuildItem; +import io.quarkus.vertx.http.sessions.spi.SessionStoreResponseBuildItem; + +public class RedisSessionsProcessor { + private static final String FEATURE = "redis-sessions"; + + @BuildStep + public FeatureBuildItem featureBuildItem() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void redisSessions(SessionStoreRequestBuildItem request, + BuildProducer response, + BuildProducer redisRequest, + RedisSessionsRecorder recorder) { + if (request.is(SessionStoreKind.REDIS)) { + response.produce(new SessionStoreResponseBuildItem(recorder.create())); + redisRequest.produce(new RequestedRedisClientBuildItem( + request.clientName(RedisConfig.DEFAULT_CLIENT_NAME))); + } + } +} diff --git a/extensions/redis-client/sessions/runtime/pom.xml b/extensions/redis-client/sessions/runtime/pom.xml new file mode 100644 index 0000000000000..caf77de86ca5f --- /dev/null +++ b/extensions/redis-client/sessions/runtime/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + io.quarkus + quarkus-redis-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-redis-client-sessions + + Quarkus - Redis Client - Vert.x Web Sessions - Runtime + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-redis-client + + + io.quarkus + quarkus-vertx-http-sessions-spi + + + io.vertx + vertx-web-sstore-redis + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java b/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java new file mode 100644 index 0000000000000..4daf96490905d --- /dev/null +++ b/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java @@ -0,0 +1,41 @@ +package io.quarkus.redis.sessions.runtime; + +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.Map; + +import io.quarkus.redis.runtime.client.config.RedisConfig; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + +import io.quarkus.arc.Arc; +import io.quarkus.redis.client.RedisClientName; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.http.sessions.spi.SessionStoreProvider; +import io.vertx.core.Vertx; +import io.vertx.ext.web.sstore.SessionStore; +import io.vertx.ext.web.sstore.redis.RedisSessionStore; +import io.vertx.redis.client.Redis; + +@Recorder +public class RedisSessionsRecorder { + public SessionStoreProvider create() { + return new SessionStoreProvider() { + @Override + public SessionStore create(Vertx vertx, Map config) { + String clientName = (String) config.get("clientName"); + Duration retryTimeout = (Duration) config.get("retryTimeout"); + Annotation qualifier = clientName != null + ? RedisClientName.Literal.of(clientName) + : Default.Literal.INSTANCE; + Instance bean = Arc.container().select(Redis.class, qualifier); + if (bean.isResolvable()) { + Redis client = bean.get(); + return RedisSessionStore.create(vertx, retryTimeout.toMillis(), client); + } + throw new IllegalStateException("Unknown Redis client: " + + (clientName != null ? clientName : RedisConfig.DEFAULT_CLIENT_NAME)); + } + }; + } +} diff --git a/extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..4bc08b105c3cf --- /dev/null +++ b/extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Redis Client - Vert.x Web Sessions" +metadata: + keywords: + - "redis" + - "vertx" + - "sessions" + guide: "https://quarkus.io/guides/http-reference#vertx-web-sessions" + categories: + - "web" + status: "preview" diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index fd8ec2b3ce36e..53ebd35b9a81a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -60,6 +60,7 @@ import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.SessionsBuildTimeConfig; import io.quarkus.vertx.http.runtime.VertxConfigBuilder; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder; @@ -67,11 +68,15 @@ import io.quarkus.vertx.http.runtime.filters.Filter; import io.quarkus.vertx.http.runtime.filters.GracefulShutdownFilter; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.sessions.spi.SessionStoreKind; +import io.quarkus.vertx.http.sessions.spi.SessionStoreRequestBuildItem; +import io.quarkus.vertx.http.sessions.spi.SessionStoreResponseBuildItem; import io.vertx.core.Handler; import io.vertx.core.http.impl.Http1xServerRequest; import io.vertx.core.impl.VertxImpl; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.sstore.SessionStore; class VertxHttpProcessor { @@ -186,6 +191,17 @@ public KubernetesPortBuildItem kubernetesForManagement( managementInterfaceBuildTimeConfig.enabled); } + @BuildStep + public void sessionStoreRequest(HttpBuildTimeConfig config, BuildProducer request) { + if (config.sessions.mode == SessionsBuildTimeConfig.SessionsMode.REDIS) { + request.produce( + new SessionStoreRequestBuildItem(SessionStoreKind.REDIS, config.sessions.redis.clientName)); + } else if (config.sessions.mode == SessionsBuildTimeConfig.SessionsMode.INFINISPAN) { + request.produce( + new SessionStoreRequestBuildItem(SessionStoreKind.INFINISPAN, config.sessions.infinispan.clientName)); + } + } + @BuildStep void notFoundRoutes( List routes, @@ -314,7 +330,9 @@ ServiceStartBuildItem finalizeRouter( ShutdownConfig shutdownConfig, LiveReloadConfig lrc, CoreVertxBuildItem core, // Injected to be sure that Vert.x has been produced before calling this method. - ExecutorBuildItem executorBuildItem) + ExecutorBuildItem executorBuildItem, + Optional sessionStoreProvider, + Capabilities capabilities) throws BuildException, IOException { Optional defaultRoute; @@ -366,6 +384,35 @@ ServiceStartBuildItem finalizeRouter( } } + if (httpBuildTimeConfig.sessions.mode != SessionsBuildTimeConfig.SessionsMode.DISABLED + && capabilities.isPresent(Capability.SERVLET)) { + throw new IllegalStateException("Vert.x Web sessions may not be enabled together with Undertow; " + + "use Undertow (servlet) sessions instead"); + } + + RuntimeValue sessionStore = null; + switch (httpBuildTimeConfig.sessions.mode) { + case DISABLED: + break; + case IN_MEMORY: + sessionStore = recorder.createInMemorySessionStore(); + break; + case REDIS: + if (sessionStoreProvider.isEmpty()) { + throw new IllegalStateException("Redis-based session store was configured, " + + "but the Quarkus Redis Sessions extension is missing"); + } + sessionStore = recorder.createRedisSessionStore(sessionStoreProvider.get().getProvider()); + break; + case INFINISPAN: + if (sessionStoreProvider.isEmpty()) { + throw new IllegalStateException("Infinispan-based session store was configured, " + + "but the Quarkus Infinispan Sessions extension is missing"); + } + sessionStore = recorder.createInfinispanSessionStore(sessionStoreProvider.get().getProvider()); + break; + } + recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, listOfManagementInterfaceFilters, @@ -376,7 +423,8 @@ ServiceStartBuildItem finalizeRouter( nonApplicationRootPathBuildItem.getNonApplicationRootPath(), launchMode.getLaunchMode(), !requireBodyHandlerBuildItems.isEmpty(), bodyHandler, gracefulShutdownFilter, - shutdownConfig, executorBuildItem.getExecutorProxy()); + shutdownConfig, executorBuildItem.getExecutorProxy(), + sessionStore); return new ServiceStartBuildItem("vertx-http"); } diff --git a/extensions/vertx-http/pom.xml b/extensions/vertx-http/pom.xml index f370261e6feee..ed02928f7066c 100644 --- a/extensions/vertx-http/pom.xml +++ b/extensions/vertx-http/pom.xml @@ -16,6 +16,7 @@ deployment runtime + sessions-spi dev-console-spi dev-console-runtime-spi deployment-spi diff --git a/extensions/vertx-http/runtime/pom.xml b/extensions/vertx-http/runtime/pom.xml index 6f0fd6ea1fdf5..387b97c5a81af 100644 --- a/extensions/vertx-http/runtime/pom.xml +++ b/extensions/vertx-http/runtime/pom.xml @@ -61,6 +61,10 @@ + + io.quarkus + quarkus-vertx-http-sessions-spi + org.graalvm.sdk graal-sdk diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java index 217c01185a875..50d985103897b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java @@ -1,9 +1,13 @@ package io.quarkus.vertx.http.runtime; +import java.util.Map; +import java.util.function.Function; + import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.inject.Produces; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.Session; @RequestScoped public class CurrentVertxRequest { @@ -17,6 +21,16 @@ public RoutingContext getCurrent() { return current; } + @Produces + @RequestScoped + public Session getCurrentSession() { + // the session might be `null`, so naively, this producer would be `@Dependent`, + // but that would break when injected into `@Singleton` or `@ApplicationScoped` beans; + // instead, when there's no session, we return a dummy implementation that always throws + Session result = current.session(); + return result != null ? result : DummySession.INSTANCE; + } + public CurrentVertxRequest setCurrent(RoutingContext current) { this.current = current; return this; @@ -35,4 +49,95 @@ public Object getOtherHttpContextObject() { public void setOtherHttpContextObject(Object otherHttpContextObject) { this.otherHttpContextObject = otherHttpContextObject; } + + private static class DummySession implements Session { + static final Session INSTANCE = new DummySession(); + + private DummySession() { + } + + private static UnsupportedOperationException fail() { + return new UnsupportedOperationException("No active session or support for sessions disabled"); + } + + @Override + public Session regenerateId() { + throw fail(); + } + + @Override + public String id() { + throw fail(); + } + + @Override + public Session put(String key, Object obj) { + throw fail(); + } + + @Override + public Session putIfAbsent(String key, Object obj) { + throw fail(); + } + + @Override + public Session computeIfAbsent(String key, Function mappingFunction) { + throw fail(); + } + + @Override + public T get(String key) { + throw fail(); + } + + @Override + public T remove(String key) { + throw fail(); + } + + @Override + public Map data() { + throw fail(); + } + + @Override + public boolean isEmpty() { + throw fail(); + } + + @Override + public long lastAccessed() { + throw fail(); + } + + @Override + public void destroy() { + throw fail(); + } + + @Override + public boolean isDestroyed() { + throw fail(); + } + + @Override + public boolean isRegenerated() { + throw fail(); + } + + @Override + public String oldId() { + throw fail(); + } + + @Override + public long timeout() { + throw fail(); + } + + @Override + public void setAccessed() { + throw fail(); + } + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index c1a2819bd3a88..99d2f1e6a8ebc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -98,4 +98,10 @@ public class HttpBuildTimeConfig { */ @ConfigItem public OptionalInt compressionLevel; + + /** + * Configuration of Vert.x Web sessions. + */ + @ConfigItem + public SessionsBuildTimeConfig sessions; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 89ffdf53d0c19..749c01bad42c6 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -265,6 +265,11 @@ public class HttpConfiguration { @ConfigItem public Map filter; + /** + * Configuration of Vert.x Web sessions. + */ + public SessionsConfig sessions; + public ProxyConfig proxy; public int determinePort(LaunchMode launchMode) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java new file mode 100644 index 0000000000000..b8b2a0c1e42ec --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java @@ -0,0 +1,58 @@ +package io.quarkus.vertx.http.runtime; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions. + */ +@ConfigGroup +public class SessionsBuildTimeConfig { + /** + * Whether Vert.x Web support for sessions is enabled (the {@code SessionHandler} is added to the router) + * and if so, which session store is used. For the {@code redis} and {@code infinispan} modes, the corresponding + * Quarkus extension must be present and a connection to the data store must be configured there. + */ + @ConfigItem(defaultValue = "disabled") + public SessionsMode mode; + + /** + * Configuration of sessions stored in Redis. + */ + public SessionsRedisBuildTimeConfig redis; + + /** + * Configuration of sessions stored in remote Infinispan cache. + */ + public SessionsInfinispanBuildTimeConfig infinispan; + + public enum SessionsMode { + /** + * Support for Vert.x Web sessions is disabled. + */ + DISABLED, + /** + * Support for Vert.x Web sessions is enabled and sessions are stored in memory. + *

+ * In this mode, if an application is deployed in multiple replicas fronted with a load balancer, + * it is necessary to enable sticky sessions (also known as session affinity) on the load balancer. + * Still, losing a replica means losing all sessions stored on that replica. + *

+ * In a multi-replica deployment, it is recommended to use an external session store (Redis or Infinispan). + * Alternatively, if Vert.x clustering is enabled ({@code quarkus.vertx.cluster}), in-memory sessions + * may be clustered ({@code quarkus.http.sessions.in-memory.clustered}), which also makes sticky sessions + * not necessary and prevents session data loss (depending on the Vert.x cluster manager configuration). + */ + IN_MEMORY, + /** + * Support for Vert.x Web sessions is enabled and sessions are stored in a remote Redis server. + * The Quarkus Redis Sessions extension must be present and a Redis connection must be configured. + */ + REDIS, + /** + * Support for Vert.x Web sessions is enabled and sessions are stored in a remote Infinispan cache. + * The Quarkus Infinispan Sessions extension must be present and an Infinispan connection must be configured. + */ + INFINISPAN, + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java new file mode 100644 index 0000000000000..b096f79cc486d --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java @@ -0,0 +1,114 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.vertx.core.http.CookieSameSite; + +/** + * Configuration of Vert.x Web sessions. + */ +@ConfigGroup +public class SessionsConfig { + /** + * The session timeout. + */ + @ConfigItem(defaultValue = "30M") + public Duration timeout; + + /** + * The requested length of the session identifier. + */ + @ConfigItem(defaultValue = "16") + public int idLength; + + /** + * The path on which the Vert.x Web {@code SessionHandler} is installed in the router, + * as well as the session cookie path. The value is relative to {@code quarkus.http.root-path}. + */ + @ConfigItem(defaultValue = "/") + public String path; + + /** + * The name of the session cookie. + */ + @ConfigItem(defaultValue = "JSESSIONID") + public String cookieName; + + /** + * Whether the session cookie has the {@code HttpOnly} attribute. + */ + @ConfigItem(defaultValue = "true") + public boolean cookieHttpOnly; + + /** + * Whether the session cookie has the {@code Secure} attribute. + *

    + *
  • {@code always}: the session cookie always has the {@code Secure} attribute
  • + *
  • {@code never}: the session cookie never has the {@code Secure} attribute
  • + *
  • {@code auto}: the session cookie only has the {@code Secure} attribute when {@code quarkus.http.insecure-requests} + * is {@code redirect} or {@code disabled}; if {@code insecure-requests} is {@code enabled}, the session cookie + * does not have the {@code Secure} attribute + *
+ */ + @ConfigItem(defaultValue = "auto") + public SessionCookieSecure cookieSecure; + + /** + * The {@code SameSite} attribute of the session cookie. + */ + @ConfigItem + public Optional cookieSameSite; // TODO maybe not `Optional` and default to `strict`? + + /** + * The {@code Max-Age} attribute of the session cookie. Note that setting this option turns the session cookie + * into a persistent cookie. + */ + @ConfigItem + public Optional cookieMaxAge; + + /** + * Configuration of sessions stored in memory. + */ + public SessionsInMemoryConfig inMemory; + + /** + * Configuration of sessions stored in Redis. + */ + public SessionsRedisConfig redis; + + /** + * Configuration of sessions stored in remote Infinispan cache. + */ + public SessionsInfinispanConfig infinispan; + + public enum SessionCookieSecure { + /** + * The session cookie only has the {@code Secure} attribute when {@code quarkus.http.insecure-requests} + * is {@code redirect} or {@code disabled}. If {@code insecure-requests} is {@code enabled}, the session cookie + * does not have the {@code Secure} attribute. + */ + AUTO, + /** + * The session cookie always has the {@code Secure} attribute. + */ + ALWAYS, + /** + * The session cookie never has the {@code Secure} attribute. + */ + NEVER; + + boolean isEnabled(HttpConfiguration.InsecureRequests insecureRequests) { + if (this == ALWAYS) { + return true; + } else if (this == NEVER) { + return false; + } else { + return insecureRequests != HttpConfiguration.InsecureRequests.ENABLED; + } + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java new file mode 100644 index 0000000000000..7985e4ffcb818 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java @@ -0,0 +1,36 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions stored in memory. + */ +@ConfigGroup +public class SessionsInMemoryConfig { + /** + * Name of the Vert.x local map or cluster-wide map to store the session data. + */ + @ConfigItem(defaultValue = "vertx-web.sessions") + public String mapName; + + /** + * Whether in-memory sessions are clustered. + *

+ * Ignored when Vert.x clustering is not enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean clustered; + + /** + * Maximum time to retry when retrieving session data from the cluster-wide map. + * The Vert.x session handler retries when the session data are not found, because + * distributing data across the cluster may take time. + *

+ * Ignored when in-memory sessions are not clustered. + */ + @ConfigItem(defaultValue = "5s") + public Duration retryTimeout; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanBuildTimeConfig.java new file mode 100644 index 0000000000000..ad71048fa7a7c --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanBuildTimeConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions stored in remote Infinispan cache. + */ +@ConfigGroup +public class SessionsInfinispanBuildTimeConfig { + /** + * Name of the Infinispan client configured in the Quarkus Infinispan Client extension configuration. + * If not set, uses the default (unnamed) Infinispan client. + *

+ * Note that the Infinispan client must be configured to so that the user has necessary permissions + * on the Infinispan server. The required minimum is the Infinispan {@code deployer} role. + */ + @ConfigItem + public Optional clientName; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanConfig.java new file mode 100644 index 0000000000000..ef515260fd55f --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInfinispanConfig.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions stored in remote Infinispan cache. + */ +@ConfigGroup +public class SessionsInfinispanConfig { + /** + * Name of the Infinispan cache used to store session data. If it does not exist, it is created + * automatically from Infinispan's default template {@code DIST_SYNC}. + */ + @ConfigItem(defaultValue = "vertx-web.sessions") + public String cacheName; + + /** + * Maximum time to retry when retrieving session data from the Infinispan cache. + * The Vert.x session handler retries when the session data are not found, because + * distributing data across an Infinispan cluster may take time. + */ + @ConfigItem(defaultValue = "5s") + public Duration retryTimeout; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisBuildTimeConfig.java new file mode 100644 index 0000000000000..e7a2b7f3150d5 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisBuildTimeConfig.java @@ -0,0 +1,19 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions stored in Redis. + */ +@ConfigGroup +public class SessionsRedisBuildTimeConfig { + /** + * Name of the Redis client configured in the Quarkus Redis extension configuration. + * If not set, uses the default (unnamed) Redis client. + */ + @ConfigItem + public Optional clientName; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisConfig.java new file mode 100644 index 0000000000000..38314ce1f3bb3 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsRedisConfig.java @@ -0,0 +1,20 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions stored in Redis. + */ +@ConfigGroup +public class SessionsRedisConfig { + /** + * Maximum time to retry when retrieving session data from the Redis server. + * The Vert.x session handler retries when the session data are not found, because + * distributing data across a potential Redis cluster may take some time. + */ + @ConfigItem(defaultValue = "2s") + public Duration retryTimeout; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index fbcb893f48b9d..b65ecbb949993 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -7,9 +7,11 @@ import java.net.BindException; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -74,6 +76,7 @@ import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers; import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils; +import io.quarkus.vertx.http.sessions.spi.SessionStoreProvider; import io.smallrye.common.vertx.VertxContext; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; @@ -104,6 +107,10 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.CorsHandler; +import io.vertx.ext.web.handler.SessionHandler; +import io.vertx.ext.web.sstore.ClusteredSessionStore; +import io.vertx.ext.web.sstore.LocalSessionStore; +import io.vertx.ext.web.sstore.SessionStore; @Recorder public class VertxHttpRecorder { @@ -191,14 +198,18 @@ private boolean uriValid(HttpServerRequest httpServerRequest) { final RuntimeValue managementConfiguration; private static volatile Handler managementRouter; + final RuntimeValue vertxConfiguration; + public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, RuntimeValue httpConfiguration, - RuntimeValue managementConfiguration) { + RuntimeValue managementConfiguration, + RuntimeValue vertxConfiguration) { this.httpBuildTimeConfig = httpBuildTimeConfig; this.httpConfiguration = httpConfiguration; this.managementBuildTimeConfig = managementBuildTimeConfig; this.managementConfiguration = managementConfiguration; + this.vertxConfiguration = vertxConfiguration; } public static void setHotReplacement(Handler handler, HotReplacementContext hrc) { @@ -346,6 +357,36 @@ public void mountFrameworkRouter(RuntimeValue mainRouter, RuntimeValue createInMemorySessionStore() { + Vertx vertx = VertxCoreRecorder.getVertx().get(); + if (httpConfiguration.getValue().sessions.inMemory.clustered + && vertxConfiguration.getValue().cluster() != null + && vertxConfiguration.getValue().cluster().clustered()) { + return new RuntimeValue<>(ClusteredSessionStore.create(vertx, + httpConfiguration.getValue().sessions.inMemory.mapName, + httpConfiguration.getValue().sessions.inMemory.retryTimeout.toMillis())); + } else { + // TODO maybe make reaper interval also configurable? + return new RuntimeValue<>(LocalSessionStore.create(vertx, + httpConfiguration.getValue().sessions.inMemory.mapName)); + } + } + + public RuntimeValue createRedisSessionStore(SessionStoreProvider provider) { + Map config = new HashMap<>(); + config.put("clientName", httpBuildTimeConfig.sessions.redis.clientName.orElse(null)); + config.put("retryTimeout", httpConfiguration.getValue().sessions.redis.retryTimeout); + return new RuntimeValue<>(provider.create(VertxCoreRecorder.getVertx().get(), config)); + } + + public RuntimeValue createInfinispanSessionStore(SessionStoreProvider provider) { + Map config = new HashMap<>(); + config.put("clientName", httpBuildTimeConfig.sessions.infinispan.clientName.orElse(null)); + config.put("cacheName", httpConfiguration.getValue().sessions.infinispan.cacheName); + config.put("retryTimeout", httpConfiguration.getValue().sessions.infinispan.retryTimeout); + return new RuntimeValue<>(provider.create(VertxCoreRecorder.getVertx().get(), config)); + } + public void finalizeRouter(BeanContainer container, Consumer defaultRouteHandler, List filterList, List managementInterfaceFilterList, Supplier vertx, LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue, @@ -355,7 +396,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute LaunchMode launchMode, boolean requireBodyHandler, Handler bodyHandler, GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, - Executor executor) { + Executor executor, RuntimeValue sessionStore) { HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); // install the default route at the end Router httpRouteRouter = httpRouterRuntimeValue.getValue(); @@ -413,6 +454,23 @@ public void handle(RoutingContext routingContext) { // Headers sent on any request, regardless of the response HttpServerCommonHandlers.applyHeaders(httpConfiguration.header, httpRouteRouter); + if (sessionStore != null) { + SessionsConfig sessions = httpConfiguration.sessions; + // TODO probably need to normalize and relativize the path here + String path = sessions.path; + SessionHandler sessionHandler = SessionHandler.create(sessionStore.getValue()) + .setSessionTimeout(sessions.timeout.toMillis()) + .setMinLength(sessions.idLength) + .setSessionCookiePath(path) + .setSessionCookieName(sessions.cookieName) + .setCookieHttpOnlyFlag(sessions.cookieHttpOnly) + .setCookieSecureFlag(sessions.cookieSecure.isEnabled(httpConfiguration.insecureRequests)) + .setCookieSameSite(sessions.cookieSameSite.orElse(null)) + .setCookieMaxAge(sessions.cookieMaxAge.map(Duration::toMillis).orElse(-1L)); + // TODO verify if this is the correct router to which the session handler should be installed + httpRouteRouter.route(path).handler(sessionHandler); + } + Handler root; if (rootPath.equals("/")) { if (hotReplacementHandler != null) { diff --git a/extensions/vertx-http/sessions-spi/pom.xml b/extensions/vertx-http/sessions-spi/pom.xml new file mode 100644 index 0000000000000..ad9b4b21be033 --- /dev/null +++ b/extensions/vertx-http/sessions-spi/pom.xml @@ -0,0 +1,27 @@ + + + + quarkus-vertx-http-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-vertx-http-sessions-spi + Quarkus - Vert.x - HTTP - Sessions SPI + + + + io.quarkus + quarkus-core-deployment + + + io.vertx + vertx-web + + + + diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreKind.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreKind.java new file mode 100644 index 0000000000000..087627aa29002 --- /dev/null +++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreKind.java @@ -0,0 +1,6 @@ +package io.quarkus.vertx.http.sessions.spi; + +public enum SessionStoreKind { + REDIS, + INFINISPAN, +} diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreProvider.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreProvider.java new file mode 100644 index 0000000000000..9599413b606aa --- /dev/null +++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreProvider.java @@ -0,0 +1,10 @@ +package io.quarkus.vertx.http.sessions.spi; + +import java.util.Map; + +import io.vertx.core.Vertx; +import io.vertx.ext.web.sstore.SessionStore; + +public interface SessionStoreProvider { + SessionStore create(Vertx vertx, Map config); +} diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreRequestBuildItem.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreRequestBuildItem.java new file mode 100644 index 0000000000000..ebde51806fc4f --- /dev/null +++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreRequestBuildItem.java @@ -0,0 +1,24 @@ +package io.quarkus.vertx.http.sessions.spi; + +import java.util.Objects; +import java.util.Optional; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class SessionStoreRequestBuildItem extends SimpleBuildItem { + private final SessionStoreKind kind; + private final Optional clientName; // may be empty for the unnamed client + + public SessionStoreRequestBuildItem(SessionStoreKind kind, Optional clientName) { + this.kind = Objects.requireNonNull(kind); + this.clientName = Objects.requireNonNull(clientName); + } + + public boolean is(SessionStoreKind kind) { + return this.kind == kind; + } + + public String clientName(String defaultName) { + return clientName.orElse(defaultName); + } +} diff --git a/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreResponseBuildItem.java b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreResponseBuildItem.java new file mode 100644 index 0000000000000..c735c5098fa77 --- /dev/null +++ b/extensions/vertx-http/sessions-spi/src/main/java/io/quarkus/vertx/http/sessions/spi/SessionStoreResponseBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.vertx.http.sessions.spi; + +import java.util.Objects; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class SessionStoreResponseBuildItem extends SimpleBuildItem { + private final SessionStoreProvider provider; + + public SessionStoreResponseBuildItem(SessionStoreProvider provider) { + this.provider = Objects.requireNonNull(provider); + } + + public SessionStoreProvider getProvider() { + return provider; + } +}