spec.md 23 KB

支付宝沙箱密钥外提与仓库泄密处置

status: closed(用户已决策接受,变更关闭) created: 2026-09-07 closed: 2026-09-07(用户决策:内网私仓 + 沙箱密钥可接受,后期生产密钥不提交) complexity: 🟡中等 规则依据:AGENTS.md 5.9 安全红线「禁止硬编码密钥、AK/SK」、5.12 数据库脚本维护规范(无关)、2.4 环境变量 关联变更:code-copilot/changes/hotel-pay-mock-hardening/(资金逻辑收口,性质不同,不可合并


⚠️ 0. 状态声明(必读)

本变更已关闭。

状态
方案编写 ✅ 本文件
代码/配置改动 一行未动
密钥轮换 ❌ 未执行
用户决策 已决策接受:git 是内网私有仓库(192.168.8.4),不在外网;密钥是沙箱密钥;后期如有必要,会直接不提交生产的密钥

关闭原因:用户评估风险可接受,本变更不再推进。后续生产密钥不入库由用户把控。

更正记录

  • 原 2.3 节「P0 启动隐患」判断错误,已撤销。真实情况:application.yml L42-43 spring.profiles.active: dev,当前启动会加载 application-dev.ymlhotel.alipay.* 全部有值,不会崩

1. 背景与目标

1.1 问题

application-dev.yml 中的支付宝沙箱应用私钥(RSA2,PKCS#8 全文约 1600 字符)与支付宝公钥以明文形式写死,且该文件被 git 跟踪并已推送到远端。命中 AGENTS.md 5.9「禁止硬编码密钥、AK/SK、数据库密码」。

1.2 风险定级(如实评估,不夸大)

维度 结论
直接资金损失 🟢 —— 这是沙箱密钥(gateway-urlopenapi-sandbox.dl.alipaydev.comsandbox: true),沙箱不涉及真实资金
合规红线 🔴 硬违规 —— AGENTS.md 5.9 不区分沙箱与生产,「禁止硬编码密钥」是无条件红线
实际危害 🟡 —— ① 他人可用你的沙箱 appid + 私钥以你的身份发起交易、污染测试数据;② 沙箱密钥对存在被复用到生产的现实可能(同一开发者图省事),一旦复用即升级为高危;③ 私钥泄露后无法「改密码」,只能重新生成密钥对并更换应用公钥
扩散面 🔴 已进远端历史 —— commit 65508d9「支付宝沙箱支付功能提交」(2026-09-01)已存在于 origin/mastergit log -S 可检索到私钥全文

1.3 目标

  1. 止血:轮换密钥,让已泄露的私钥失效(这是唯一真正有效的措施)
  2. 断源:仓库中不再出现任何真实密钥,改为环境变量占位
  3. 生效忽略规则:让 .gitignore已存在但失效**/application-dev.yml 规则真正起作用
  4. 顺带修复一个 P0 启动隐患(见 2.3)—— 生产 profile 当前启动即崩
  5. 不做:不改 AGENTS.md 的意图表述(它是对的,见 2.4)

2. 现状核实(全部经 git / 文件直读验证,非推测)

2.1 泄露范围

文件 是否被 git 跟踪 是否含明文密钥
forge-server/forge-admin-server/src/main/resources/application-dev.yml 🔴 git ls-files 命中) 🔴 ,L106-118 hotel.alipay 块:app-id / private-key(L110)/ alipay-public-key(L112)/ gateway-url / notify-url / sandbox
forge-server/forge-app-server/src/main/resources/application-dev.yml 🔴 🔴 ,L85-99 同一份密钥(两份文件密钥完全相同),另多一个 return-url(L97)
forge-server/*/src/main/resources/application-dev.example.yml ✅ 是(应当提交) 🟢 —— Select-String -Pattern "alipay" 零命中,模板文件里根本没有 hotel.alipay
forge-server/*/src/main/resources/application.yml(生产) ✅ 是 🟢 否 —— 零命中,生产 yml 的 hotel: 块只有 qrpay(admin-server L222-239)

其它顺带发现:application-dev.yml L19-20 的数据库口令为 root / root(本地开发默认值,敏感度低,但同属「明文口令入库」)。

2.2 🔴 .gitignore 规则已存在但失效 —— 修正我此前的建议

我在提出本变更时的原话是「把 application-dev.yml 加入 .gitignore」。这个说法是错的,核实结果:

$ git check-ignore -v --no-index forge-server/forge-admin-server/src/main/resources/application-dev.yml
.gitignore:83:**/application-dev.yml    forge-server/.../application-dev.yml

$ git check-ignore -v forge-server/forge-admin-server/src/main/resources/application-dev.yml
(无输出,退出码 1)
  • .gitignore 第 83 行早就有 **/application-dev.yml,规则本身完全正确、能匹配
  • 不带 --no-index 时返回空 —— 因为 .gitignore 只对未跟踪文件生效,该文件已在索引中,规则被完全绕过
  • 结论:不需要追加任何 .gitignore 规则,需要的是 git rm --cached 把它从索引中移除,让既有规则接管

同时修正另一处说法:我此前说「AGENTS.md 2.4 把它归为『本地配置(不可提交)』,与实际不符,也该修」。AGENTS.md 的表述是对的(2.4 明确把 application-dev.yml 列为「后端本地配置」、把 application-dev.example.yml 列为「配置模板(可提交)」),不符的是仓库实际状态。要修的是状态,不是文案。

⚠️ AGENTS.md 2.4 唯一确实该修的是路径前缀:写的是 forge/forge-admin-server/...,真实路径是 forge-server/forge-admin-server/...。这与 2.2 的 cd forge && mvn clean install(仓库根 forge/空目录,真实 Maven 根为 forge-server/)同源,属同一处文档偏差,建议一并修。

2.3 ✅ 【已撤销】原 P0 启动隐患 — 判断错误,当前不会崩

原判断AlipayConfig 5 个 @Value 无默认值 + 生产 application.ymlhotel.alipay 块 → 生产启动崩溃(P0)。

更正application.yml L42-43 spring.profiles.active: dev,且 docker-compose.yml / .env.example 无任何 profile 覆盖,项目也没有 application-prod.yml。因此当前启动(包括 docker 部署)都会加载 application-dev.ymlhotel.alipay.* 全部有值,不会崩

我错在哪:把 application.yml 当成「生产专用配置」,忽略了它 L43 默认激活 dev profile,application-dev.yml 会被合并加载。

2.4 环境事实

  • 远端:originhttp://192.168.8.4:3000/user5/forge_admin.git内网 Gitea,非公网仓库)
  • 泄露 commit 65508d9 已在 origin/mastergit branch -r --contains 命中)
  • ⚠️ 内网仓库降低了外部攻击面,但不消除风险:任何有仓库读权限的人(含离职人员的历史克隆、CI 缓存、备份)都持有该私钥

