diff --git a/src/main/java/com/yunhai/platform/gateway/route/GatewayProperties.java b/src/main/java/com/yunhai/platform/gateway/route/GatewayProperties.java new file mode 100644 index 0000000..4bb871a --- /dev/null +++ b/src/main/java/com/yunhai/platform/gateway/route/GatewayProperties.java @@ -0,0 +1,42 @@ +package com.yunhai.platform.gateway.route; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 网关配置(T0.12 起;环境值走平台配置层,jar 内 yml 只留占位)。 + * + */ +@Component +@ConfigurationProperties(prefix = "yh.gateway") +public class GatewayProperties { + + /** target → base-url(值不含路径;转发 = base-url + 完整原始 URI,换主机不换路径) */ + private Map routes = new LinkedHashMap<>(); + + /** 观察模式:true = 全判定只记不拦(P1 只观察,R4) */ + private boolean observe = true; + + public Map getRoutes() { + return routes; + } + + public void setRoutes(Map routes) { + this.routes = routes; + } + + public boolean isObserve() { + return observe; + } + + public void setObserve(boolean observe) { + this.observe = observe; + } +} diff --git a/src/main/java/com/yunhai/platform/gateway/route/TargetRouter.java b/src/main/java/com/yunhai/platform/gateway/route/TargetRouter.java new file mode 100644 index 0000000..5730d6f --- /dev/null +++ b/src/main/java/com/yunhai/platform/gateway/route/TargetRouter.java @@ -0,0 +1,77 @@ +package com.yunhai.platform.gateway.route; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 目标路由与转发 URL 计算(T0.12):{@code /{target}/oapi/**} → 提供方 base-url 的映射。 + *

+ * 换主机不换路径:forwardUrl = base-url + 完整原始 URI(含 query)。target 首段仅用于 + * 路由选择、注册表匹配与 canonical 剥离,不参与转发路径改写——提供方自带 context-path, + * URI 原样送达即天然对齐,签名 canonical 逐字节不变。
+ * 网关只做 oapi:未知 target / 单段 URI / 第二段非 oapi(如 /{target}/api/x)一律返回 null + * = 裸 404({@code /api} 与页面不属于网关职责)。 + */ +@Component +public class TargetRouter { + + /** 路由产物:判定字段 + 转发 URL */ + public static class Routing { + public final String target; + /** 剥掉 target 首段后的路径(如 /oapi/message/list)——RouteRegistry 与签名 canonical 同一坐标系 */ + public final String pathAfterStrip; + /** base-url + 完整原始 URI(含 query),逐字符不改写 */ + public final String forwardUrl; + + Routing(String target, String pathAfterStrip, String forwardUrl) { + this.target = target; + this.pathAfterStrip = pathAfterStrip; + this.forwardUrl = forwardUrl; + } + } + + @Autowired + private GatewayProperties properties; + + /** 测试/显式装配入口 */ + void setProperties(GatewayProperties properties) { + this.properties = properties; + } + + /** + * @param requestUri 原始请求 URI(未解码,如 /userbase/oapi/x) + * @param queryString 原始 query(无 '?' 前缀;null = 无) + * @return 路由产物;不匹配(未知 target / 单段 / 非 oapi 第二段)返回 null = 裸 404 + */ + public Routing route(String requestUri, String queryString) { + if (requestUri == null || requestUri.isEmpty() || requestUri.equals("/")) { + return null; + } + // 剥首段 target:/userbase/oapi/x → target=userbase, rest=/oapi/x + int secondSlash = requestUri.indexOf('/', 1); + String target; + String rest; + if (secondSlash < 0) { + return null; // 单段 URI(/foo)——无 oapi 第二段 + } else { + target = requestUri.substring(1, secondSlash); + rest = requestUri.substring(secondSlash); + } + if (target.isEmpty()) { + return null; // 双斜杠开头等畸形 + } + // 网关只做 oapi:第二段必须恰为 oapi(/oapi、/oapi/xxx 均可) + if (!rest.equals("/oapi") && !rest.startsWith("/oapi/")) { + return null; + } + String baseUrl = properties.getRoutes().get(target); + if (baseUrl == null || baseUrl.trim().isEmpty()) { + return null; // 未知 target = 裸 404 + } + String normalizedBase = baseUrl.endsWith("/") + ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + String url = normalizedBase + requestUri + + (queryString == null || queryString.isEmpty() ? "" : "?" + queryString); + return new Routing(target, rest, url); + } +} diff --git a/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java b/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java new file mode 100644 index 0000000..cad395f --- /dev/null +++ b/src/test/java/com/yunhai/platform/gateway/route/TargetRouterTest.java @@ -0,0 +1,79 @@ +package com.yunhai.platform.gateway.route; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * TargetRouter 用例(T0.12 TDD):已知 target forwardUrl 逐字符断言(换主机不换路径)、 + * 未知 target / 单段 / 非 oapi 第二段一律裸 404(null)、base-url 尾斜杠归一化。 + */ +class TargetRouterTest { + + private TargetRouter router; + + @BeforeEach + void setUp() { + router = new TargetRouter(); + GatewayProperties props = new GatewayProperties(); + Map routes = new LinkedHashMap<>(); + routes.put("userbase", "http://userbase-service:8080"); + routes.put("notice", "http://notice-service:8080/"); + props.setRoutes(routes); + router.setProperties(props); + } + + @Test + void 已知target_forwardUrl逐字符断言() { + TargetRouter.Routing r = router.route("/userbase/oapi/x", "a=1"); + assertThat(r).isNotNull(); + assertThat(r.forwardUrl).as("换主机不换路径:base-url + 完整原始 URI 含 query,逐字符") + .isEqualTo("http://userbase-service:8080/userbase/oapi/x?a=1"); + assertThat(r.target).isEqualTo("userbase"); + assertThat(r.pathAfterStrip).as("剥段路径 = 注册表/签名坐标系").isEqualTo("/oapi/x"); + } + + @Test + void 无query的URI不带问号() { + TargetRouter.Routing r = router.route("/notice/oapi/notice/list", null); + assertThat(r.forwardUrl).isEqualTo("http://notice-service:8080/notice/oapi/notice/list"); + assertThat(r.pathAfterStrip).isEqualTo("/oapi/notice/list"); + } + + @Test + void 未知target_裸404() { + assertThat(router.route("/nobody/oapi/x", null)).as("未配置的 target = 裸 404").isNull(); + } + + @Test + void 单段URI_裸404() { + assertThat(router.route("/", null)).isNull(); + assertThat(router.route("/foo", null)).isNull(); + } + + @Test + void 非oapi第二段_裸404() { + assertThat(router.route("/userbase/api/x", null)).as("网关只做 oapi,/api 一律 404").isNull(); + assertThat(router.route("/userbase/oapix", null)).as("前缀相同不算 oapi 段").isNull(); + } + + @Test + void 尾斜杠base归一化_无双斜杠() { + TargetRouter.Routing r = router.route("/notice/oapi/x", null); + assertThat(r.forwardUrl).as("base-url 尾斜杠不得产生双斜杠") + .isEqualTo("http://notice-service:8080/notice/oapi/x"); + } + + @Test + void 裸oapi路径可路由() { + // /{target}/oapi(无尾路径)属 /{target}/oapi/** 匹配范围 + TargetRouter.Routing r = router.route("/userbase/oapi", null); + assertThat(r).isNotNull(); + assertThat(r.forwardUrl).isEqualTo("http://userbase-service:8080/userbase/oapi"); + assertThat(r.pathAfterStrip).isEqualTo("/oapi"); + } +}