diff --git a/pom.xml b/pom.xml index e5e44c4..a62ac39 100644 --- a/pom.xml +++ b/pom.xml @@ -45,12 +45,18 @@ org.apache.httpcomponents httpclient - + com.yunhai.platform yh-app-auth-spring-boot-starter 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-data-redis + com.mysql mysql-connector-j diff --git a/src/main/java/com/yunhai/platform/gateway/auth/GatewayKeyService.java b/src/main/java/com/yunhai/platform/gateway/auth/GatewayKeyService.java new file mode 100644 index 0000000..7f27ed9 --- /dev/null +++ b/src/main/java/com/yunhai/platform/gateway/auth/GatewayKeyService.java @@ -0,0 +1,42 @@ +package com.yunhai.platform.gateway.auth; + +import com.yunhai.platform.appauth.SignKeyResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 网关密钥解析薄封装(T0.11):100% 复用 starter {@link SignKeyResolver}(F3), + * 不重写密钥逻辑、不自建密钥缓存——Redis 连接走平台配置层(spring.redis.*,密钥链专用 db0)。 + *

+ * 语义与 starter 完全一致(D4:密钥解析永远 fail-closed): + * 未知 appKey / 禁用 / Redis 异常 / starter 未装配,一律返回 {@code null} = 拒绝, + * 绝不抛 500(上层 T1.2 SignVerifyFilter 将 null 转为裸 401)。 + * 任何"Redis 挂了先放行"的逻辑都被禁止(nonce/限流降级在 T3.6,密钥永不降级)。 + */ +@Component +public class GatewayKeyService { + + private static final Logger log = LoggerFactory.getLogger(GatewayKeyService.class); + + @Autowired(required = false) + private SignKeyResolver signKeyResolver; + + /** + * @return 调用方 appSecret;解析不出(未知/禁用/异常/未装配)一律 null = 拒绝 + */ + public String getSecret(String appKey) { + if (signKeyResolver == null || appKey == null || appKey.trim().isEmpty()) { + return null; + } + try { + // starter 契约:未知/禁用/Redis 异常内部已转 null(fail-closed);此处不二次包装语义 + return signKeyResolver.getSecret(appKey); + } catch (Exception e) { + // 防御:starter 契约之外的意外异常同样落为拒绝,不向请求线程抛 500 + log.warn("[GatewayKeyService] key resolve unexpected error, reject appKey={}", appKey, e); + return null; + } + } +} diff --git a/src/test/java/com/yunhai/platform/gateway/auth/GatewayKeyServiceTest.java b/src/test/java/com/yunhai/platform/gateway/auth/GatewayKeyServiceTest.java new file mode 100644 index 0000000..e99feb5 --- /dev/null +++ b/src/test/java/com/yunhai/platform/gateway/auth/GatewayKeyServiceTest.java @@ -0,0 +1,65 @@ +package com.yunhai.platform.gateway.auth; + +import com.yunhai.platform.appauth.SignKeyResolver; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * GatewayKeyService 四态(T0.11 TDD,mock SignKeyResolver): + * 合法 / 未知 / 禁用 / Redis 异常。D4 红线:密钥解析永远 fail-closed—— + * 异常态断言"拒绝"(返回 null)而非抛异常(500)。 + */ +class GatewayKeyServiceTest { + + private GatewayKeyService service(SignKeyResolver resolver) { + GatewayKeyService svc = new GatewayKeyService(); + ReflectionTestUtils.setField(svc, "signKeyResolver", resolver); + return svc; + } + + @Test + void 合法appKey返回secret() { + SignKeyResolver resolver = mock(SignKeyResolver.class); + when(resolver.getSecret("100617")).thenReturn("secret-flow-center"); + assertThat(service(resolver).getSecret("100617")).isEqualTo("secret-flow-center"); + } + + @Test + void 未知appKey拒绝() { + // starter 契约:Redis 无该 key(未知 app)返回 null + SignKeyResolver resolver = mock(SignKeyResolver.class); + when(resolver.getSecret("999999")).thenReturn(null); + assertThat(service(resolver).getSecret("999999")).as("未知 appKey = 拒绝").isNull(); + } + + @Test + void 禁用appKey拒绝() { + // starter SignKeyResolver 内部对 enabled=false / secret 空 fallen 返回 null(fail-closed 单层); + // 网关侧语义与 starter 完全一致——到达本封装即为 null + SignKeyResolver resolver = mock(SignKeyResolver.class); + when(resolver.getSecret("100009")).thenReturn(null); + assertThat(service(resolver).getSecret("100009")).as("禁用 appKey = 拒绝").isNull(); + } + + @Test + void redis异常拒绝而非抛500() { + SignKeyResolver resolver = mock(SignKeyResolver.class); + when(resolver.getSecret("100617")).thenThrow(new RuntimeException("redis unavailable")); + GatewayKeyService svc = service(resolver); + assertThatCode(() -> { + assertThat(svc.getSecret("100617")).as("Redis 异常 = 拒绝(fail-closed),不抛 500").isNull(); + }).doesNotThrowAnyException(); + Mockito.verify(resolver).getSecret("100617"); + } + + @Test + void starter未装配同样拒绝() { + assertThat(service(null).getSecret("100617")).as("无 SignKeyResolver bean(未配 Redis)同样 fail-closed").isNull(); + } +}