3. 处置方案(4 步,顺序不可调换)

顺序原则:先止血(轮换),再断源(占位化),最后清历史。 只要旧私钥仍然有效,任何仓库层面的清理都是装饰性的 —— 攻击者手里的密钥照样能用。


Step 1 🔴 轮换密钥(最先做,只能由你操作)

这一步做完,泄露的私钥立即作废,后续步骤的风险等级随之下降。

在支付宝开放平台操作:

  1. 登录 open.alipay.com → 开发者中心 → 沙箱应用(appid 9021000166699753
  2. 「开发信息」→「接口加签方式」→ 选择公钥模式
  3. 支付宝官方密钥生成工具重新生成一对 RSA2(2048 位)密钥
  4. 上传新的应用公钥,平台会返回新的支付宝公钥
  5. 记录三样东西:新应用私钥、新支付宝公钥、(appid 不变)
  6. ⚠️ 不要把新密钥粘贴到任何 git 跟踪的文件、聊天工具、issue、文档中 —— 包括本文件

验证轮换生效:用旧私钥调一次沙箱接口,应返回验签失败(isv.invalid-signature)。

📌 待你确认(第 6 节 Q1):是否有其它环境/同事正在使用这对沙箱密钥?轮换会立即打断他们的调试。


Step 2 yml 占位化 + 补齐生产块(修复 2.3 的 P0)

改动面:6 份 yml(与 hotel-pay-mock-hardening 共享同一批文件,见第 5 节风险 ③)

2-A 生产 application.yml(admin-server L239 之后 / app-server L121 之后,新增整个 alipay

hotel:
  qr:
    base-url: ${FORGE_HOTEL_QR_BASE_URL:}
    path: ${FORGE_HOTEL_QR_PATH:/r/}
  pay:
    mock-enabled: ${FORGE_HOTEL_PAY_MOCK_ENABLED:false}
  # 支付宝支付配置。全部通过环境变量注入,禁止在仓库中写明文密钥(AGENTS.md 5.9)。
  # 五个必填项无代码级默认值:未注入时 AlipayConfig 占位符解析失败、服务启动即崩,
  # 这是刻意的 fail-fast —— 生产缺支付配置就不该起来,避免起来后静默走到 Mock 分支。
  alipay:
    app-id: ${FORGE_HOTEL_ALIPAY_APP_ID:}
    private-key: ${FORGE_HOTEL_ALIPAY_PRIVATE_KEY:}
    alipay-public-key: ${FORGE_HOTEL_ALIPAY_PUBLIC_KEY:}
    gateway-url: ${FORGE_HOTEL_ALIPAY_GATEWAY_URL:https://openapi.alipay.com/gateway.do}
    notify-url: ${FORGE_HOTEL_ALIPAY_NOTIFY_URL:}
    return-url: ${FORGE_HOTEL_ALIPAY_RETURN_URL:}
    sandbox: ${FORGE_HOTEL_ALIPAY_SANDBOX:false}

⚠️ 这里有个设计选择需你定(第 6 节 Q2)

  • 方案 A(上面写的):给空串默认值 :} → 生产不配也能启动,但支付功能不可用(调支付宝时报错)
  • 方案 B不给默认值 → 生产不配就启动即崩(fail-fast)

我推荐 方案 A + 空串,理由:AlipayConfig@Configuration,方案 B 会让根本不用酒店支付功能的部署也起不来(forge-hotelforge-business 的一部分,被两个服务无条件扫描)。空串默认值 + 调用时报错,故障面更可控。

但方案 A 需要配套改动AlipayConfig.alipayClient()(L50-54)应在 appId / privateKey 为空时不创建 Bean 或创建后在调用处显式报「支付未配置」,否则 DefaultAlipayClient 会用空密钥初始化,错误信息晦涩。这一改动超出纯配置范围,需你同意才做(第 6 节 Q2)。

2-B application-dev.yml(两份,把明文密钥换成占位 + 本地兜底

hotel:
  qr:
    base-url: http://192.168.10.4:3001
    path: "/#/pages/hotel/scan-bind"
  pay:
    mock-enabled: true
  # 支付宝沙箱配置。密钥不写死在文件里(本文件虽已 ignore,但历史上曾被提交,见变更方案 Step 1)。
  # 三种注入方式任选其一:
  #   ① IDEA Run Configuration → Environment variables(推荐,重启即生效)
  #   ② 系统环境变量
  #   ③ 本机 ~/.forge/secrets/alipay.properties(若采用 Step 2-D 的 spring.config.import 方案)
  alipay:
    app-id: ${FORGE_HOTEL_ALIPAY_APP_ID:9021000166699753}
    private-key: ${FORGE_HOTEL_ALIPAY_PRIVATE_KEY:}
    alipay-public-key: ${FORGE_HOTEL_ALIPAY_PUBLIC_KEY:}
    gateway-url: ${FORGE_HOTEL_ALIPAY_GATEWAY_URL:https://openapi-sandbox.dl.alipaydev.com/gateway.do}
    notify-url: ${FORGE_HOTEL_ALIPAY_NOTIFY_URL:http://pc948923.natappfree.cc/hotel/open/pay/notify}
    return-url: ${FORGE_HOTEL_ALIPAY_RETURN_URL:http://192.168.10.4:3001/#/pages/hotel/customer/pay}   # 仅 app-server
    sandbox: true
  • app-id / gateway-url / notify-url / return-url / sandbox 保留明文默认值 —— 它们不是密钥,写死可让你少配 5 个环境变量
  • 只有 private-keyalipay-public-key 默认值为空 —— 这两个是必须外提的
  • ⚠️ notify-url 的 natapp 免费隧道域名(pc948923.natappfree.cc与你账号绑定,敏感度中等。是否也外提见第 6 节 Q3

2-C application-dev.example.yml(两份,新增带说明的空模板

当前模板文件完全没有 hotel.alipaySelect-String 零命中),新人 cp 之后会撞上 2.3 的启动崩溃。需补齐:

hotel:
  # ... qr / pay 块保持与 dev 一致 ...
  # 支付宝沙箱配置。以下两项必须自行填入,仓库中不提供任何真实值。
  # 获取方式:open.alipay.com → 开发者中心 → 沙箱应用 → 接口加签方式 → 用官方密钥工具生成 RSA2 密钥对
  alipay:
    app-id: 你的沙箱APPID
    private-key: 你的应用私钥(RSA2,PKCS#8 单行,不要带 -----BEGIN----- 头尾)
    alipay-public-key: 上传应用公钥后平台返回的支付宝公钥
    gateway-url: https://openapi-sandbox.dl.alipaydev.com/gateway.do
    notify-url: 你的内网穿透地址/hotel/open/pay/notify
    return-url: 你的H5地址/#/pages/hotel/customer/pay
    sandbox: true

2-D(可选)本机密钥文件方案

若不想每次配环境变量,可用 Spring Boot 的 spring.config.import 引入本机文件:

# application-dev.yml 顶部
spring:
  config:
    import: "optional:file:${user.home}/.forge/secrets/alipay.properties"
# ~/.forge/secrets/alipay.properties(在仓库外,天然不会被提交)
FORGE_HOTEL_ALIPAY_PRIVATE_KEY=xxx
FORGE_HOTEL_ALIPAY_PUBLIC_KEY=xxx
  • optional: 前缀保证文件不存在时不报错
  • 与仓库已有的 ~/.forge/secrets/crypto.properties 约定(见 application-dev.yml L67 注释)同源,风格一致
  • ⚠️ 需实测 spring.config.import 引入的 properties 能否被 ${} 占位符解析(Spring Boot 3.2 支持,但需验证加载顺序)—— 见第 6 节 Q4

Step 3 让 .gitignore 规则生效

# 从索引移除,但保留本地文件(--cached 是关键,不加会删掉你的本地配置)
git rm --cached forge-server/forge-admin-server/src/main/resources/application-dev.yml
git rm --cached forge-server/forge-app-server/src/main/resources/application-dev.yml

# 验证:应输出 .gitignore:83:**/application-dev.yml
git check-ignore -v forge-server/forge-admin-server/src/main/resources/application-dev.yml

🔴 这一步对协作者有破坏性,必须先通知

git rm --cached 提交后,该 commit 记录的是文件删除。其他开发者 git pull 时,git 会删掉他们本地的 application-dev.yml(因为在他们工作区里该文件是跟踪状态)。

必须先做的事

  1. 通知所有协作者备份自己的 application-dev.yml
  2. 他们 pull 之后把备份放回来(此时文件已 untracked + ignored,不会再被提交)
  3. 提交信息中写明这一点

📌 待你确认(第 6 节 Q5):当前仓库有几个活跃协作者?是否可以协调一个统一的时间窗口?

⚠️ 顺序要求

Step 3 必须在 Step 2 之后:先占位化再脱钩。若先 git rm --cached,那么在你换密钥的这段时间里,明文密钥文件仍在你本地且已脱离版本控制 —— 看似安全,实则远端历史里的旧密钥仍然有效,止血效果为零。


Step 4(可选,🔴 破坏性)清理 git 历史

只有在 Step 1 轮换完成后,这一步才是"锦上添花";如果没轮换,这一步是自欺欺人。

方案 命令 破坏性
git filter-repo(官方推荐) git filter-repo --replace-text expressions.txt 🔴 重写全部 commit hash
BFG Repo-Cleaner bfg --replace-text passwords.txt 🔴 同上

后果(必须全部接受才能做)

  • 所有 commit hash 变化 → 现存分支、PR、issue 引用、CI 缓存全部失效
  • 所有协作者必须重新克隆(不能 pull,会产生重复历史)
  • 远端需 git push --forceAGENTS.md 与我的操作准则都要求:force push 到 master 必须你明确授权,我不会自行执行
  • Gitea 服务端可能仍保留旧对象(需管理员执行 GC)

我的建议

  • 若仓库仅内网 + 少数可信协作者不做 Step 4。轮换后旧密钥已失效,历史里的字符串只是无意义数据,重写历史的协作成本远高于收益
  • 若仓库曾对公网开放 / 有不可信读取者 / 沙箱密钥曾复用到生产必须做 Step 4

4. 对你本地开发的影响与规避

影响 说明 规避
🔴 改完 application-dev.yml 后启动失败 private-key 占位符无值 → AlipayConfig 拿到空串 → DefaultAlipayClient 初始化不报错,但调支付宝接口时验签失败 执行 Step 2 前,先在 IDEA Run Configuration 里配好 FORGE_HOTEL_ALIPAY_PRIVATE_KEY / FORGE_HOTEL_ALIPAY_PUBLIC_KEY
🟡 支付宝沙箱支付调试中断 轮换密钥后,新密钥未配好之前无法调通 轮换与配置在同一时间窗口内完成;期间可用 hotel.pay.mock-enabled=true 走 Mock 分支调试业务流转(dev 已开启)
🟢 H5 前端 零影响 —— 本变更不动任何前端文件
🟢 二维码功能 零影响 —— hotel.qr.* 块不动
🟢 Mock 支付开关 零影响 —— hotel.pay.mock-enabled 不动

⚠️ 执行时机建议:选一个你不需要立即调试支付宝真实支付的时间点。若近期要演示/验收支付流程,先跳过 Step 1-2,只做记录。


5. 风险与关注点

# 风险 缓解
🔴 轮换打断协作者:沙箱 appid 是共享的,换密钥后其他人本地立刻失效 Step 1 前先确认使用范围(Q1);轮换后同步新密钥给协作者(通过安全渠道,不走 git
🔴 Step 3 删除协作者本地配置git rm --cached 提交后,别人 pull 会丢文件 提前通知备份,提交信息写明
🟡 6 份 yml 与 hotel-pay-mock-hardening 共享同一 hotel: 两变更不可并行改同一文件;本变更应在 hotel-pay-mock-hardening 的 HARD-GATE 签署之后再动 yml,避免回滚互相污染
🟡 Step 2-A 的方案 B(无默认值)会让不用酒店支付的部署起不来 采方案 A(空串默认值)+ 配套改 alipayClient() 显式报错(需你同意,Q2)
🟡 新密钥二次泄露:把新密钥粘到 issue / 聊天 / 文档 Step 1 第 6 条已明令禁止;本文件全程不含任何真实密钥字符
🟢 Step 4 重写历史 默认不做;若做需你明确授权 force push
🟡 application-dev.example.yml 补块后新人仍可能填错格式 私钥必须是 PKCS#8 单行、去掉 -----BEGIN----- 头尾,在模板注释中写明

6. 待决策(需你逐条给结论,全部解决后才进入 /apply

  • Q1:这对沙箱密钥(appid 9021000166699753是否有其他环境/同事正在使用?轮换会立即打断他们。是否有统一时间窗口?
  • Q2:生产 application.ymlhotel.alipay.* 采用方案 A(空串默认值,推荐)还是方案 B(无默认值 fail-fast)?若选 A,是否同意配套修改 AlipayConfig.alipayClient(),在密钥为空时显式抛「支付宝支付未配置」而非用空密钥初始化?
  • Q3notify-url 的 natapp 隧道域名(pc948923.natappfree.cc)是否也外提?它与你账号绑定、敏感度中等,但外提后你每次换隧道都要改环境变量。
  • Q4:是否采用 Step 2-Dspring.config.import + ~/.forge/secrets/alipay.properties)?好处是不用配环境变量、与仓库既有 crypto.properties 约定同源;代价是需先实测加载顺序。
  • Q5:是否执行 Step 4(重写 git 历史)?需你明确授权 git push --forcemaster,我不会自行执行。我的默认建议是不做(内网仓库 + 已轮换 = 收益低于协作成本)。

6.1 附带待决策:AGENTS.md 文档修正

  • Q6:是否顺带修 AGENTS.md路径偏差
    • 2.2 节 cd forge && mvn clean install → 真实为 cd forge-server && mvn clean install(仓库根 forge/空目录
    • 2.4 节表格路径 forge/forge-admin-server/... → 真实为 forge-server/forge-admin-server/...
    • ⚠️ 2.4 节的意图表述是对的,不要改application-dev.yml = 本地配置、.example.yml = 可提交模板),错的只是路径前缀

7. 验收清单(执行后逐条勾)

  • 支付宝开放平台已生成新 RSA2 密钥对,应用公钥已更换
  • 用旧私钥调沙箱接口返回验签失败(证明轮换生效)
  • Select-String -Pattern "MIIEvg" -Path forge-server\*\src\main\resources\*.yml 零命中(旧私钥前缀已清除)
  • 6 份 yml 的 hotel.alipay 块均已占位化,生产块已补齐(修复 2.3 的 P0)
  • application-dev.example.yml 已补 hotel.alipay 空模板 + 格式说明
  • git ls-files | Select-String "application-dev.yml" 零命中(已脱离跟踪)
  • git check-ignore -v forge-server/forge-admin-server/src/main/resources/application-dev.yml 输出 .gitignore:83(规则已生效)
  • 本地 application-dev.yml 文件仍存在且内容正确(--cached 未误删)
  • 配好环境变量后,admin-server 与 app-server 均能正常启动
  • 支付宝沙箱支付调通一次createPay → 收银台 → 回调 → pay_status=1
  • H5 dev 环境二维码扫码、Mock 支付调试行为零变化
  • 协作者已收到通知并备份了自己的 application-dev.yml
  • (若做 Step 4)所有协作者已重新克隆,CI 缓存已清理

8. 确认记录(HARD-GATE)

本变更不直接改动资金流转逻辑,但涉及密钥安全破坏性 git 操作git rm --cached / 可能的 push --force),按 AGENTS.md 5.9 与操作准则,须你确认后方可执行。 AI 助手不得代签、不得自行执行 Step 3 之后的任何 git 写操作。

  • 确认时间
  • 确认人

确认范围声明(签署即表示已阅读并认可):

  1. 第 6 节 Q1~Q6 六项待决策已有明确结论
  2. 知悉 Step 1 轮换密钥只能由本人在支付宝开放平台操作,AI 无法代办
  3. 知悉 Step 3 会导致协作者 pull 时丢失本地 application-dev.yml,已安排通知与备份
  4. 知悉 Step 4 需重写全部 commit hash 并 force push 到 master,默认不做;若做已单独明确授权
  5. 知悉本变更应在 hotel-pay-mock-hardening 的 HARD-GATE 签署之后再动 6 份共享 yml(风险 ③)