T0.11: GatewayKeyService 薄封装复用 starter SignKeyResolver(四态 fail-closed);显式引 data-redis(starter 标 optional 不传递)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user