包安全与签名
为什么必须有签名
应用包的两部分都是完全可信代码:
- 后端 jar 是任意 Java 代码 —— 它跑在宿主 JVM 里,能读数据库、发网络请求;
- 前端 JS 跑在宿主同源上下文 —— 它能拿用户 token 调任何接口。
静态扫描挡不住有意作恶的插件。签名是唯一真防线:它不判断代码好坏, 只回答"这个包是不是我签发的、有没有被改过"。
产物白名单管的是另一件事
白名单挡不住恶意代码,但能挡掉「往前端目录塞个 .jsp 让 Web 容器执行」 这类结构性问题 —— 因为前端目录就是宿主的静态资源目录。
| 位置 | 规则 |
|---|---|
| 包根 | 只允许 app.json 与 signature.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.json 与 migrations/*.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,超出会明确报截断)。 不要做成定时任务 —— 只会每天白拉一遍对象存储。
unverifiable 与 invalid 的处置完全不同
前者去配公钥,后者说明包被改过或不是同一私钥。 所以它们被刻意分成两个分类,而不是混在一起。
记得签名这件事,交给工具提醒
未传私钥时打包脚本会 Write-Warning 明确说出「宿主是 enforce, 这个包上传/部署会被拒绝」,并在本机存在私钥时把该敲的命令原样打出来。
见 密钥与签名。