This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 11404ed209 fix(bootstrap): isolate gateway session cookies (#7897)
11404ed209 is described below
commit 11404ed20972e229787e4acedb424d427e9a190f
Author: cloudbase-ai <[email protected]>
AuthorDate: Wed Oct 21 14:15:11 2026 +0800
fix(bootstrap): isolate gateway session cookies (#7897)
Signed-off-by: cloudbase-ai <[email protected]>
Co-authored-by: cloudbase-ai
<[email protected]>
---
docs/gateway-session-cookies.md | 86 +++++++++++++
shenyu-bootstrap/pom.xml | 18 ++-
.../src/main/resources/application.yml | 5 +
.../shenyu/bootstrap/GatewaySessionCookieTest.java | 143 +++++++++++++++++++++
4 files changed, 245 insertions(+), 7 deletions(-)
diff --git a/docs/gateway-session-cookies.md b/docs/gateway-session-cookies.md
new file mode 100644
index 0000000000..79ea52a6a9
--- /dev/null
+++ b/docs/gateway-session-cookies.md
@@ -0,0 +1,86 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+-->
+
+# Gateway and backend session cookies
+
+The packaged `shenyu-bootstrap` configuration uses `SHENYU_SESSION` for the
+gateway's reactive WebSession cookie. Backend applications can continue using
+`SESSION` without sharing the gateway's session identifier.
+
+Spring WebFlux can expire an incoming cookie when a component accesses a
session
+identifier that is unknown to the gateway's session store. Security filters may
+access the session independently of the dashboard's OAuth2 plugin enabled flag.
+If both gateway and backend use `SESSION`, the resulting `Set-Cookie: SESSION=;
+Max-Age=0` can clear the backend's browser login. The gateway and backend need
+different session-cookie names on a shared public origin.
+
+## Configuration and scope
+
+The default is set using Spring Boot's standard reactive session property:
+
+```yaml
+server:
+ reactive:
+ session:
+ cookie:
+ name: SHENYU_SESSION
+```
+
+The bootstrap distribution copies this same application configuration into its
+`conf` directory. Custom launchers or external configuration files that replace
+the packaged configuration should set this property explicitly. For example, an
+operator can select another unused name with
+`SERVER_REACTIVE_SESSION_COOKIE_NAME=CUSTOM_GATEWAY_SESSION`.
+
+This configures the gateway's own WebSession identifier. It does not rename
+proxied backend cookies or change OAuth2 authorization rules. Spring Boot's
+existing cookie path, domain, Secure, HttpOnly, SameSite, and session-store
+configuration continue to apply.
+
+When the gateway receives an expired `SHENYU_SESSION`, it may clear that cookie
+while forwarding a backend `SESSION`. Choose a different gateway name if a
+backend already uses `SHENYU_SESSION`.
+
+## Upgrade compatibility
+
+An existing gateway login saved under the former `SESSION` name will not be
+recognized under the new default cookie name. Users may need to authenticate
+with the gateway again after adopting this configuration. Backend session
+identifiers and persisted session-store data are not rewritten.
+
+An explicitly configured cookie name takes precedence over the packaged
+configuration. A deployment deliberately retaining `SESSION` can set
+`server.reactive.session.cookie.name=SESSION`; this also retains the original
+collision behavior if a backend uses that name. Review both cookie owners
+before choosing the compatibility override.
+
+These defaults apply to the current packaged bootstrap configuration. An older
+ShenYu deployment needs the property added to its own compatible Spring Boot
+configuration; changing the dashboard plugin flag alone does not isolate
+WebFlux cookies.
+
+## Regression coverage
+
+`GatewaySessionCookieTest` reads the actual bootstrap YAML and uses Spring
Boot's
+`WebSessionIdResolverAutoConfiguration`, `DefaultWebSessionManager`, a real
+reactive HTTP handler, and an ephemeral loopback HTTP server. It checks backend
+cookie preservation, expiry of only the gateway cookie, creation of new gateway
+sessions, explicit name overrides, and a legacy shared-name collision control.
+
+The fixture exercises the WebFlux session boundary without contacting a CAS or
+OAuth2 identity provider. External provider login/logout and
deployment-specific
+cookie domain/path policies should also be checked when rolling out the change.
diff --git a/shenyu-bootstrap/pom.xml b/shenyu-bootstrap/pom.xml
index 70949ade5e..0a61916a1e 100644
--- a/shenyu-bootstrap/pom.xml
+++ b/shenyu-bootstrap/pom.xml
@@ -493,19 +493,23 @@
</profiles>
<build>
<finalName>shenyu-bootstrap</finalName>
+ <testResources>
+ <testResource>
+ <directory>src/test/resources</directory>
+ </testResource>
+ <testResource>
+ <directory>src/main/resources</directory>
+ <includes>
+ <include>application.yml</include>
+ </includes>
+ </testResource>
+ </testResources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
</plugins>
</build>
</project>
diff --git a/shenyu-bootstrap/src/main/resources/application.yml
b/shenyu-bootstrap/src/main/resources/application.yml
index 5d39ca2225..d926275a6c 100644
--- a/shenyu-bootstrap/src/main/resources/application.yml
+++ b/shenyu-bootstrap/src/main/resources/application.yml
@@ -16,6 +16,11 @@
server:
port: 9195
address: 0.0.0.0
+ reactive:
+ session:
+ cookie:
+ # Keep gateway sessions separate from backend SESSION cookies.
+ name: SHENYU_SESSION
compression:
enabled: true
minResponseSize: 1MB # If the response data is greater than 1MB, enable
compression.
diff --git
a/shenyu-bootstrap/src/test/java/org/apache/shenyu/bootstrap/GatewaySessionCookieTest.java
b/shenyu-bootstrap/src/test/java/org/apache/shenyu/bootstrap/GatewaySessionCookieTest.java
new file mode 100644
index 0000000000..7bd48c9d0e
--- /dev/null
+++
b/shenyu-bootstrap/src/test/java/org/apache/shenyu/bootstrap/GatewaySessionCookieTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.shenyu.bootstrap;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
+import
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration;
+import org.springframework.boot.context.properties.bind.Binder;
+import
org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
+import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
+import org.springframework.web.server.session.DefaultWebSessionManager;
+import reactor.core.publisher.Mono;
+import reactor.netty.DisposableServer;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.http.server.HttpServer;
+import reactor.netty.resources.LoopResources;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Exercises the packaged bootstrap session-cookie configuration over real
HTTP.
+ */
+class GatewaySessionCookieTest {
+
+ @Test
+ void testBackendSessionCookieSurvivesGatewaySessionAccess() {
+ Tuple2<List<String>, String> response =
request(serverProperties(null), "SESSION=backend-session", false);
+
+ assertEquals("backend-session", response.getT2());
+ assertTrue(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SESSION=backend-renewed")));
+ assertFalse(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SESSION=;") && cookie.contains("Max-Age=0")));
+ }
+
+ @Test
+ void testExpiredGatewaySessionOnlyClearsGatewayCookie() {
+ Tuple2<List<String>, String> response = request(serverProperties(null),
+ "SHENYU_SESSION=expired-gateway; SESSION=backend-session",
false);
+
+ assertEquals("backend-session", response.getT2());
+ assertTrue(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SHENYU_SESSION=;") && cookie.contains("Max-Age=0")));
+ assertFalse(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SESSION=;") && cookie.contains("Max-Age=0")));
+ }
+
+ @Test
+ void testNewGatewaySessionUsesItsOwnCookie() {
+ Tuple2<List<String>, String> response =
request(serverProperties(null), "SESSION=backend-session", true);
+
+ assertEquals("backend-session", response.getT2());
+ assertTrue(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SHENYU_SESSION=") && !cookie.contains("Max-Age=0")));
+ }
+
+ @Test
+ void testExplicitCookieNameOverrideIsHonored() {
+ Tuple2<List<String>, String> response =
request(serverProperties("CUSTOM_GATEWAY_SESSION"), "SESSION=backend-session",
true);
+
+ assertTrue(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("CUSTOM_GATEWAY_SESSION=")));
+ assertFalse(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SHENYU_SESSION=")));
+ }
+
+ @Test
+ void testLegacySharedNameReproducesBackendSessionDeletion() {
+ Tuple2<List<String>, String> response =
request(serverProperties("SESSION"), "SESSION=backend-session", false);
+
+ assertTrue(response.getT1().stream().anyMatch(cookie ->
cookie.startsWith("SESSION=;") && cookie.contains("Max-Age=0")));
+ }
+
+ private ServerProperties serverProperties(final String cookieName) {
+ YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
+ yaml.setResources(new ClassPathResource("application.yml"));
+ Properties properties = Objects.requireNonNull(yaml.getObject());
+ if (Objects.nonNull(cookieName)) {
+ properties.setProperty("server.reactive.session.cookie.name",
cookieName);
+ }
+ return new Binder(new
MapConfigurationPropertySource(properties)).bind("server",
ServerProperties.class).get();
+ }
+
+ private Tuple2<List<String>, String> request(final ServerProperties
properties, final String cookies, final boolean startSession) {
+ DefaultWebSessionManager sessions = new DefaultWebSessionManager();
+ sessions.setSessionIdResolver(new
WebSessionIdResolverAutoConfiguration(properties, new
WebFluxProperties()).webSessionIdResolver());
+ HttpWebHandlerAdapter handler = new HttpWebHandlerAdapter(exchange ->
exchange.getSession().flatMap(session -> {
+ if (startSession) {
+ session.getAttributes().put("gateway-authentication",
"active");
+ }
+ exchange.getResponse().addCookie(ResponseCookie.from("SESSION",
"backend-renewed").path("/").build());
+ String original =
Objects.requireNonNull(exchange.getRequest().getCookies().getFirst("SESSION")).getValue();
+ byte[] bytes = original.getBytes(StandardCharsets.UTF_8);
+ return
exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes)));
+ }));
+ handler.setSessionManager(sessions);
+ handler.afterPropertiesSet();
+ LoopResources loops = LoopResources.create("session-cookie-test", 1,
true);
+ DisposableServer server = null;
+ try {
+ server = HttpServer.create().host("127.0.0.1").port(0).runOn(loops)
+ .handle(new
ReactorHttpHandlerAdapter(handler)).bindNow(Duration.ofSeconds(5));
+ return
Objects.requireNonNull(HttpClient.newConnection().runOn(loops).keepAlive(false)
+ .headers(headers -> headers.set(HttpHeaderNames.COOKIE,
cookies)).get()
+ .uri("http://127.0.0.1:" + server.port() + "/session")
+ .responseSingle((response, body) ->
body.asString().defaultIfEmpty("")
+ .map(text ->
Tuples.of(List.copyOf(response.responseHeaders().getAll(HttpHeaderNames.SET_COOKIE)),
text)))
+ .block(Duration.ofSeconds(5)));
+ } finally {
+ try {
+ if (Objects.nonNull(server)) {
+ server.disposeNow(Duration.ofSeconds(5));
+ }
+ } finally {
+ loops.disposeLater().block(Duration.ofSeconds(5));
+ }
+ }
+ }
+}