From 30b4b0504a0ee115d89f1ef0d4e6100480d029d0 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 16 Sep 2026 18:08:25 +0800 Subject: [PATCH] =?UTF-8?q?oapi=E9=AA=8C=E7=AD=BE=E9=93=BE=E6=B2=BB?= =?UTF-8?q?=E7=90=86=E2=91=A2=EF=BC=9A=E5=88=A0=E9=99=A4=20/oapi/internal/?= =?UTF-8?q?sign-key=20=E5=85=9C=E5=BA=95=E7=AB=AF=E7=82=B9=EF=BC=8Coapi=20?= =?UTF-8?q?=E5=8F=AF=E6=95=B4=E6=AE=B5=E5=AF=B9=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删 SignKeyInternalController(/oapi/internal/sign-key/{appId},返回 appSecret)、 SignKeyInternalIpFilter、SignKeyInternalProperties 三文件 - application.yml 删 yh.sign-key-internal 块(其中 192.168.10.0/200 本就是非法前缀,永不匹配) - OpenApiAuthEnforceConfiguration:删 IP 白名单过滤器与 SignKeyInternalProperties 注册; **保留 /api/open 与 /oapi/* 的强制签名**(② oapi 整段对外代理依赖它) - OapiLogAspect:删对 SignKeyInternalController 的排除死代码 - SignKeyCacheService 契约固化:验签密钥以共享 Redis sign:app:* 为单一真源、UserBase 唯一写入方、 读写走密钥链专用库(缺省 db0)、读侧无兜底(Redis 不可用即 fail-closed) - 实测线上该端点在容器内匿名访问为 403(IP 白名单生效)、公网无 Caddy 路由; 删除后既无密钥外泄面,也使 /userbase/oapi/* 整段代理安全 验证:mvn compile 通过;本模块单测 124 项中 5 项 MySQL 连接失败为环境性(已用 git stash 在 原始树复跑证明同样失败)。 --- .../yunhai/userbase/aspect/OapiLogAspect.java | 8 -- .../OpenApiAuthEnforceConfiguration.java | 21 +-- .../config/SignKeyInternalIpFilter.java | 124 ------------------ .../config/SignKeyInternalProperties.java | 55 -------- .../oapi/SignKeyInternalController.java | 70 ---------- .../service/impl/SignKeyCacheService.java | 17 ++- src/main/resources/application.yml | 6 - 7 files changed, 14 insertions(+), 287 deletions(-) delete mode 100644 src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java delete mode 100644 src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java delete mode 100644 src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java diff --git a/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java index b9027a6..961e8c7 100644 --- a/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java +++ b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java @@ -40,7 +40,6 @@ import java.util.Map; * 拦截 {@code controller.oapi} 包下所有方法,规则:
* - POST 写操作:全量自动记录(无需注解),级别/类型由 OperationLogResolver 关键词判定;
* - GET 查询:默认不记录(高频调用,防刷爆表),满足任一条件则记录:方法标注 {@code @OapiLog}、或配置 oapi.log-get=true;
- * - 排除 {@code SignKeyInternalController}(/oapi/internal,响应含 appSecret 明文,绝不落库);
* - 请求参数与响应均经 {@link SensitiveDataMasker} 脱敏后入库;
* - finally 中异步落库(有界池 oapiLogExecutor),失败不影响主业务。
* @@ -52,9 +51,6 @@ import java.util.Map; @Component public class OapiLogAspect { - /** 内部密钥端点控制器,排除(响应含 appSecret 明文)。 */ - private static final String INTERNAL_CONTROLLER = "com.yunhai.userbase.controller.oapi.SignKeyInternalController"; - /** GET 默认不记录,开启本配置后全量记录。 */ @Value("${oapi.log-get:false}") private boolean logGet; @@ -67,10 +63,6 @@ public class OapiLogAspect { @Around("execution(* com.yunhai.userbase.controller.oapi..*.*(..))") public Object around(ProceedingJoinPoint point) throws Throwable { - if (INTERNAL_CONTROLLER.equals(point.getSignature().getDeclaringTypeName())) { - return point.proceed(); - } - HttpServletRequest request = getRequest(); String httpMethod = request != null ? request.getMethod() : ""; boolean isGet = "GET".equalsIgnoreCase(httpMethod); diff --git a/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java b/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java index 2779fac..a1e94b0 100644 --- a/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java +++ b/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java @@ -25,8 +25,8 @@ import java.io.IOException; * 强制开放接口必须带应用签名 + UserBase 自身 CAS 认证兜底的 UserDetailsProvider。
*
* yh-app-auth 的 {@link AppAuthFilter} 设计为「无签名头则放行」(以兼容浏览器 CAS 流), - * 但 UserBase 作为用户中心的开放接口({/open/api/**})与内部密钥兜底端点({/oapi/internal/**}) - * 绝不允许匿名访问。本过滤器在 {@code AppAuthFilter} 之后运行,校验其写入的请求属性: + * 但 UserBase 作为用户中心的开放接口({/open/api/**} 与 {/oapi/**})绝不允许匿名访问。 + * 本过滤器在 {@code AppAuthFilter} 之后运行,校验其写入的请求属性: * 已验签(属性存在)则放行,否则返回 401。
* 顺序须晚于 {@code AppAuthFilter}(HIGHEST_PRECEDENCE+10),故置为 +11。
*
@@ -39,7 +39,7 @@ import java.io.IOException; * 导致其 @ConditionalOnMissingBean 抢先注册 NoOp,PathBasedAuthInterceptor 注入后首次请求即抛错。 */ @Configuration -@EnableConfigurationProperties({ SignKeyInternalProperties.class, AppAuthProperties.class }) +@EnableConfigurationProperties({ AppAuthProperties.class }) public class OpenApiAuthEnforceConfiguration { @Bean @@ -51,21 +51,6 @@ public class OpenApiAuthEnforceConfiguration { return bean; } - /** - * 内部开放接口(/oapi/internal/*)来源 IP 白名单: - * 在验签过滤器之前先执行(HIGHEST_PRECEDENCE + 9),先卡掉公网来源再进行后续处理。 - * 配置项 yh.sign-key-internal.ips / enabled 可由 Nacos 下发。 - */ - @Bean - public FilterRegistrationBean signKeyInternalIpFilter( - SignKeyInternalProperties properties) { - FilterRegistrationBean bean = new FilterRegistrationBean<>(); - bean.setFilter(new SignKeyInternalIpFilter(properties)); - bean.addUrlPatterns("/oapi/internal/*"); - bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 9); - return bean; - } - /** * UserBase 本地 CAS 登录用户提供者:按登录名/手机号查 UserBase 自身库返回 BoaUser。
* 加 {@code @Primary} 稳定覆盖 yh-auth 默认 NoOp 实现;bean 名固定=userDetailsProvider, diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java deleted file mode 100644 index 2359640..0000000 --- a/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.yunhai.userbase.config; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.CollectionUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.List; - -/** - * 内部开放接口(/oapi/internal/*)来源 IP 白名单过滤器。

- * 防止密钥兜底端点(/oapi/internal/sign-key/{appId})被公网直接枚举调用; - * 只允许内网 / 显式配置的白名单来源通过,未命中返回 403。

- * 支持 X-Forwarded-For / X-Real-IP 等常见代理头解析,取最左侧真实来源 IP 做校验。 - */ -public class SignKeyInternalIpFilter extends OncePerRequestFilter { - - private static final Logger log = LoggerFactory.getLogger(SignKeyInternalIpFilter.class); - - private final SignKeyInternalProperties properties; - - public SignKeyInternalIpFilter(SignKeyInternalProperties properties) { - this.properties = properties; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - if (!properties.isEnabled()) { - filterChain.doFilter(request, response); - return; - } - String sourceIp = resolveSourceIp(request); - List effectiveIps = properties.getEffectiveIps(); - if (CollectionUtils.isEmpty(effectiveIps) || matchAny(sourceIp, effectiveIps)) { - filterChain.doFilter(request, response); - return; - } - log.warn("[SignKeyInternal] 拒绝非白名单来源访问内部接口: uri={}, sourceIp={}, whitelist={}", - request.getRequestURI(), sourceIp, effectiveIps); - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":403,\"msg\":\"内部接口不允许从该来源访问\",\"data\":null}"); - response.getWriter().flush(); - } - - private static String resolveSourceIp(HttpServletRequest request) { - String[] headers = new String[] { - "X-Forwarded-For", - "X-Real-IP", - "Proxy-Client-IP", - "WL-Proxy-Client-IP", - "HTTP_CLIENT_IP", - "HTTP_X_FORWARDED_FOR" - }; - for (String header : headers) { - String raw = request.getHeader(header); - if (raw == null || raw.isEmpty() || "unknown".equalsIgnoreCase(raw)) { - continue; - } - // X-Forwarded-For 可能是 多个IP逗号分隔,取最左侧 - String first = raw.split(",")[0].trim(); - if (!first.isEmpty()) { - return first; - } - } - return request.getRemoteAddr(); - } - - private static boolean matchAny(String ip, List cidrs) { - for (String cidr : cidrs) { - if (matchCidr(ip, cidr)) { - return true; - } - } - return false; - } - - private static boolean matchCidr(String ip, String cidr) { - if (ip == null || cidr == null) { - return false; - } - try { - int prefixLen; - String network; - if (cidr.contains("/")) { - String[] parts = cidr.split("/"); - network = parts[0].trim(); - prefixLen = Integer.parseInt(parts[1].trim()); - } else { - network = cidr.trim(); - // 判断地址族决定默认前缀长度 - InetAddress na = InetAddress.getByName(network); - prefixLen = (na instanceof Inet6Address) ? 128 : 32; - } - InetAddress ipAddr = InetAddress.getByName(ip); - InetAddress netAddr = InetAddress.getByName(network); - if (ipAddr.getClass() != netAddr.getClass()) { - return false; - } - byte[] ipBytes = ipAddr.getAddress(); - byte[] netBytes = netAddr.getAddress(); - int bits = prefixLen; - for (int i = 0; i < ipBytes.length && bits > 0; i++) { - int mask = bits >= 8 ? 0xFF : (0xFF << (8 - bits)); - if ((ipBytes[i] & mask) != (netBytes[i] & mask)) { - return false; - } - bits -= 8; - } - return true; - } catch (UnknownHostException | NumberFormatException ignored) { - return false; - } - } -} diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java deleted file mode 100644 index f5eb76b..0000000 --- a/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.yunhai.userbase.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * 内部开放接口(/oapi/internal/*)访问白名单 IP 配置。
- * 支持单 IP、CIDR(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1/32)。
- *

- * 可在 Nacos 上配置,例如:

- * yh:
- *   sign-key-internal:
- *     ips:
- *       - 10.0.0.0/8
- *       - 172.16.0.0/12
- *       - 192.168.0.0/16
- *       - 127.0.0.1/32
- *       - 172.20.10.5/32   # 某台固定机器
- * 
- * 未配置时,默认允许 10/172.16-31/192.168 私有段 + 127.0.0.1(方便本地开发/内网部署)。 - */ -@ConfigurationProperties(prefix = "yh.sign-key-internal") -public class SignKeyInternalProperties { - - /** 显式允许的来源 IP(CIDR 或 单 IP)。为空时使用默认私有段规则。 */ - private List ips = new ArrayList<>(); - - /** 是否启用白名单校验;设置 false 可临时关闭(生产不建议)。 */ - private boolean enabled = true; - - public List getIps() { return ips; } - - public void setIps(List ips) { this.ips = ips; } - - public boolean isEnabled() { return enabled; } - - public void setEnabled(boolean enabled) { this.enabled = enabled; } - - /** 未配置 ips 时返回默认私有段 + localhost 规则。 */ - public List getEffectiveIps() { - if (ips != null && !ips.isEmpty()) { - return ips; - } - return Arrays.asList( - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "127.0.0.1/32", - "0:0:0:0:0:0:0:1/128" - ); - } -} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java b/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java deleted file mode 100644 index b435789..0000000 --- a/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.yunhai.userbase.controller.oapi; - -import com.yunhai.platform.appauth.AppAuthProperties; -import com.yunhai.userbase.config.Config; -import com.yunhai.userbase.entity.ApplicationInfo; -import com.yunhai.userbase.repository.ApplicationInfoRepository; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * 验签兜底端点:供其他业务服务在「共享 Redis 宕机且本地缓存也未命中」时直连获取某应用的密钥。

- * 直接从 MySQL 读(绕过可能同宕的共享 Redis),是密钥的权威源;写入侧仍由 - * {@code SignKeyCacheService} 同步到 Redis。返回形状与 BasePlatform 读侧解析契约一致:
- * {@code {"appKey":"100616","secret":"...","enabled":true}}。

- * 解析优先级:
- * 1) 本库 application_info(覆盖全部「已颁发」应用的实时密钥);
- * 2) 配置式受信应用 {@code yh.app-auth.trusted-apps}(平台自身默认密钥,如 应用中心 100616, - * 即便尚未落库也可解析,避免「默认 appKey 未建行」时此处返回 404 导致验签链路失败);
- * 两者皆无 → 404。

- * 注意:此端点返回的是应用密钥,必须仅内网可达(由部署网络隔离保障),切勿暴露到公网。 - */ -@RestController -@RequestMapping(Config.OPEN_API_PATH + "internal") -public class SignKeyInternalController { - - private final ApplicationInfoRepository applicationInfoRepository; - private final AppAuthProperties appAuthProperties; - - public SignKeyInternalController(ApplicationInfoRepository applicationInfoRepository, - AppAuthProperties appAuthProperties) { - this.applicationInfoRepository = applicationInfoRepository; - this.appAuthProperties = appAuthProperties; - } - - @GetMapping("sign-key/{appId}") - public ResponseEntity> signKey(@PathVariable String appId) { - Integer key; - try { - key = Integer.parseInt(appId.trim()); - } catch (NumberFormatException e) { - return ResponseEntity.notFound().build(); - } - ApplicationInfo info = applicationInfoRepository.findByAppKey(key); - if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { - return ok(info.getAppKey() == null ? appId : String.valueOf(info.getAppKey()), - info.getAppSecret()); - } - // 库内无此应用密钥:回退到配置式受信应用(平台自身默认密钥 100616 等), - // 避免「应用中心默认 appKey 未落库」时兜底查询返回 404。 - String cfgSecret = appAuthProperties.getTrustedApps().get(appId); - if (cfgSecret != null && !cfgSecret.trim().isEmpty()) { - return ok(appId, cfgSecret); - } - return ResponseEntity.notFound().build(); - } - - private static ResponseEntity> ok(String appKey, String secret) { - Map body = new LinkedHashMap<>(); - body.put("appKey", appKey); - body.put("secret", secret); - body.put("enabled", true); - return ResponseEntity.ok(body); - } -} diff --git a/src/main/java/com/yunhai/userbase/service/impl/SignKeyCacheService.java b/src/main/java/com/yunhai/userbase/service/impl/SignKeyCacheService.java index 421584a..f10d0f8 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/SignKeyCacheService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/SignKeyCacheService.java @@ -25,16 +25,21 @@ import java.util.Map; import java.util.Set; /** - * 应用签名密钥的 Redis 桥接服务。
+ * 应用签名密钥的 Redis 桥接服务——验签密钥以共享 Redis {@code sign:app:*} 为单一真源。
*
* 职责:把 {@code application_info} 表的 appKey/appSecret 同步到 Redis,供各业务服务的 - * common-sign-starter(SignKeyResolver)读取做验签——业务服务无需直连 UserBase 数据库。
+ * yh-app-auth starter(SignKeyResolver)读取做验签——业务服务无需直连 UserBase 数据库。
*
- * 约定(读侧 starter 必须严格一致):
+ * 契约(读侧 starter 必须严格一致):
* 1) key 前缀 {@code sign:app:},后接 appKey,例 {@code sign:app:100616};
* 2) value 为 fastjson 序列化的 JSON * 字符串:{@code {"appKey":"100616","secret":"...","enabled":true}};
* 3) 不设过期时间,依赖 Redis 持久化(AOF/RDB)保证重启不丢;删除应用时直接删 key(等价于禁用)。
+ * 4) UserBase 是唯一写入方(启动 {@link #refreshAll()} + CRUD 事务提交后同步 + 每日对账), + * 各提供方只读;读写都走密钥链专用库({@code yh.app-auth.sign-redis.database},缺省 db0), + * 与应用主库解耦——否则写入方与读取方分落不同库,链路静默 401。
+ * 5) 读侧 fail-closed:Redis 不可用即拒绝(已无本地缓存与远端兜底端点)。 + * 恢复梯级:AOF 回放 → 必要时重启 UserBase 触发 {@link #refreshAll()} 全量重刷 → 每日对账兜底。
*
* 变更同步说明(IDEA 灰色=正常):本类的 {@code onCacheChanged(...)} 由 Spring 容器在运行时通过 * 反射注册为事务监听器({@code @TransactionalEventListener}),不会在 IDE 的静态调用链中直接显示 @@ -58,9 +63,9 @@ public class SignKeyCacheService implements ApplicationRunner, ApplicationContex private StringRedisTemplate stringRedisTemplate; /** - * 密钥链专用模板(yh.app-auth.sign-redis.database 配置时由 yh-app-auth starter 注册)。 - * 存在则密钥写入走专用库——sign:app:* 是全平台共享数据,读取方(各提供方 SignKeyResolver) - * 配置同一库号才能看到;未配置(bean 不存在)= 现状,跟应用主库。 + * 密钥链专用模板(yh-app-auth starter 缺省即注册,落 db0)。密钥写入走专用库—— + * {@code sign:app:*} 是全平台共享数据,读取方(各提供方 SignKeyResolver)同库才能看到; + * 若某消费方显式配了其它库号,此处须配同一值(默认无需任何配置)。 */ @Autowired(required = false) @org.springframework.beans.factory.annotation.Qualifier("signKeyStringRedisTemplate") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f540524..7efc29c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -71,12 +71,6 @@ yh: identity: # 允许调用 oapi/identity/switch 的应用 appKey 白名单(逗号分隔;100001=UserHome,后期会新增) switch-allowed-app-keys: 100001,100002 - sign-key-internal: - enabled: true - ips: - - 10.0.0.0/8 - - 172.16.0.0/12 - - 192.168.10.0/200 # 接口文档(springdoc):分组见 SwaggerConfig(oapi=开放接口契约权威源 / api=业务接口) # 契约快照导出:GET /v3/api-docs/oapi