T1.6: 观察模式开关 + Filter 链组装(链序定型 台账0→Sanitize1→Routing2→SignVerify3→身份注入4(转发器内)→Dispatch5;/api/internal/** 三环旁路直达 controller;observe=true 坏签名放行、false 裸401 集成对照;400/404 协议层错误不受 observe 影响;关掉 starter AppAuthFilter(正确键 yh.app-auth.enabled=false)避免抢链首误 401)

This commit is contained in:
chenxing
2026-09-21 09:45:52 +08:00
parent 28b9bfca3c
commit 10374abd6d
7 changed files with 602 additions and 16 deletions
@@ -0,0 +1,116 @@
package com.yunhai.platform.gateway.filter;
import com.yunhai.platform.gateway.log.AccessLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 台账采集(T1.6 链序第 0 环,最外层):只记 oapi 请求(网关自身 {@code /api/internal/**} 不记),
* 出口状态取网关真实响应码,costMs 取端到端耗时。
* <p>
* 转发是异步的(T1.3 {@code AsyncContext})——请求线程提前返回时响应尚未完成,
* 此时挂 {@link AsyncListener} 在 complete 时记录,保证台账 status 不是"半途值";
* 记录动作本身在 {@link AccessLogService} 内只入队,绝不拖慢请求。
*/
@Component
@Order(0)
public class AccessLogFilter implements Filter {
private static final String ATTR_START_NANOS = "yh.gateway.start.nanos";
@Autowired
private AccessLogService accessLogService;
/** 测试/显式装配入口 */
void setAccessLogService(AccessLogService accessLogService) {
this.accessLogService = accessLogService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if (RoutingFilter.isGatewayInternal(RoutingFilter.sanitizedUri(req))) {
chain.doFilter(request, response); // 网关自身端点不进 open_api 台账
return;
}
req.setAttribute(ATTR_START_NANOS, System.nanoTime());
try {
chain.doFilter(request, response);
} finally {
if (req.isAsyncStarted()) {
req.getAsyncContext().addListener(new LedgerListener(req, res));
} else {
record(req, res);
}
}
}
private void record(HttpServletRequest req, HttpServletResponse res) {
Object start = req.getAttribute(ATTR_START_NANOS);
int costMs = start instanceof Long ? (int) ((System.nanoTime() - (Long) start) / 1_000_000L) : 0;
// backendCode:流式转发零缓冲(T1.3),本期不解析提供方信封 code → null
accessLogService.recordRequest(req, res.getStatus(), null, costMs);
}
/** 异步收尾(complete/timeout/error)时记一次,重复触发由 claimed 挡住 */
private class LedgerListener implements AsyncListener {
private final HttpServletRequest req;
private final HttpServletResponse res;
private final java.util.concurrent.atomic.AtomicBoolean claimed =
new java.util.concurrent.atomic.AtomicBoolean();
LedgerListener(HttpServletRequest req, HttpServletResponse res) {
this.req = req;
this.res = res;
}
@Override
public void onComplete(AsyncEvent event) {
recordOnce();
}
@Override
public void onTimeout(AsyncEvent event) {
recordOnce();
}
@Override
public void onError(AsyncEvent event) {
recordOnce();
}
@Override
public void onStartAsync(AsyncEvent event) {
// 网关不嵌套异步:忽略
}
private void recordOnce() {
if (claimed.compareAndSet(false, true)) {
try {
record(req, res);
} catch (RuntimeException e) {
// 台账故障绝不影响请求收尾
}
}
}
}
}
@@ -0,0 +1,61 @@
package com.yunhai.platform.gateway.filter;
import com.yunhai.platform.gateway.envelope.EnvelopeWriter;
import com.yunhai.platform.gateway.forward.StreamingForwarder;
import com.yunhai.platform.gateway.route.TargetRouter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 链尾转发(T1.6 链序第 5 环):oapi 请求到此一律交给 {@link StreamingForwarder}
* 流式送提供方并原样回传,链在此终结(不再回到网关的 DispatcherServlet)。
* <p>
* 身份注入(链序第 4 环)在转发器内完成(T1.4 {@code CallerHeader}),所以本环只需带上
* 请求属性。缺路由 attribute(未经 RoutingFilter 的异常路径)按 404 收口,绝不裸转。
* 超时/体积暂无 per-route 覆盖(RouteSpec 不含该列),用转发器默认值。
*/
@Component
@Order(5)
public class DispatchFilter implements Filter {
@Autowired
private StreamingForwarder forwarder;
/** 测试/显式装配入口 */
void setForwarder(StreamingForwarder forwarder) {
this.forwarder = forwarder;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String uri = RoutingFilter.sanitizedUri(req);
if (RoutingFilter.isGatewayInternal(uri)) {
chain.doFilter(request, response); // 自身端点:交给 controller,不经提供方
return;
}
Object routing = req.getAttribute(RoutingFilter.ATTR_ROUTING);
if (!(routing instanceof TargetRouter.Routing)) {
EnvelopeWriter.writeBare(res, 404);
return;
}
forwarder.forward(req, res, ((TargetRouter.Routing) routing).forwardUrl,
StreamingForwarder.DEFAULT_READ_TIMEOUT_MS, StreamingForwarder.DEFAULT_BODY_LIMIT_BYTES);
}
}
@@ -0,0 +1,89 @@
package com.yunhai.platform.gateway.filter;
import com.yunhai.platform.gateway.envelope.EnvelopeWriter;
import com.yunhai.platform.gateway.route.RouteRegistry;
import com.yunhai.platform.gateway.route.RouteSpec;
import com.yunhai.platform.gateway.route.TargetRouter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 路由判定(T1.6 链序第 2 环):Sanitize 之后、验签之前,把 URI 定到 target 与提供方
* ({@link TargetRouter},未知 target / 非 oapi 第二段 = <b>裸 404</b>,协议层错误永不放行),
* 并取注册表最严策略({@link RouteRegistry})存入 attribute 供后续环节消费。
* <p>
* {@code /api/internal/**} 属网关自身路径(如 T0.10 刷新端点),不进 oapi 链、不验签、不转发。
*/
@Component
@Order(2)
public class RoutingFilter implements Filter {
/** 网关自身管理端点前缀:绕开 oapi 判定链(T0.10 /api/internal/routes/refresh) */
public static final String INTERNAL_PREFIX = "/api/internal";
/** 路由产物 attribute({@link TargetRouter.Routing}) */
public static final String ATTR_ROUTING = "yh.gateway.routing";
/** 注册表策略 attribute({@link RouteSpec}) */
public static final String ATTR_ROUTE_SPEC = "yh.gateway.route.spec";
@Autowired
private TargetRouter targetRouter;
@Autowired
private RouteRegistry routeRegistry;
/** 测试/显式装配入口 */
void setTargetRouter(TargetRouter targetRouter) {
this.targetRouter = targetRouter;
}
void setRouteRegistry(RouteRegistry routeRegistry) {
this.routeRegistry = routeRegistry;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String uri = sanitizedUri(req);
if (isGatewayInternal(uri)) {
chain.doFilter(request, response); // 自身端点:直达 controller
return;
}
TargetRouter.Routing routing = targetRouter.route(uri, req.getQueryString());
if (routing == null) {
EnvelopeWriter.writeBare(res, 404); // 404 属协议层,与 observe 无关
return;
}
req.setAttribute(ATTR_ROUTING, routing);
req.setAttribute(ATTR_ROUTE_SPEC,
routeRegistry.resolve(routing.target, routing.pathAfterStrip, req.getMethod()));
chain.doFilter(request, response);
}
/** 一律消费 Sanitize 归一化后的原始 URI;缺 attribute(未经 Sanitize)时退回 getRequestURI */
static String sanitizedUri(HttpServletRequest req) {
Object attr = req.getAttribute(SanitizeFilter.ATTR_SANITIZED_URI);
return attr instanceof String ? (String) attr : req.getRequestURI();
}
static boolean isGatewayInternal(String uri) {
return uri != null
&& (uri.equals(INTERNAL_PREFIX) || uri.startsWith(INTERNAL_PREFIX + "/"));
}
}
@@ -75,6 +75,10 @@ public class SignVerifyFilter implements Filter {
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if (RoutingFilter.isGatewayInternal(RoutingFilter.sanitizedUri(req))) {
chain.doFilter(request, response); // 网关自身端点不经 oapi 验签
return;
}
boolean pass = false;
String appKey = req.getHeader("X-App-Key");
+19 -16
View File
@@ -22,9 +22,10 @@ spring:
batch_size: 200
order_inserts: true
redis:
# 密钥链(sign:app:{appKey})走共享 Redis db0——由平台配置层注入
host: #
port: #
# 密钥链(sign:app:{appKey})走共享 Redis db0——host/port 由平台配置层注入。
# 注意:jar 内只留注释,不留 `host:` 空键(空值绑不到 int 会让 context 起不来)
# host: <平台配置层>
# port: <平台配置层>
main:
allow-bean-definition-overriding: true
@@ -39,20 +40,22 @@ management:
enabled: true
yh:
app:
auth:
# starter 验签配置(T0.11 起 GatewayKeyService 复用 SignKeyResolver)
# include/exclude 路径策略由平台配置层下发;本文件不配任何真实值
enabled: # 默认随 starter
# starter 的服务端验签过滤器(AppAuthFilter,注册在 HIGHEST_PRECEDENCE+10)必须关掉:
# 网关自身的 SignVerifyFilter(T1.2) 是唯一验签面,否则它会抢在链首把合法流量按
# "完整 URI 含 target 段"验签 → 全部 401。注意前缀是 yh.app-auth(不是 yh.app.auth)。
# 密钥链 SignKeyRedisAutoConfiguration 独立装配,不受此开关影响(GatewayKeyService 仍可用)。
app-auth:
enabled: false
gateway:
# 路由表(T0.12):target -> base-url,值 = 容器网内服务直连 URL,走平台配置层
routes: #
# userbase: http://userbase-service:8080
# userclass: http://userclass-service:8080
# cms: http://cms-service:8080
# svc-hall: http://svc-hall-service:8080
# flow-center: http://flow-center-service:8080
# usermsg: http://usermsg-service:8080
# notice: http://notice-service:8080
# (只留注释不留空键:空值会让 Binder 把 Map 绑成 null)
# routes:
# userbase: http://userbase-service:8080
# userclass: http://userclass-service:8080
# cms: http://cms-service:8080
# svc-hall: http://svc-hall-service:8080
# flow-center: http://flow-center-service:8080
# usermsg: http://usermsg-service:8080
# notice: http://notice-service:8080
# 观察模式(T1.6):true = 全判定只记不拦(R4:P1 只观察);生产初期保持 true
observe: true
@@ -0,0 +1,182 @@
package com.yunhai.platform.gateway.chain;
import com.sun.net.httpserver.HttpServer;
import com.yunhai.platform.gateway.auth.GatewayKeyService;
import com.yunhai.platform.gateway.entity.OpenApiLog;
import com.yunhai.platform.gateway.repository.OpenApiLogRepository;
import com.yunhai.platform.utils.crypto.SignUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
/**
* T1.6 链序集成测试(observe=true 默认):Sanitize(0/1) → Routing(2) → SignVerify(3) →
* 身份注入(4,转发器内) → Dispatch(5) 全链走通——假后端收到注入的 X-Caller-App-Key、
* 伪造值被剥、响应原样透传、台账异步入库;observe 语义边界:坏签名放行、
* 但协议层错误(400/404)不受 observe 影响永远拦;/api/internal/** 自身端点不被链拦截。
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class GatewayChainTest {
private static final String SECRET = "sec-chain-0123456789abcdef";
private static final HttpServer BACKEND;
private static final int BACKEND_PORT;
static final AtomicReference<String> CALLER_SEEN = new AtomicReference<>();
static final AtomicReference<String> URI_SEEN = new AtomicReference<>();
static {
try {
BACKEND = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
BACKEND.setExecutor(Executors.newFixedThreadPool(4));
BACKEND.createContext("/", exchange -> {
CALLER_SEEN.set(exchange.getRequestHeaders().getFirst("X-Caller-App-Key"));
URI_SEEN.set(exchange.getRequestURI().toString());
byte[] body = "pong".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("X-Upstream", "v1");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(body);
}
exchange.close();
});
BACKEND.start();
BACKEND_PORT = BACKEND.getAddress().getPort();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@DynamicPropertySource
static void routes(DynamicPropertyRegistry registry) {
registry.add("yh.gateway.routes.demo", () -> "http://127.0.0.1:" + BACKEND_PORT);
}
@Autowired
TestRestTemplate rest;
@Value("${local.server.port}")
int port;
@Autowired
OpenApiLogRepository logRepository;
@MockBean
GatewayKeyService keyService;
@BeforeEach
void setUp() {
logRepository.deleteAll();
CALLER_SEEN.set(null);
URI_SEEN.set(null);
when(keyService.getSecret("100617")).thenReturn(SECRET);
}
private HttpHeaders signedHeaders() {
return signedHeaders("/oapi/hello");
}
/** 客户端签名口径 = 剥掉 target 段后的 path[?query](F1),故 query 必须进 canonical */
private HttpHeaders signedHeaders(String pathToSign) {
String ts = String.valueOf(System.currentTimeMillis());
HttpHeaders h = new HttpHeaders();
h.add("X-App-Key", "100617");
h.add("X-Timestamp", ts);
h.add("X-Signature", SignUtil.signRequest("GET", pathToSign, ts, SECRET));
return h;
}
/** 台账异步落库(500ms 周期 + 异步 complete),轮询等待 */
private List<OpenApiLog> awaitLedger() throws InterruptedException {
for (int i = 0; i < 100; i++) {
List<OpenApiLog> all = logRepository.findAll();
if (!all.isEmpty()) {
return all;
}
Thread.sleep(50);
}
return logRepository.findAll();
}
@Test
void 全链走通_身份注入_响应透传_台账入库() throws Exception {
ResponseEntity<String> resp = rest.exchange("/demo/oapi/hello", HttpMethod.GET,
new HttpEntity<>(signedHeaders()), String.class);
assertThat(resp.getStatusCodeValue()).isEqualTo(200);
assertThat(resp.getBody()).as("提供方响应体原样透传").isEqualTo("pong");
assertThat(resp.getHeaders().getFirst("X-Upstream")).as("提供方响应头原样透传").isEqualTo("v1");
assertThat(URI_SEEN.get()).as("换主机不换路径").isEqualTo("/demo/oapi/hello");
assertThat(CALLER_SEEN.get()).as("链序终值 = 网关注入的验签 appKey").isEqualTo("100617");
List<OpenApiLog> ledger = awaitLedger();
assertThat(ledger).as("台账入库").isNotEmpty();
OpenApiLog row = ledger.get(0);
assertThat(row.getTargetCode()).isEqualTo("demo");
assertThat(row.getPath()).isEqualTo("/oapi/hello");
assertThat(row.getCallerAppKey()).isEqualTo("100617");
assertThat(row.getStatus()).isEqualTo(200);
}
@Test
void 伪造身份头被剥_终值为网关注入() throws Exception {
HttpHeaders h = signedHeaders();
h.add("X-Caller-App-Key", "hack");
ResponseEntity<String> resp = rest.exchange("/demo/oapi/hello", HttpMethod.GET,
new HttpEntity<>(h), String.class);
assertThat(resp.getStatusCodeValue()).isEqualTo(200);
assertThat(CALLER_SEEN.get()).as("先剥后注入:链上终值唯一").isEqualTo("100617");
}
@Test
void observe为true坏签名放行且台账有记录() throws Exception {
String ts = String.valueOf(System.currentTimeMillis());
HttpHeaders h = new HttpHeaders();
h.add("X-App-Key", "100617");
h.add("X-Timestamp", ts);
h.add("X-Signature", "deadbeef");
ResponseEntity<String> resp = rest.exchange("/demo/oapi/hello", HttpMethod.GET,
new HttpEntity<>(h), String.class);
assertThat(resp.getStatusCodeValue()).as("R4:P1 只观察不拦").isEqualTo(200);
assertThat(CALLER_SEEN.get()).as("FAIL_OBSERVED 不注入身份").isNull();
List<OpenApiLog> ledger = awaitLedger();
assertThat(ledger).as("坏签名也进台账(caller 尽力记)").isNotEmpty();
assertThat(ledger.get(0).getCallerAppKey()).isEqualTo("100617");
}
@Test
void observe不影响协议层错误_400与404永远拦() throws Exception {
// 原始 URI 直发(不经客户端归一化):编码形态 %2e%2e 解码后是 ..,Sanitize 必裸 400
URI traversal = new URI("http://127.0.0.1:" + port + "/demo/oapi/a/%2e%2e/b");
assertThat(rest.getForEntity(traversal, String.class).getStatusCodeValue())
.as("Sanitize 400 不受 observe 影响").isEqualTo(400);
assertThat(rest.getForEntity("/noway/oapi/x", String.class).getStatusCodeValue())
.as("未知 target 404 不受 observe 影响").isEqualTo(404);
}
@Test
void 网关自身internal端点不被链拦_refresh可用() {
ResponseEntity<String> resp = rest.postForEntity("/api/internal/routes/refresh", null, String.class);
assertThat(resp.getStatusCodeValue()).as("自身端点走 controller,不进 oapi 链").isEqualTo(200);
}
}
@@ -0,0 +1,131 @@
package com.yunhai.platform.gateway.chain;
import com.sun.net.httpserver.HttpServer;
import com.yunhai.platform.gateway.auth.GatewayKeyService;
import com.yunhai.platform.gateway.entity.OpenApiLog;
import com.yunhai.platform.gateway.repository.OpenApiLogRepository;
import com.yunhai.platform.utils.crypto.SignUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
/**
* T1.6 observe=false(收口形态)对照用例:坏签名不再只记,而是<b>裸 401</b> 拦下、
* 请求不到提供方;台账仍记录该次判定(status=401,caller 尽力记 appKey)。
* 与 {@link GatewayChainTest} 分属两个 Spring context(observe 属性不同)。
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "yh.gateway.observe=false")
@ActiveProfiles("test")
class GatewayEnforceChainTest {
private static final String SECRET = "sec-enforce-0123456789abc";
private static final HttpServer BACKEND;
private static final int BACKEND_PORT;
static final AtomicReference<String> CALLER_SEEN = new AtomicReference<>();
static {
try {
BACKEND = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
BACKEND.setExecutor(Executors.newFixedThreadPool(2));
BACKEND.createContext("/", exchange -> {
CALLER_SEEN.set(exchange.getRequestHeaders().getFirst("X-Caller-App-Key"));
byte[] body = "pong".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(body);
}
exchange.close();
});
BACKEND.start();
BACKEND_PORT = BACKEND.getAddress().getPort();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@DynamicPropertySource
static void routes(DynamicPropertyRegistry registry) {
registry.add("yh.gateway.routes.demo", () -> "http://127.0.0.1:" + BACKEND_PORT);
}
@Autowired
TestRestTemplate rest;
@Autowired
OpenApiLogRepository logRepository;
@MockBean
GatewayKeyService keyService;
@BeforeEach
void setUp() {
logRepository.deleteAll();
CALLER_SEEN.set(null);
when(keyService.getSecret("100617")).thenReturn(SECRET);
}
private List<OpenApiLog> awaitLedger() throws InterruptedException {
for (int i = 0; i < 100; i++) {
List<OpenApiLog> all = logRepository.findAll();
if (!all.isEmpty()) {
return all;
}
Thread.sleep(50);
}
return logRepository.findAll();
}
@Test
void 坏签名observe为false裸401且不到提供方() throws Exception {
HttpHeaders h = new HttpHeaders();
h.add("X-App-Key", "100617");
h.add("X-Timestamp", String.valueOf(System.currentTimeMillis()));
h.add("X-Signature", "deadbeef");
ResponseEntity<String> resp = rest.exchange("/demo/oapi/hello", HttpMethod.GET,
new HttpEntity<>(h), String.class);
assertThat(resp.getStatusCodeValue()).as("收口形态:裸 401").isEqualTo(401);
assertThat(resp.getBody()).as("裸码无信封").isNullOrEmpty();
assertThat(CALLER_SEEN.get()).as("请求不得送达提供方").isNull();
List<OpenApiLog> ledger = awaitLedger();
assertThat(ledger).as("拦截同样进台账").isNotEmpty();
assertThat(ledger.get(0).getStatus()).isEqualTo(401);
assertThat(ledger.get(0).getCallerAppKey()).isEqualTo("100617");
}
@Test
void 好签名observe为false正常放行() throws Exception {
HttpHeaders h = new HttpHeaders();
h.add("X-App-Key", "100617");
String ts = String.valueOf(System.currentTimeMillis());
h.add("X-Timestamp", ts);
h.add("X-Signature", SignUtil.signRequest("GET", "/oapi/hello", ts, SECRET));
ResponseEntity<String> resp = rest.exchange("/demo/oapi/hello", HttpMethod.GET,
new HttpEntity<>(h), String.class);
assertThat(resp.getStatusCodeValue()).isEqualTo(200);
assertThat(CALLER_SEEN.get()).isEqualTo("100617");
}
}