diff --git a/pom.xml b/pom.xml
index d980c35..559de03 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,14 @@
com.yunhai.platform
yh-flow-callback-spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
org.jasig.cas.client
diff --git a/src/main/java/com/yunhai/svchall/config/OpenApiAuthEnforceConfiguration.java b/src/main/java/com/yunhai/svchall/config/OpenApiAuthEnforceConfiguration.java
deleted file mode 100644
index 2e7adf3..0000000
--- a/src/main/java/com/yunhai/svchall/config/OpenApiAuthEnforceConfiguration.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.yunhai.svchall.config;
-
-import com.yunhai.platform.appauth.AppAuthFilter;
-import org.springframework.boot.web.servlet.FilterRegistrationBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.Ordered;
-
-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;
-
-/**
- * 开放接口签名强制(照 Cms OpenApiAuthEnforceConfiguration 范式):yh-app-auth 的
- * {@link AppAuthFilter}(order +10,全路由)设计为「无签名头则放行」以兼容浏览器 CAS 流;
- * 本过滤器(order +11)只挂 /oapi/**,校验其写入的请求属性——已验签放行,否则 401。
- * /oapi/svc/**(CMS 供数)无用户会话语义,一律应用签名三件套。
- * 注意:流程中心回调在 /callback/**(不在本过滤器范围),由 yh-flow-callback starter 的 X-Flw 验签过滤器用
- * X-Flw-* 四头验签,两种签名体系互不干扰。
- */
-@Configuration
-public class OpenApiAuthEnforceConfiguration {
-
- @Bean
- public FilterRegistrationBean requireAppSignFilter() {
- FilterRegistrationBean bean = new FilterRegistrationBean<>();
- bean.setFilter(new RequireAppSignFilter());
- bean.addUrlPatterns("/oapi/*");
- bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 11);
- return bean;
- }
-
- public static class RequireAppSignFilter implements Filter {
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- HttpServletRequest req = (HttpServletRequest) request;
- HttpServletResponse res = (HttpServletResponse) response;
- if (req.getAttribute(AppAuthFilter.APP_AUTH_ATTR) == null) {
- res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "missing app signature");
- return;
- }
- chain.doFilter(request, response);
- }
- }
-}
diff --git a/src/main/java/com/yunhai/svchall/entity/ServiceFlowReg.java b/src/main/java/com/yunhai/svchall/entity/ServiceFlowReg.java
index 8920397..9e5ad2c 100644
--- a/src/main/java/com/yunhai/svchall/entity/ServiceFlowReg.java
+++ b/src/main/java/com/yunhai/svchall/entity/ServiceFlowReg.java
@@ -31,7 +31,7 @@ public class ServiceFlowReg extends BaseEntity {
@Column(name = "flow_guid", nullable = false, length = 32)
private String flowGuid;
- /** 回调接收端点(= 本应用对外可达地址 + /callback/flow) */
+ /** 回调接收端点(2026-10-05 起 = 平台推导地址 + /oapi/flow/callback;权威镜像在 starter 表 flow_callback_reg) */
@Column(name = "callback_url", nullable = false, length = 500)
private String callbackUrl;
diff --git a/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java b/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java
index a4a5330..9e080f8 100644
--- a/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java
+++ b/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java
@@ -161,9 +161,10 @@ public class FlowCenterClient implements FlowClient {
body.setFlowGuid(flowGuid);
body.setCallbackUrl(callbackUrl);
body.setEventTypes(eventTypes);
- // 申报本服务 context-path(从回调 URL path 段推导,去掉尾部 /callback/flow):
+ // 申报本服务 context-path(从回调 URL path 段推导,去掉尾部 /oapi/flow/callback):
// 推送方按它剥离后再签应用签名,与本服务验签过滤器剥
// contextPath 后的 target 对齐——本地裸地址(无 ctx)推导为空串,行为不变(不下发空串)。
+ // 生产注册路径由 starter FlowCallbackRegistrar 承担(basePath 与 URL 前缀段同源推导)。
String basePath = deriveCallbackBasePath(callbackUrl);
if (basePath != null && !basePath.isEmpty()) {
body.setBasePath(basePath);
@@ -293,9 +294,9 @@ public class FlowCenterClient implements FlowClient {
}
/**
- * 回调 URL → 接收方 context-path:取 URL path 段去掉尾部 "/callback/flow"
- * (如 /svc-hall/callback/flow → /svc-hall;裸地址 /callback/flow → 空串)。
- * 包可见:契约测试冻结推导形态。
+ * 回调 URL → 接收方 context-path:取 URL path 段去掉尾部 "/oapi/flow/callback"
+ * (如 /svc-hall/oapi/flow/callback → /svc-hall;裸地址 → 空串)。
+ * 包可见:契约测试冻结推导形态(2026-10-05 随回调端点收敛 Notice 形态同步)。
*/
static String deriveCallbackBasePath(String callbackUrl) {
if (callbackUrl == null) {
@@ -306,7 +307,7 @@ public class FlowCenterClient implements FlowClient {
if (path == null || path.isEmpty()) {
return "";
}
- int idx = path.lastIndexOf("/callback/flow");
+ int idx = path.lastIndexOf("/oapi/flow/callback");
if (idx > 0) {
return path.substring(0, idx);
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 5064875..2398235 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -45,7 +45,7 @@ yh:
allow-headers: all
allow-origins: all
# 用户面(发起/我的办件)与管理面全在 /api/* 下;本应用无匿名面。
- # 流程中心回调走 /callback/*(不在 pattern 内,由 FlowCallbackVerifyFilter 验签保护)
+ # 流程中心回调与业务供数都在 /oapi/*(不在 pattern 内,由平台应用签名强制验签保护)
url-pattern: /api/*
app-auth:
enabled: true
@@ -55,35 +55,56 @@ yh:
# 联调态密钥(与 flow-center trusted-apps["100018"] 逐字一致);
# 注意旧占位"裸 32 个 0"会被 YAML 按八进制整数解析成 "0",签名时 SignUtil 校验直接失败,必须带字母+引号
app-secret: "svc-hall-dev-secret-100018"
- # 入站验签白名单:CMS(供数消费方,appKey=100005);流程中心 flow-center 分配 appKey 后在此追加
+ # 提供方强制验签(两级模型 §4.1,flow-center 同款):本地 OpenApiAuthEnforceConfiguration 副本已删,
+ # 换此一行——/oapi/**(含 /oapi/svc/** 供数与 /oapi/flow/callback 回调接收)无签名三件套即 401
+ # (starter 版 RequireAppSignFilter,order +11 行为等价)
+ server:
+ enforce-patterns: [/oapi/*]
+ # 中央密钥链(两级模型 §4.2):trusted-apps 先(确定性兜底),未命中走 SignKeyResolver
+ # (Redis sign:app:* → Caffeine → UserBase 兜底端点)——任何在 UserBase 注册过的 appKey 自动可验。
+ # 需 data-redis 依赖(yh-app-auth 侧 optional 不传递,pom 已显式引入)
+ trust-registry: true
+ # 入站验签白名单(组合链第一层,Redis 停摆时确定性工作):CMS(/oapi/svc/** 消费方,appKey=100005)、
+ # flow-center(回调推送方,appKey=100019 联调态密钥;生产密钥由平台注入)
trusted-apps:
100005: 2c7885de594742cc840b8762910b2d2e
+ "100019": "flow-center-dev-secret-100019"
open:
# 身份密钥唯一来源 = yh.app-auth.app-key/app-secret(本文件 app-auth 段),open.* 副本已删
base-host: http://base.yunhaixingtong.com
- base-path: /userbase
# 服务目录(子应用配置优化方案 §4.2):跨服务出站唯一地址来源,命名目标走 SDK 门面 /
- # YschOpenCall。cms 未晋升 PlatformService 枚举(按名直调);flow-center 已入枚举,此处
- # 显式覆盖为本地端口。完整 URL 拆 host+ctx,per-target base-path 与提供方 context-path
- # 对齐 → 签名天然一致。生产改容器直连:flow-center=http://flow-center-service:10008/flow-center、
+ # YschOpenCall。cms 未晋升 PlatformService 枚举(按名直调);flow-center 已入枚举。
+ # 完整 URL 拆 host+ctx,per-target base-path 与提供方 context-path 对齐 → 签名天然一致。
+ # 生产改容器直连:flow-center=http://flow-center-service:10008/flow-center、
# cms=http://yschcms-service:10005/yschcms。
+ # (base-path /userbase 已删 2026-10-05,对齐 Notice 形态:该键代码默认值即 /userbase,删键无行为变化,
+ # 回调前缀推导读的是 yh.flow-callback.target/base-path 三层回落,与本键无关)
services:
+ # 生产零配置:不配则枚举兜底 = base-host(网关)+/flow-center;此处显式覆盖仅本地开发用
+ # (Notice 同款)。生产如仍被平台层注入容器直连值,属平台配置项(projman yh.open.services 段)。
flow-center: http://localhost:8085
cms: http://base.yunhaixingtong.com:10005/yschcms
- # 流程中心回调接收端 starter(2026-09-20 迁移,X-Flw 验签式):端点保持既有 /callback/flow,
- # 存量注册的 callback_url 不变、零 secret 轮换;幂等/乱序门由 starter 承担,
- # 业务映射见 SvcFlowCallbackHandler;secret 镜像在 flow_callback_reg(迁移 SQL 见 sql/)
+ # 流程中心回调接收端 starter(2026-09-20 迁移;2026-10-05 收敛 Notice 形态):
+ # 端点/幂等/乱序门由 starter 承担,业务映射见 SvcFlowCallbackHandler;
+ # 验签由上方 /oapi/* 平台应用签名强制验签负责(X-Flw 不启用;flow-center 推送三件套+X-Flw 双带,
+ # Notice 线上实证此形态);secret 镜像在 flow_callback_reg(迁移 SQL 见 sql/)
flow-callback:
- path: /callback/flow
+ path: /oapi/flow/callback
biz-types: [svc_item]
- verify-flw-signature: true
- callback-url: ${svchall.public-base-url}/callback/flow
+ # 漂移自动核对(Notice 同款):端点路径/ctx 变更后,镜像行有效但登记 URL 与当前期望不一致时
+ # 自动重新注册(会轮换 secret)——2026-10-05 改路径后 160 存量 /callback/flow 行靠它收敛
+ reconcile-on-register: true
+ # 注册地址不再显式配置(callback-url 已删):由 starter 推导
+ # (open-host ?: yh.open.base-host) + /{前缀} + path,前缀三层回落 target → base-path → 运行时自证
+ # context-path(本仓三层均未显式配置 → 生产 ctx=/svc-hall 自证)。
+ # ⇒ 生产 = http://open-gateway-service:10013/svc-hall/oapi/flow/callback(base-host 平台注入)
+ # 改地址回滚:把全地址写回 callback-url 即可(回滚绳),无需回滚镜像。
# ===== 办事大厅专属配置 =====
svchall:
- # 本应用对外可达基址(回调注册 callbackUrl = {public-base-url}/callback/flow;projman 注入)
- public-base-url: http://localhost:8086
+ # (public-base-url 已删 2026-10-05:唯一消费方是回调注册 callback-url 拼接,回调地址改由
+ # yh.flow-callback 三段推导(见上方该段),本应用地址类配置进一步清零;file.public-base-url 是另一键不受影响)
# 办事大厅前端地址(办件 bizUrl 落地页用)
front:
base-url: http://localhost:8087
@@ -121,6 +142,11 @@ management:
endpoint:
health:
show-details: never
+ # 密钥链验签依赖 Redis(sign:app:*)。关掉 Redis 健康指标:Redis 抖动不应把 /actuator/health
+ # 拉成 DOWN(503),否则部署健康检查与探活会误判整机不可用(Notice/flow-center 同口径)
+ health:
+ redis:
+ enabled: false
# 文件存储磁盘路径(Notice 三件套约定;Docker 部署时与 volume 挂载一致)
FILE_PATH: D:/bsch/svchall/upload/
diff --git a/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientCallbackBasePathTest.java b/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientCallbackBasePathTest.java
index 196332a..e4a28fc 100644
--- a/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientCallbackBasePathTest.java
+++ b/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientCallbackBasePathTest.java
@@ -6,27 +6,34 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* 回调注册 basePath 推导契约:接收方 context-path 从回调 URL path 段去掉尾部
- * /callback/flow 推导(部署带 ctx=签名对齐关键;本地裸地址=空串保持旧形态)。
+ * /oapi/flow/callback 推导(部署带 ctx=签名对齐关键;本地裸地址=空串保持旧形态)。
* 2026-09-16 修复:注册漏带 basePath,flow-center 推送按整路径签名 →
- * FlowCallbackVerifyFilter 剥 ctx 后对不上 → 401(本地无 ctx 掩盖)。
+ * 验签过滤器剥 ctx 后对不上 → 401(本地无 ctx 掩盖)。
+ * 2026-10-05:回调端点收敛 Notice 形态(/oapi/flow/callback),剥尾段口径同步。
*/
class FlowCenterClientCallbackBasePathTest {
@Test
void derive_withContextPath() {
assertEquals("/svc-hall",
- FlowCenterClient.deriveCallbackBasePath("http://svc-hall-service:10007/svc-hall/callback/flow"));
+ FlowCenterClient.deriveCallbackBasePath("http://open-gateway-service:10013/svc-hall/oapi/flow/callback"));
}
@Test
void derive_bareLocalAddressIsEmpty() {
- assertEquals("", FlowCenterClient.deriveCallbackBasePath("http://localhost:8086/callback/flow"));
+ assertEquals("", FlowCenterClient.deriveCallbackBasePath("http://localhost:8086/oapi/flow/callback"));
}
@Test
- void derive_gatewayForm() {
+ void derive_containerDirectForm() {
assertEquals("/svc-hall",
- FlowCenterClient.deriveCallbackBasePath("http://base.yunhaixingtong.com/svc-hall/callback/flow"));
+ FlowCenterClient.deriveCallbackBasePath("http://svc-hall-service:10007/svc-hall/oapi/flow/callback"));
+ }
+
+ @Test
+ void derive_legacyOldEndpointNotStripped() {
+ // 旧端点尾段不再是合法推导输入:不命中剥尾段即返回空串(显式冻结,不静默兼容旧路径)
+ assertEquals("", FlowCenterClient.deriveCallbackBasePath("http://svc-hall-service:10007/svc-hall/callback/flow"));
}
@Test
diff --git a/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientFacadeTest.java b/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientFacadeTest.java
index 4c0e5f1..563c43a 100644
--- a/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientFacadeTest.java
+++ b/src/test/java/com/yunhai/svchall/service/impl/FlowCenterClientFacadeTest.java
@@ -127,14 +127,14 @@ class FlowCenterClientFacadeTest {
any(FlowCallbackRegBody.class), eq(SvcModels.CallbackRegVo.class))).thenReturn(stub);
String secret = client().registerCallback(3L, "guid-1",
- "http://svc-hall-service:10007/svc-hall/callback/flow", null);
+ "http://open-gateway-service:10013/svc-hall/oapi/flow/callback", null);
assertEquals("secret-1", secret);
ArgumentCaptor body = ArgumentCaptor.forClass(FlowCallbackRegBody.class);
verify(openCall).post(eq(TARGET), eq("/oapi/flow/callback/reg"), anyMap(),
body.capture(), eq(SvcModels.CallbackRegVo.class));
assertEquals("/svc-hall", body.getValue().getBasePath());
- assertEquals("http://svc-hall-service:10007/svc-hall/callback/flow", body.getValue().getCallbackUrl());
+ assertEquals("http://open-gateway-service:10013/svc-hall/oapi/flow/callback", body.getValue().getCallbackUrl());
assertNull(body.getValue().getEventTypes());
}