安全校验模式
这一页只回答三个问题:app-market.security.mode 三档各自拦什么、某个部署实际生效的是哪一档、切到 enforce 之前必须做完什么。
签名怎么算、密钥怎么管见 密钥与签名;产物白名单允许哪些文件见 包安全与签名。本页不重复那些内容。
这个开关拦什么
检查只有一处实现:wujies-platform/wujies-modules/wujies-app-market/src/main/java/cn/wujies/appmarket/security/PackageSecurityInspector.java。它判两件事:
| 判什么 | 判据 | 不通过时的原话(Report.problems) |
|---|---|---|
| 产物白名单 | 包根只允许 app.json 与 signature.txt;顶层只允许 backend/、frontend/、migrations/;backend/ 只允许清单声明的那一个 .jar;migrations/ 只允许 .sql;frontend/ 只允许白名单扩展名 | 包根出现不允许的文件: …、不允许的顶层目录: …、backend/ 下出现清单未声明的产物: … |
| 签名 | 包根有 signature.txt,且能用服务端的 app-market.security.public-key 验过 | 应用包未签名(缺少 signature.txt)。…、应用包带签名,但服务端未配置 app-market.security.public-key,无法校验。、应用包签名校验失败: … |
它不判断代码好坏,也不区分发布方——只回答"这个包是不是我签发的、有没有被改过"。所以"还没开放第三方上架"不是把模式调松的理由。
两个校验点都调 inspectOrThrow,缺一不可:
| 校验点 | 谁调 | 校验的是哪一份字节 | 结论落在哪 |
|---|---|---|---|
| 上传时 | AppPackageController.parse(POST /app/package/parse) | 刚上传的那份 | 响应字段 securityNote |
| 下载后、解包前 | AppPackageDeployService.deploy(该类第 188 行,排在 verifyChecksum 之后) | 即将落位的那份 | 部署报告的 security 字段 |
被 enforce 拒掉时抛 ServiceException,文案固定以这句开头,可直接拿它 grep 日志:
应用包安全检查未通过(app-market.security.mode=enforce):…因为校验在解包之前,被拒时磁盘上不会落位任何文件(这一点实测过)。另外,问题条目最多只报前 6 条(MAX_REPORTED),汇报里的「问题 N 项」是截断后的数量。
三档模式的实际差别
| 模式 | inspect() 给出的结论 | 会不会拦 | 服务端日志 | 接口响应 / 部署报告 |
|---|---|---|---|---|
off | accepted 恒为 true(问题照样被算出来) | 不拦 | 不写(日志分支显式排除了 off) | 仍带出问题清单 |
warn | accepted 恒为 true | 不拦 | 应用包安全检查发现问题(warn,仍放行): 模式=warn, 签名=…, 问题 N 项: … | 同上 |
enforce | 有问题则 accepted=false | 拦(上传与部署两处都拦) | 应用包安全检查未通过(enforce,将拒绝): … | 无问题时才写进报告:模式=enforce, 签名=有效, 无问题 |
Report.summary() 不看模式,所以 off 下上传与部署的响应里照样能看到 模式=off, 签名=无签名文件, 问题 1 项: …。off 省掉的只是拦截与日志,检查本身仍然跑。类注释里那句"完全不检查"只在行为意义上成立。
normalizeMode(PackageSecurityInspector 末尾)会 trim + 转小写,只认 off / warn / enforce,其余一律当 warn,不报错。1Panel 表单那一栏是 type: text + rule: paramCommon,没有枚举校验,所以 enforce 这类写法多半会被 trim 救回来,而 strict、on、true 会悄悄退化成 warn。
另外,自检接口返回的 mode 字段是原样字符串(SignatureReadinessService 直接把 @Value 注入的值放进报告),不做归一化——它显示 strict 时实际按 warn 跑。判断当前行为看日志/部署报告里的 模式=,不要只看自检报告那个字段。
生效值来自哪:同一个键有三个来源
| 来源 | 值 | 位置 |
|---|---|---|
| 环境变量(容器化部署) | 1Panel 安装表单里的 SECURITY_MODE,默认 warn、required: true | wujies-platform/deploy/1panel/apps/wujies-platform-server/5.6.3/data.yml 与 ...-client/5.6.3/data.yml;compose 里是 APP_MARKET_SECURITY_MODE: ${SECURITY_MODE} |
| jar 内 yml(本机直跑时生效) | enforce | wujies-platform/wujies-admin/src/main/resources/application.yml 的 app-market.security.mode |
| 代码注解兜底(yml 缺失或键被删时) | warn | PackageSecurityInspector 与 SignatureReadinessService 的 @Value("${app-market.security.mode:warn}") |
覆盖优先级(高 → 低):JVM 系统属性 -Dapp-market.security.mode=… → 环境变量 APP_MARKET_SECURITY_MODE → jar 内 application.yml → 注解兜底。
于是同一份代码在两种部署形态下默认值相反:
| 部署形态 | 默认生效值 | 怎么改 | 要重建什么 |
|---|---|---|---|
本机直跑(wujies-platform/script/prune/start-host.ps1) | enforce(yml 值) | 改 application.yml 后重启;start-host.ps1 -Build 会顺带重新 mvn -pl wujies-admin -am -B package | yml 打在 jar 里,改了就必须重建宿主 |
| 1Panel 应用包 | warn(表单值,盖掉镜像里 yml 的 enforce) | 安装表单里填,或装完到 1Panel「应用 → 参数」改 应用包检查模式 保存 | 不用重建镜像:面板会重建容器;参数细节见 /deploy/configuration |
public-key 没有环境变量:仓库里两个 1Panel 包的 data.yml 与 docker-compose.yml 都没有公钥字段,deploy/docker/backend/entrypoint.sh 也不导出它。所以容器里生效的公钥,就是构建镜像时打进 jar 的那个 application.yml 里的那一把——而按该文件自己的注释,那是"开发验证阶段生成的"密钥。
后果:1Panel 表单里把模式调成 enforce,用你自己 wujies/wujies-plugins/keys/private.pem 签的包会全部落到 invalid,报「签名与包内容不匹配」。这时候看起来像"签名坏了",实际是公钥不匹配。
要换公钥只有两条路:改 application.yml 的 public-key 后重建后端镜像(只重启容器没用),或自己在 compose 里加 APP_MARKET_SECURITY_PUBLIC_KEY(PEM 是多行文本,注意 YAML 块标量写法)。
切到 enforce 之前必须做完两件事
1. 公钥就位,且与发布私钥成对
没配公钥时 enforce 会拒绝所有包(包括你自己签的),报错里会明说「未配置公钥」而不是「签名无效」。打包时顺手就能核对:package.ps1 会拿本次签名用的公钥与宿主 application.yml 的 public-key 比对,一致打 ✓,不一致或为空会 Write-Warning(用的是 PackageKeys 同一套规范化)。
2. 已发布版本都能过:签名就绪度自检
GET /app/console/signature-readiness # 权限 app:console:list它把 app_version 里状态为 1(已发布,AppMarketConstants.VERSION_STATUS_RELEASED)、未删除的版本逐个下载验签,直接回答"现在能不能切":
| 返回字段 | 含义 |
|---|---|
mode | 原样的配置值(见上面的告警,不一定等于实际行为) |
publicKeyConfigured | public-key 是否非空 |
ready | blockers 为空才为 true |
summary | 一句话结论,可直接给人看 |
checked / truncated | 实际检查了几个包、是否因超过上限(100)被截断 |
counts | 五类计数,键恒为 signed / unsigned / invalid / unverifiable / failed |
blockers | 阻止切换的原因(逐条可读) |
items | 逐版本明细:versionId / appCode / version / status / reason |
| 分类 | 判据(SignatureReadinessService.checkOne) | 怎么处置 |
|---|---|---|
signed | 有签名且验过 | 切了会被放行 |
unsigned | 包里没有 signature.txt | 就地重签,或重新打包上传成新版本 |
invalid | 有签名但验不过 | 包被改过,或不是用同一私钥签的 |
unverifiable | 有签名但服务端没配公钥 | 去配 public-key(不是包的问题) |
failed | 包地址为空或下载失败 | 不阻止切换,但表示这个版本没被检查到 |
会真实下载所有包(上限 100,超出明确报 truncated)。不要做成定时任务——只会每天白拉一遍对象存储。就地重签的完整流程见 密钥与签名。
顺带把发布侧的默认行为固定下来
wujies/wujies-plugins/package.ps1 已经替你把"忘了签名"堵住:本机 keys/private.pem 存在时自动使用(跨机器/CI 用 WUJIES_SIGNING_KEY),显式传 -SigningKey 时若文件不存在会自动 keygen 到该目录;确实要打未签名包就显式加 -NoSign(与 -SigningKey 互斥)。参数表见 打包应用。
未传私钥时它打印的是「宿主 app-market.security.mode=enforce 时这个包上传/部署会被拒绝」——它并不读宿主 yml 里的 mode(只读 public-key 做比对)。所以给 warn 部署打未签名包时这句警告会照旧出现,那只是提示,不是失败。
怎么切、怎么验收
本机直跑改的就是这一段(wujies-platform/wujies-admin/src/main/resources/application.yml):
app-market:
security:
mode: enforce # off / warn / enforce
public-key: | # 与发布私钥成对的公钥;留空时 enforce 拒绝所有包
-----BEGIN PUBLIC KEY-----
…
-----END PUBLIC KEY-----按部署形态选一条:
| 形态 | 切的动作 | 生效时机 |
|---|---|---|
| 本机直跑 | 改 application.yml 的 mode: enforce → start-host.ps1 -Build(或手工 mvn -pl wujies-admin -am -B install -DskipTests)→ 重启 | 重启后 |
| 1Panel 包 | 「应用 → 参数」把 应用包检查模式 改成 enforce → 保存 | 面板重建容器后 |
| 临时验证(不改文件) | 设环境变量 APP_MARKET_SECURITY_MODE=enforce 再启动 | 进程启动时 |
切完用这三条自证:
| 用例 | 期望 |
|---|---|
| 再调一次自检 | mode=enforce、ready=true、counts.unsigned=0、counts.invalid=0 |
| 部署一个已签名包(控制台「部署」) | 成功,部署报告 security = 模式=enforce, 签名=有效, 无问题 |
上传一个未签名包(POST /app/package/parse) | 被拒,文案以 应用包安全检查未通过(app-market.security.mode=enforce):应用包未签名(缺少 signature.txt) 开头 |
warn 模式下问题的痕迹落在哪
| 位置 | 有没有 | 说明 |
|---|---|---|
上传响应 securityNote | ✅ | AppPackageController.parse 写入 |
控制台「部署」响应 security | ✅ | AppPackageDeployService.deploy 写入 |
| 后端服务日志 | ✅ | log.warn("应用包安全检查发现问题(warn,仍放行)…"),grep 应用包安全检查 即可 |
| 任务中心的任务日志 | ❌ | 安装/升级路径只把 releasedCount / deferredLoad / migrationCount 写进任务日志(PluginAppInstaller),安全检查结论不进任务中心 |
所以在 warn 下排查"某个包为什么没签名"时:任务中心里看不到,要看后端服务日志,或自己调一次自检接口。任务中心本身的读法见 任务中心。
用户版(client)下自检接口不可用
app-market.edition=client 时,MarketEditionGuardFilter 会按路径前缀直接 403(附带明确文案,不是 404、也不是静默失效)挡掉 /app/console、/app/version、/app/package 以及 /app/category 的写操作。
例外只有 /app/console/owned-menus(菜单管理页要用它打归属标签)。签名就绪度自检不在例外里——它读的是本地目录,用户版下没有意义。所以用户版要确认签名状况,只能在官方版那一侧查,或看服务端日志。形态细节见 目录与版本形态。