Skip to content

包安全与签名

为什么必须有签名

应用包的两部分都是完全可信代码

  • 后端 jar 是任意 Java 代码 —— 它跑在宿主 JVM 里,能读数据库、发网络请求;
  • 前端 JS 跑在宿主同源上下文 —— 它能拿用户 token 调任何接口。

静态扫描挡不住有意作恶的插件。签名是唯一真防线:它不判断代码好坏, 只回答"这个包是不是我签发的、有没有被改过"。

产物白名单管的是另一件事

白名单挡不住恶意代码,但能挡掉「往前端目录塞个 .jsp 让 Web 容器执行」 这类结构性问题 —— 因为前端目录就是宿主的静态资源目录。

位置规则
包根只允许 app.jsonsignature.txt
顶层目录只允许 backend/frontend/migrations/
backend/只允许 .jar
migrations/只允许 .sql
frontend/按白名单扩展名,且硬禁止一批危险类型

前端硬禁止的类型(在配置的白名单之上再减一层,写进配置也改不动):

.jsp .jspx .jspf .php .phtml .war .jar .class
.exe .dll .so .dylib .sh .bat .cmd .ps1 .vbs
.properties .xml .yml .yaml .sql

签名机制

签名是分离式的:包根一个 signature.txt,覆盖除它自己以外的全部文件 (含 app.jsonmigrations/*.sql)。

为什么不把签名塞进 app.json

为了避开「JSON 键序跨语言不一致」这个经典坑 —— 那需要先做规范化 JSON,极易出错。

摘要算法

对包内每个文件(排除 signature.txt 自己),按路径字符串升序依次拼:

<相对路径>\n<内容 SHA-256 十六进制小写>\n

拼出来的整段 UTF-8 字节就是要签名 / 校验的数据,算法是 SHA256withRSA

两条要点:

  • 路径参与签名 —— 否则把 A 文件改名成 B 可以逃过校验;
  • 只签名内容、不签名 zip 元数据(时间戳、压缩级别、外部属性)。

signature.txt 的格式

app-market-signature/1
SHA256withRSA
<base64 签名>
# 覆盖文件数: N
# 内容摘要: <canonical UTF-8 的 SHA-256 十六进制小写>

解析时注释行一律忽略;格式版本只接受 1;算法必须恰好SHA256withRSA

签名工具是纯 JDK 的

它只依赖 PackageDigest / PackageKeys 这两个纯 JDK 类, 用 -cp <classes> 直接启动,classpath 上没有 Spring、没有 ruoyi-common

别在签名相关类里引入平台依赖

一旦引用了带平台依赖的类,签名工具会在自查阶段直接 NoClassDefFoundError: org/dromara/common/core/exception/ServiceException

这个坑真实发生过:签名本身成功了,卡在 afterwards 的自查上。

两个校验点,缺一不可

校验点作用
上传时尽早告诉发布方,别等到安装才发现
下载后(真正的防线)校验的是即将落位的那份字节,能挡住"上传之后对象存储里的包被替换"

安装/升级复用的就是下载后那段代码。

三种模式

模式行为
off不检查,等价于改造前
warn检查并记录问题(部署报告的 security 字段 + 日志),照常安装
enforce有问题就拒绝

注解兜底值 ≠ yml 实际值

security.mode 的代码注解兜底是 warn(为存量未签名包的过渡期准备的), 但本仓库的 application.yml实际配的是 enforce

切到 enforce 有一个前提:public-key 已配置。 没配公钥时 enforce 会拒绝所有包(包括你自己签的),报错里会明说原因。

签名就绪度自检

GET /app/console/signature-readiness(权限 app:console:list) 会把已发布版本逐个下载验签,直接回答"现在能不能切"。

分类含义怎么办
signed签名有效
unsigned包里没有 signature.txt就地重签,或重新打包上传成新版本
invalid有签名但校验不过包被改过,或不是用同一私钥签的
unverifiable有签名,但服务端没配公钥去配 public-key不是包的问题)
failed下载失败 / 没有包地址不影响切 enforce,只表示这个版本没被检查到

它是按需调用的诊断接口

它会真实下载所有包(上限 100,超出会明确报截断)。 不要做成定时任务 —— 只会每天白拉一遍对象存储。

unverifiableinvalid 的处置完全不同

前者去配公钥,后者说明包被改过或不是同一私钥。 所以它们被刻意分成两个分类,而不是混在一起。

记得签名这件事,交给工具提醒

未传私钥时打包脚本会 Write-Warning 明确说出「宿主是 enforce, 这个包上传/部署会被拒绝」,并在本机存在私钥时把该敲的命令原样打出来

密钥与签名

相关阅读

基于 MIT 协议开源 · 文档与官网由源码生成