Wujies 无界平台
运维

备份与安全

本文是 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 远程读)
app_schema_history 为什么必须备

应用包的迁移脚本是按目录驱动执行的:扫到脚本 → 对比账本里的摘要 → 没执行过就执行。账本丢了会同时发生两件坏事:

  1. 已经跑过的 DDL 会被再跑一遍。幂等脚本(CREATE TABLE IF NOT EXISTS)能扛住,但不幂等的脚本会直接失败,更糟的是「加列」这类脚本如果没做判断,第二次执行就会报错并让升级卡死。
  2. 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 里没有安全问题 —— 它只回答「这个包是不是这把私钥签的」
正式发布前就备份好换钥的代价取决于已经签过多少包:旧钥签过的全部作废

换钥要同时做三件事

  1. 重新生成密钥对(旧私钥丢了就只能重来)。

    powershell -NoProfile -ExecutionPolicy Bypass -File plugins\package.ps1 -Keygen
  2. 把新的 public.pem 整段贴进 app-market.security.public-key(生成命令会把可粘贴的 YAML 片段打印出来;也可以加 -SyncPublicKey 让脚本直接改 yml)。

  3. 重建宿主并重启。

第 3 步最常被漏

公钥是启动期读进内存的。只改 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),本身就不是弱口令;外部库要自己保证
2Redis(spring.data.redis)密码 / 主机面板里那个 Redis 没设密码时安装表单的密码栏是空的,而它是必填项 —— 先去面板设个密码
3Sa-Token JWT 密钥sa-token.jwt-secret-key换掉之后所有已签发的 token 失效,所有人都要重新登录
4api-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/),尤其是「别人还在用」这道闸不会保护你 —— 它保护的是别人。


相关阅读

目录