备份与安全
本文是 1.x 的归档文档(冻结于 1.x)。该分支只接收勘误修正, 内容基本不再变动;文中的源码路径、配置项与版本号均以 1.x 为准,不建议用于新项目。
新项目请查看 5.x 最新文档。已在 1.x 上运行的项目,本文仍是可靠依据。
这份文档回答两个问题:丢了什么会真的痛,以及公开部署之前必须做哪些脱敏。
要备份什么
分两类:数据库表与文件。每项都标了「丢了会怎样」—— 按这个后果排优先级,不要按体积排。
数据库表
| 对象 | 内容 | 丢了会怎样 |
|---|---|---|
app_install | 这台部署装了哪些应用、装的是哪个版本、状态(enabled / disabled / uninstalled) | 不知道本机该有那些应用。菜单的隐藏逻辑依赖「有没有已启用安装实例」,菜单会整体显示异常;升级/卸载失去入口 |
app_install_task | 安装/升级/卸载的步骤与日志 | 丢掉的是审计线索与排查依据。注意它是刻意保留的,包含「彻底删除」之后的历史记录(应用名显示「(应用已删除)」) |
app_version_menu | 版本 → 菜单的归属映射(version_id / menu_id / menu_key) | 菜单归属信息丢失。菜单同步会失去「复用既有 menu_id」的依据,升级时会建出一套重复菜单;「彻底删除」也找不到该删哪些菜单行 |
app_schema_history | 迁移账本:(app_code, script) → 摘要 / 成功标记 / 耗时 | 最痛的一项,见下一节 |
目录表 app_info / app_version / app_category / app_plan(+ app_dependency) | 应用与版本、分类、包地址与校验和 | 丢的是目录本身。目录集中部署时,要备的是「目录服务」那一份,安装方本地那份是空的(它走 catalog.mode=remote 远程读) |
应用包的迁移脚本是按目录驱动执行的:扫到脚本 → 对比账本里的摘要 → 没执行过就执行。账本丢了会同时发生两件坏事:
- 已经跑过的 DDL 会被再跑一遍。幂等脚本(
CREATE TABLE IF NOT EXISTS)能扛住,但不幂等的脚本会直接失败,更糟的是「加列」这类脚本如果没做判断,第二次执行就会报错并让升级卡死。 checksum校验失去基准。「已执行过的脚本内容不能再改」这条保护完全依赖账本里记的摘要 —— 账本丢了,改过的脚本会被当新脚本执行。
反过来说,「彻底删除」时删掉这个应用的迁移记录是刻意的:不删的话重装时建表脚本会被当成「已执行过」跳过,结果是装成功了但所有页面报表不存在。备份与这里并不矛盾:备份是为了灾难恢复,删除是为了让重装能真正重建。
文件
| 对象 | 内容 | 丢了会怎样 |
|---|---|---|
plugins/keys/(尤其 private.pem) | 包签名私钥 | 丢了没法找回,只能重新生成一对新钥;旧钥签过的包全部作废,必须重签一遍,并且所有部署都要更新公钥并重启。这个目录已 gitignore,所以它只存在于你的机器上 |
plugins-dist/ | 后端插件 jar 落位目录 + active-plugins.txt | 可恢复:重新从市场拉包落位(控制台点「部署」,或重装/升级)。但要注意 active-plugins.txt 记录「当前该用哪个 jar」,丢了之后重启加载会退化成扫描目录 |
app-market-dist/remote-apps/ | 前端远程应用产物(每个 appCode 一个目录) | 可恢复:同上,重新部署即可。丢了的表现是 /remote-apps/<code>/index.js 404、菜单点开是「远程视图不可用」 |
| 对象存储里的安装包 | 各版本的 .zip | 目录表里的 package_url 指向它。丢了就没法安装/部署该版本 —— 所以私钥与已发布包要一起归档(重签需要原始包) |
「就地重签」需要原始的、未被改动的包。只备私钥、不备已发布的包,换钥/补签时就得从对象存储里捞 —— 如果对象存储也一起出事,那些版本只能重新打包上传成新版本。
密钥纪律
| 规则 | 说明 |
|---|---|
| 私钥只留在发布方 | plugins/keys/ 已加进 .gitignore。私钥一旦进仓库,任何人都能签出被后端认可的包,签名就完全白做了 |
| 仓库之外要有备份 | 密码管理器、CI 的 secret、离线的加密归档都可以。私钥丢了没有找回的办法,只能重新生成 |
| 绝不覆盖已有私钥 | keygen 遇到目录里已存在 private.pem(或 public.pem)会直接拒绝。换钥是破坏性操作,必须显式做 |
| 公钥不是秘密 | 它放在 app-market.security.public-key 里没有安全问题 —— 它只回答「这个包是不是这把私钥签的」 |
| 正式发布前就备份好 | 换钥的代价取决于已经签过多少包:旧钥签过的全部作废 |
换钥要同时做三件事
-
重新生成密钥对(旧私钥丢了就只能重来)。
powershell -NoProfile -ExecutionPolicy Bypass -File plugins\package.ps1 -Keygen -
把新的
public.pem整段贴进app-market.security.public-key(生成命令会把可粘贴的 YAML 片段打印出来;也可以加-SyncPublicKey让脚本直接改 yml)。 -
重建宿主并重启。
公钥是启动期读进内存的。只改 yml 不重建、不重启,后端还在用旧公钥 —— enforce 模式下会把你刚签的包全部拒掉,而报错看起来像「签名无效」,极容易往错的方向查。
还有一层:application.yml 是打在宿主 jar 里的,所以必须 mvn -pl wujies-admin -am -B install -DskipTests 重建 jar,再重启。
换钥之后别忘了:所有已签名的包都要重签(就地重签,版本号与状态都不动),并用 GET /app/console/signature-readiness 确认 ready=true。流程见 密钥与签名。
公开部署前的脱敏清单
下面每一项在公开部署前都要换成新生成的值,并且不要出现在任何文档、聊天记录、截图、工单里。本页不贴任何真实值,只写占位符。
| # | 位置 | 要换什么 | 备注 |
|---|---|---|---|
| 1 | 数据库连接(spring.datasource) | 用户名 / 密码 / 主机 / 库名 | 1Panel 安装时库名与账号是 random: true 随机生成的(ry-vue_xxxxxx / ruoyi_xxxxxx),本身就不是弱口令;外部库要自己保证 |
| 2 | Redis(spring.data.redis) | 密码 / 主机 | 面板里那个 Redis 没设密码时安装表单的密码栏是空的,而它是必填项 —— 先去面板设个密码 |
| 3 | Sa-Token JWT 密钥 | sa-token.jwt-secret-key | 换掉之后所有已签发的 token 失效,所有人都要重新登录 |
| 4 | api-decrypt 公私钥 | api-decrypt.publicKey / privateKey | 与前端 VITE_APP_... 那对是配套的,必须成对替换 |
| 5 | 签名公钥 | app-market.security.public-key | 换成你自己的那把(见上一节)。注意它换掉就意味着要重建宿主 + 重启 + 重签旧包 |
| 6 | 目录服务共享令牌 | app-market.catalog.token | 服务端与调用方必须一致;公网必须配(留空 = 不校验调用方) |
| 7 | 前端 .env.* 的接口地址 | VITE_APP_BASE_API 等 | 用示例/占位符替换真实域名。不要保留 .env.production 里的内网地址 |
| 8 | 对象存储凭据 | sys_oss_config 表里的 AK/SK、endpoint、bucket | 它存在数据库里,不在 yml。脱敏时容易漏 |
| 9 | 监控中心 / 调度中心的账号密码 | 安装表单里的值 | 监控中心默认账号 wujies(密码 123456 或安装表单随机生成的值)—— 记下来并改掉默认口令 |
| 10 | 默认后台账号 | admin 的初始密码 | 初始化 SQL 里的默认口令只用于首次登录,上线前必须改掉 |
| 11 | 各应用的站点配置 | sys_config 里 config_category 为应用编码的行 | mail.pass / mail.user 一律清空、mail.enabled=false;*.logo 等指向导出机器 OSS 行 id 的字段要置空(换部署就是死引用) |
仓库里的 application.yml 带着开发验证阶段生成的密钥对(对应 plugins/keys/private.pem,该目录已 gitignore)。正式对外发布前必须重新生成一对并替换 public-key,同时把旧私钥备份到仓库之外。
共享令牌的安全边界
app-market.catalog.token 是官方版(目录服务)与调用方之间的共享令牌,用在 /app/catalog/** 这组服务间接口上。
| 事实 | 含义 |
|---|---|
| 它通过明文请求头传递 | 只防扫描器、防误调用,不防抓包。真正保密要靠 HTTPS |
| 服务端留空 = 不校验调用方 | 内网自用可以;公网必须配,否则谁都能读你的目录 |
/app/catalog/** 已在 security.excludes 里 | 也就是说这条路径不走登录鉴权,令牌是唯一的门槛 |
| 两端配成一致即可 | 没有轮换机制,改一次要两边同时改并重启 |
令牌只是第二道门。可执行的做法:
- 目录服务与安装方走内网 / VPN / 私网互联;
- 必须过公网时,在反向代理上只放行
/app/catalog/**这一组前缀,其余后端路径一律不放行; - 不要把 Actuator 暴露到公网(
include: '*'+show-details: ALWAYS会吐出配置与 bean 拓扑); - 客户安装时是按
packageUrl从对象存储拉包的,所以 OSS 也要对客户可达(不能只对你自己的内网开)。
访问日志与审计
排障与审计要看的日志分布如下(详细位置与保留天数见 运维手册):
| 想查什么 | 去哪 |
|---|---|
| 应用操作审计(谁改了哪个应用/版本) | sys_oper_log 表 —— @Log 注解标注的上架端接口(应用管理、应用版本、应用上下架、部署、安装/升级/卸载/启停、任务删除)都会落一行 |
| 登录记录 | sys_logininfor 表(登录成功/失败与 IP) |
| 应用生命周期过程 | app_install_task(步骤 + 日志 + 失败原因),界面在「我的应用 → 应用任务」 |
| 框架级运行时问题 | logs/sys-console.log(唯一含 WARN 的落盘文件,只留 1 天)、logs/sys-error.log(留 60 天) |
| 目录被谁读过 | 目录服务那一侧的请求日志(找 GET /app/catalog/...)。注意验证时不要自己手工 curl 对端,否则会把自己的请求当成调用方的证据 |
| 容器入口脚本的结论 | docker compose logs backend 里 [entry] 前缀的行(中间件地址解析、初始化是否完整、调度/监控注册) |
gallery_image_access_log 是图床应用自己的业务功能(记录每张图片被谁在什么时间访问),它随图床应用一起安装与卸载,与平台的运维审计无关。要查「谁动了平台」,看 sys_oper_log;要查「谁看了哪张图」,看这张表。
卸载安全的两道闸
「彻底删除」会删菜单、数据表、迁移记录与本机产物。为了不把整个部署带下水,服务端有两道闸 —— 都只跳过、不让整次卸载失败,并把原因写进任务日志。
| 闸 | 触发条件 | 行为 |
|---|---|---|
| 别人还在用 | 同一应用在 app_install 里还有其它租户(状态不是 uninstalled) | 跳过菜单 / 数据表 / 迁移记录 / 产物的删除,只清本租户的安装记录。因为 sys_menu、数据表、本机产物都是一个部署内全局共享的 —— 替别的租户做不可恢复的决定是不可接受的 |
| 平台表前缀保护 | 解析出的表名以 sys_ / app_ / gen_ / flow_ / act_ / qrtz_ / snailjob_ / undo_log 开头 | 跳过该表并告警。防的是一个写错的脚本(例如 CREATE TABLE IF NOT EXISTS app_install)把平台表带下水 |
配合两条规则一起看:
- 删哪些表完全由应用包决定:平台从落位的迁移脚本里解析
CREATE TABLE的表名(先剥注释,只认CREATE TABLE,ALTER/INSERT/DROP都不算「拥有」)。包里没有建表脚本 → 一张表都删不掉。 - 清单里必须写按钮(
type: "F")。归属映射里只有清单声明过的菜单;按钮历史来源是平台初始化 SQL,从来不在映射里 —— 不写进清单就会在彻底删除时留成「父菜单没了的孤儿」。现在的版本会顺着parent_id把后代一起删。
备份视角的结论:卸载的默认那条路(保留数据)是可恢复的,彻底删除不是。 做彻底删除前先确认备份(表 + plugins-dist/ + remote-apps/),尤其是「别人还在用」这道闸不会保护你 —— 它保护的是别人。