Files
senmesh-zc/deploy/offline/部署说明.md
KodouWZF 4eb67e246e feat(交付): 客户侧两台服务器离线部署包
内网机/外网机各起一套栈,两台之间不开任何端口,文件只经光闸在
gate/out 与对端 gate/in 之间单向摆渡。一份 compose + 一份 config +
一份 nginx 模板,全部差异收在 .env 里。

- docker-compose.yml:5 服务只发布前端端口;app 容器 IP 锁死并由
  compose 注入 -Dcode.snowflake.workid.<IP>=<WORK_ID>,绕开兜底 99
  超雪花 5 位上限导致启动即崩的问题
- .env.iw / .env.ow:出厂配置与随机口令已填好,仅 MESHSAFE_SECRET
  故意留占位(以 MeshSafe 侧白名单为准);按 maintainer 决定原样入库
- install.sh iw|ow:按角色建 .env -> 自检 -> sha256 校验 tar -> 架构
  核对 -> docker load 后校验 tag -> 建目录/属主/SELinux 标签 -> 起栈
  并逐个等健康。修掉 pipefail 下 `grep | head -1` 静默中断、架构字段
  误读 manifest.json(实际在 Config blob)、die() 未定义先被调用
- export-images.sh:就地刷新 images/*.tar、sql、.env 的 tag 与
  SHA256SUMS,再打 dist 交付盘;tar 与 SHA256SUMS 由 .gitignore 排除
- sql/01+02:从 backend/db 的 PG 全量与初始数据覆盖拷贝。三个
  upgrade-*.sql 的 PG 段(x_upload_config、x_file_security)已在
  init-table-postgres-full.sql 内,空库跑 01->02 即完整现状
- smoke_test.py:18 条断言的现场验收,凭证全部走环境变量
- 部署说明.md:拓扑、交付清单、出厂口令表、iw/ow 差异、验收清单、
  13 条已知坑

镜像 tar 不进 git,装机包由 export-images.sh 出盘。
2026-11-02 20:40:16 +08:00

220 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# s-file 客户侧离线部署(两台服务器:内网机 + 外网机)
> 本目录是**交付用**部署;本机联调用的两套栈同起在一台机器上,见 [../部署说明.md](../部署说明.md)。
> 客户现场没有网络、没有光闸模拟目录,一切靠 `docker load` 的镜像 tar + 真实光闸摆渡目录。
---
## 1. 拓扑
```
内网(客户办公网) 外网(外部报送网)
┌────────────────────────┐ ┌────────────────────────┐
│ 内网服务器 A │ │ 外网服务器 B │
│ ┌──────────────────┐ │ 光闸/单向 │ ┌──────────────────┐ │
│ │ sfile-iw-web :80 │◄─┼── 摆渡设备 ──►┼─ │ sfile-ow-web :80 │ │
│ │ sfile-iw-app :8080│ │ (不开放网络) │ │ sfile-ow-app │ │
│ │ pg/redis/mq(内部) │ │ │ │ pg/redis/mq(内部)│
│ └──────────────────┘ │ │ └──────────────────┘ │
│ /data/sfile/gate/out │─── 发 ────► │ /data/sfile/gate/in │
│ /data/sfile/gate/in │◄─── 收 ──── │ /data/sfile/gate/out │
└───────────┬────────────┘ └───────────┬────────────┘
│ http://host.docker.internal:<MeshSafe端口>
┌──────┴───────────┐
│ MeshSafe 文件安全检测 │ 同机容器 / 独立服务器 / 集群均可,只要求 内网机能 HTTP 访问到
└──────────────────┘
```
- 两台服务器之间**不开任何 TCP 端口**,文件只经光闸在 `gate/out` ⇄ 对端 `gate/in` 之间单向搬运。
- 每台机器一个独立 compose 栈,同一份 `docker-compose.yml`,差别全在 `.env`。
- 摆渡目录由光闸设备挂载/同步进宿主机,应用只认容器内固定路径 `/data/transfer/send`、`/data/transfer/receive`。
---
## 2. 交付包内容
本目录本身**就是**交付包,`.env`、`sql`、`images/*.tar` 都已就位,整目录拷走即可装机(不需要现场再回仓库取任何东西)。
```
deploy/offline/
├── docker-compose.yml 一台机器的 5 个服务(web/app/pg/redis/mq),现场不改
├── .env.iw 内网机出厂配置(已填好,含口令)← install.sh iw 会拷成 .env
├── .env.ow 外网机出厂配置(同上,口令与内网机不同)
├── .env.example 空白模板,只在客户环境差异大到出厂值不适用时才用
├── VERSION.txt 本次出盘的版本/平台/时间/镜像清单
├── config/application.yml 静态默认配置,里面不放任何口令
├── nginx/default.conf.template 前端反代(内/外网共用,靠 SFILE_SITE_ROOT 选站点)
├── sql/01-init-table.sql 全量建表(92K,含 x_file_security)
├── sql/02-init-data.sql 初始数据(2.8K,admin、机构、平台编码)
├── images/*.tar 5 个镜像,共 650M(docker save 原始大小)
├── images/SHA256SUMS.txt 交付盘校验值,install.sh 开机先比对它
└── scripts/
├── install.sh 现场一键安装(要带 iw / ow 参数)
├── smoke_test.py 现场自动化验收(18 条断言)
└── export-images.sh 我方出盘脚本,现场用不到
```
> 仓库里 `images/*.tar`、`SHA256SUMS.txt`、`dist/` 已被 `.gitignore` 排除——镜像 tar 不进气仓,只走交付盘。
镜像清单与已验证版本(tag 必须与 tar 里的完全一致,`install.sh` 会核对 sha256、架构与 tag 三者):
| 镜像 | 交付 tag | tar 大小 | 本机验证版本 | 说明 |
|---|---|---|---|---|
| `sfile-app` | `1.0.0` | 313M | JDK8 运行时 eclipse-temurin:8-jre-jammy | 内含 s-file.jar |
| `sfile-web` | `1.0.0` | 39M | nginx:alpine | **内网包与外网包都在同一个镜像里** |
| `postgres` | `18` | 161M | 18.3 | |
| `redis` | `8` | 53M | 8.6.3 | |
| `rabbitmq` | `3.13-management-alpine` | 84M | 3.13.7 | |
---
## 3. 现场要先定的 6 件事
| 项 | 变量 | 建议 |
|---|---|---|
| 服务器 CPU 架构 | — | `uname -m`:x86_64→amd64,aarch64(鲲鹏/飞腾)→arm64。**出盘前必须确认**,架构不符装不上 |
| 前端对外端口 | `WEB_PORT` | 出厂值 80;被占用改 8080/9080,同步改客户访问地址 |
| 数据盘根目录 | `SFILE_DATA_ROOT` | 独立挂载点,出厂值 `/data/sfile`。上传文件+库+日志都在这,按 500MB/文件上限估:`文件平均大小 × 日均份数 × 保留天数`,一般 ≥200G |
| 光闸摆渡目录 | `GATE_SEND_DIR` / `GATE_RECV_DIR` | 与光闸厂商确认挂载点,**发出/收到是两个不同目录**,两侧机器各自配自己的 |
| MeshSafe 地址与密钥 | `MESHSAFE_BASE_URL` / `MESHSAFE_SECRET` | 同机:`http://host.docker.internal:44380`;独立机:`http://<IP>:<端口>`;`MESHSAFE_SECRET` 出厂是占位值,**接了检测就必须按 MeshSafe 交付单改**,没接就 `MESHSAFE_ENABLED=false` |
| 各类口令 | `POSTGRES_PASSWORD` 等 | 出厂已给随机值可直接跑,正式交付按客户安全要求轮换(见 §5.1) |
机器规格建议:8 核 / 16G 起(应用堆 `-Xmx` 3G,PG + MQ + Redis 还要约 2-3G,留足页缓存),系统盘 100G + 数据盘独立。
---
## 4. 出盘(我方,需要联网一次)
```bash
# 前置:backend/target/s-file.jar 与 frontend/ 已构建
cd deploy/offline
./scripts/export-images.sh 1.0.0 # amd64
TARGET_PLATFORM=linux/arm64 ./scripts/export-images.sh 1.0.0 # 鲲鹏/飞腾
```
脚本会把 `images/*.tar`、`sql/*.sql`、`.env.iw/.env.ow` 的镜像 tag、`SHA256SUMS.txt` 就地刷新,
再打一份 `dist/sfile-offline-<版本>-<平台>.tar.gz` 用于拷盘/刻录。带盘就带这个 gz。
---
## 5. 装机(客户侧,两台机器各执行一次)
```bash
# 1) 解包(两台机器用同一个盘,解出来的目录一模一样)
tar -xzf sfile-offline-1.0.0-linux-amd64.tar.gz -C /opt/
cd /opt/sfile-offline-1.0.0-linux-amd64
# 2) 只改必须改的:MeshSafe 密钥(不接检测就关掉开关)
vi .env.iw # 内网机;外网机改 .env.ow
# 3) 一键装:按角色建 .env -> 自检 -> 校验 tar -> docker load -> 建目录 -> 起栈 -> 等健康
./scripts/install.sh iw # 内网机
./scripts/install.sh ow # 外网机
```
`install.sh` 第一次执行会把 `.env.iw`/`.env.ow` 拷成 `.env`,之后只读 `.env`——现场改动不会被覆盖。
想回到出厂值:`rm .env && ./scripts/install.sh iw`。
参数与已有 `.env` 的角色不一致时直接拒绝,防止在内网机上起了外网栈。
装完会打印访问地址。首次登录后**立刻改掉 admin 的初始口令**。
### 5.1 出厂口令(交付单要抄一份,并建议现场轮换)
| 变量 | 内网机出厂值 | 外网机出厂值 | 现场是否必须改 |
|---|---|---|---|
| `POSTGRES_PASSWORD` | `Sfile-iw-Pg-N1tOz1MxgdaS` | `Sfile-ow-Pg-pT-673--i1rk` | 建议(只在本机容器网络内生效,不对网暴露) |
| `MQ_PASSWORD` | `Sfile-iw-Mq-UTqVSnCDZ0LB` | `Sfile-ow-Mq-sfW5Cv-Nk-cA` | 建议(同上) |
| `JWT_SECRET` | `86a6b015…7d70`(64 位) | `9eeeea03…6988`(64 位) | 必须保密;泄露等于能伪造登录态,两侧已不同 |
| `USER_DEFAULT_PASSWORD` | `Sfile-iw-Usr-FBLS-0fA` | `Sfile-ow-Usr-WqDJ7PDS` | **必须改**:新建用户的初始密码,业务员要知道 |
| admin 登录口令 | 由 `backend/db/init-data.sql` 灌入 | 同左 | **必须改**:装完第一次登录就改 |
| `MESHSAFE_SECRET` | `CHANGE_ME_meshsafe_secret` | `CHANGE_ME_meshsafe_secret` | **故意留空**:以 MeshSafe 侧 `MESHSAFE_API_CLIENTS` 白名单为准,装机前替换 |
> `install.sh` 只对**启用了 MeshSafe 且 secret 还是占位值**的情况卡住;`MESHSAFE_ENABLED=false` 时其余占位值不拦,方便先装后接检测。
### 5.2 内网机 / 外网机配置差异
`.env.iw` 与 `.env.ow` 已按下表分别填好,这里列出来只为让客户网络/安全评审能对上:
| 变量 | 内网机 | 外网机 |
|---|---|---|
| `SFILE_ROLE` | `iw` | `ow` |
| `SFILE_TRANSFER_LOCATION` | `1` | `2` |
| `SFILE_SITE_ROOT` | `/usr/share/nginx/inside` | `/usr/share/nginx/outside` |
| `GATE_SEND_DIR` | `/data/sfile/gate/out`(光闸的**内→外**发出目录) | `/data/sfile/gate/out`(光闸的**外→内**发出目录) |
| `GATE_RECV_DIR` | `/data/sfile/gate/in`(光闸的**外→内**投递目录) | `/data/sfile/gate/in`(光闸的**内→外**投递目录) |
| `MESHSAFE_CHANNEL` / `MESHSAFE_CLIENT_ID` | `sfile-iw` | `sfile-ow` |
| `JWT_SECRET` / `POSTGRES_PASSWORD` / `MQ_PASSWORD` | 一套 | 另一套(已确认不同) |
| `WEB_PORT` | 80 | 80(两台机器各占自己的 80,不冲突) |
| `APP_SUBNET` / `APP_NODE_IP` / `APP_WORK_ID` | `172.30.88.0/24` / `.10` / `1` | `172.30.98.0/24` / `.10` / `2` |
> 两侧网段/机器号故意错开:客户如果先把两台合并成一台机器做演示,不改也能同起。
> `SFILE_TRANSFER_LOCATION` 写反是本方案最容易踩的坑:值对调后,两侧都以为自己是发送端,摆渡目录里的包永远不会被解包。
---
## 6. 验收清单(现场按这个走一遍)
| # | 动作 | 期望 |
|---|---|---|
| 1 | 浏览器开 `http://<内网机IP>:<端口>` | 左上角有「内网」徽标,能登录 |
| 2 | 开 `http://<外网机IP>:<端口>` | 无「内网」徽标(设计上只有内网侧显示标识),能登录 |
| 3 | 内网上传一个正常文件 | 「安全检测」列几秒内从**检测中**变**检测通过** |
| 4 | 内网上传 EICAR 测试串 | 变**检测不通过**,理由含 `Eicar-Test-Signature` 或 `内嵌 EICAR` |
| 5 | 对该文件点分享/传输 | 被拒:`安全检测未通过,禁止分享与传输` |
| 6 | 内网把通过的文件「传输」到外网 | 内网 `GATE_SEND_DIR` 出现 `<n>_<id>.zip` |
| 7 | 等 1 分钟内网监听任务 | 外网「内穿外」目录出现 `<原名>_<时间戳>` 文件夹,文件在内 |
| 8 | 外网该文件的安全检测 | 自动送检并变**检测通过** |
| 9 | 内网对不通过文件做人工复核放行 | 状态变**检测通过**、标记**人工复核**,之后可传输 |
| 10 | `docker compose ps` 两台机各 5 个 healthy | 无 `Restarting`/`unhealthy` |
第 3-9 步可用 `scripts/smoke_test.py` 自动跑(在有 python3 + requests 的机器上,指向客户 IP),现场没条件就手工点。
```bash
SFILE_IW_BASE=http://<内网机IP>:<端口>/api \
SFILE_OW_BASE=http://<外网机IP>:<端口>/api \
SFILE_ADMIN_USER=admin SFILE_ADMIN_PWD='<现场初始口令>' \
python3 scripts/smoke_test.py
```
---
## 7. 运维
```bash
docker compose ps # 状态
docker logs -f sfile-iw-app # 后端日志(含送检/摆渡全过程)
docker compose up -d app # 改完 .env 或 config 后重启后端
docker compose down # 停止(不动数据)
docker compose down -v --remove-orphans # ⚠ 数据在 bind 目录里,不会丢;但别顺手执行
```
**备份只需三样**:`$SFILE_DATA_ROOT/pgdata`(库)、`$SFILE_DATA_ROOT/upload`(文件本体)、`.env`(口令)。
恢复:换机器后原路径放回 + `docker load` + `install.sh`。
数据库升级:新版本的增量 SQL 放 `backend/db/upgrade-*.sql`,人工执行
```bash
docker exec -i sfile-iw-pg psql -U sfile -d s_file < upgrade-20261101-meshsafe-file-security.sql
```
(`sql/01/02` 只在**空库首次启动**执行,已有数据卷会被忽略。)
---
## 8. 已知坑与取舍
1. **占位值检查是有意的,别图省事先注释掉**:`install.sh` 只在「`MESHSAFE_ENABLED=true` 且 `MESHSAFE_SECRET` 还是占位值」时拒绝启动,其余 `CHANGE_ME` 也会拦。没接检测就老实写 `MESHSAFE_ENABLED=false`。
2. **交付盘必须整目录拷**:`install.sh` 会核 5 个 tar 的 sha256、个数、以及 `.env` 里 5 个镜像 tag 是否都能 load 到。只拷脚本不拷 `images/`(tar 不进气仓,见 `.gitignore`)是最常见的现场翻车。
3. **架构不符装不上**:`sfile-app`/`sfile-web` 是在构建机上 `docker build` 出来的,信创 ARM 机必须用 `TARGET_PLATFORM=linux/arm64` 重新出盘(`install.sh` 会拦你)。
4. **PostgreSQL 18 的数据目录挂载点是 `/var/lib/postgresql`**(15 及以前是 `/var/lib/postgresql/data`)。自己换库版本时改 `.env` 的 `PG_DATA_DIR`,否则容器会报「检测到未挂载的数据」并反复重启。
5. **nginx 反代必须透传带端口的 Host**(模板里已用 `$http_host`)。改成 `$host` 会让后端 `RequestUtils.getRootDomain` 对无端口地址抛 `IllegalStateException`,症状就是**登录 500**。
6. **发号器 workerId 必须能取到,否则应用起不来。** 后端按 `code.snowflake.workid.<app容器IP>` 取机器号,取不到时回退默认值 `99`,而 99 超出雪花 5 位上限,启动即 `IllegalArgumentException: worker Id can't be greater than 31`。本方案的做法是把 app 的容器 IP 锁死(`APP_NODE_IP`)并由 compose 自动写进 JVM 参数,`.env` 里 `APP_SUBNET / APP_GATEWAY / APP_NODE_IP` 三个值必须同网段。**只想改代码解决的话**,把 `CodeConfiguration#snowFlakeCodeGenerator` 的兜底 `99L` 改成一个合法值即可解除这层 IP 耦合——但那样同侧多实例会撞 ID,需自行权衡。
7. **MeshSafe 凭证**:`MESHSAFE_CLIENT_ID`/`SECRET` 必须在 MeshSafe 侧 `MESHSAFE_API_CLIENTS` 白名单里存在,否则送检全部 401。`MESHSAFE_FAIL_OPEN=true` 时检测服务挂了不阻断业务(只记失败),等保从严请置 `false`。
8. **`host.docker.internal` 需要 Docker ≥ 20.10**;MeshSafe 在独立服务器时不需要它,可把 compose 里那两行 `extra_hosts` 删掉。
9. **PDF 盖章依赖 spire.pdf 是桩实现**:本机因拿不到商业包做了替代 jar,运行时调用会抛 `UnsupportedOperationException`。交付前若客户要电子签章功能,必须换回 `e-iceblue:spire.pdf:3.7.4` 正式包重新打 jar。
10. **摆渡延迟固定为监听周期**(默认 1 分钟一次)。客户嫌慢就调 `FILE_TRANSFER_MONITOR_CRON`,不要改成秒级——它会整目录扫描。
11. **两侧数据库完全独立**,同一份 `init-data.sql` 灌出来 admin 初始口令一致,交付时两侧都要改。
12. **装完才发现角色装反了**(内网机上跑了 `install.sh ow`):`.env` 与参数角色不一致时 `install.sh` 直接拒绝,所以真发生一定是 `rm .env` 之后重装。compose 项目名跟着角色走,旧项目还在占用 `APP_SUBNET`,会报 `invalid pool request: Pool overlaps with other one on this address space`。先把旧项目拆掉再起:
`docker compose -p sfile-<旧角色> down` → `rm .env` → `./scripts/install.sh <正确角色>`。
13. **检测结论按文件内容复用**:MeshSafe 的 `fileCode` 是内容寻址,同一份字节重复上传会直接沿用上次结论,不会重新扫。这是性能上的设计,但等保评审可能会问「改判是否会被复用绕过」,现场演示验收时请用**每次内容都不同**的测试文件。