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

Reply via email to