T0.11: GatewayKeyService 薄封装复用 starter SignKeyResolver(四态 fail-closed);显式引 data-redis(starter 标 optional 不传递)

This commit is contained in:
chenxing
2026-09-21 00:24:02 +08:00
parent d4bfe8a3b4
commit 6cb0fa1b5b
3 changed files with 114 additions and 1 deletions
+7 -1
View File
@@ -45,12 +45,18 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- 复用 starter 的 SignKeyResolver/SignUtil(传递引入 data-redis);密钥链单一事实源 -->
<!-- 复用 starter 的 SignKeyResolver/SignUtil(密钥链单一事实源);密钥解析永远 fail-closed -->
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-app-auth-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<!-- starter 将 data-redis 标 optional(未引 Redis 的服务回退配置式 Provider),不传递;
网关密钥链必须走 Redis(sign:app:*),按 T0.8 白名单意图显式引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
@@ -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)。
* <p>
* 语义与 starter 完全一致(D4:<b>密钥解析永远 fail-closed</b>):
* 未知 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;
}
}
}
@@ -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();
}
}