forked from wangziqi/gongxue-base
feat: harden production storage readiness
This commit is contained in:
@@ -61,6 +61,8 @@
|
||||
- 已补租户后台媒体运营报表,支持资料/视频访问汇总、Top 资源/视频和 traceId 回查;大租户后续再把实时查询替换为日/周预聚合。
|
||||
- 已收紧锁定资源 CDN 边界:`members/svip/private` 外链默认拒绝,必须显式 provider-managed 才允许;视频绑定资源也复用该策略。
|
||||
- 已补外部 HTTP 杀毒/内容安全 scanner 接入层、失败关闭、生产 readiness 阻断和动态水印 traceId 审计;继续联调真实扫描服务、转码/CDN 级水印、CDN 刷新和对象生命周期策略。
|
||||
- 已补 API/worker 生产启动 fail-fast:生产环境会拒绝 `local_dev` 存储、空 bucket、关闭租户前缀;worker 会拒绝未接入外部 HTTP scanner 或 fail-open。
|
||||
- 已补 `docs/refactor/object-storage-production-runbook.md`,真实云厂商联调必须按 runbook 抽样验收。
|
||||
- `content_assets` 继续作为资源台账,不允许前端绕过台账直接访问私有资源。
|
||||
|
||||
3. 真实导入 dry-run
|
||||
|
||||
216
docs/refactor/object-storage-production-runbook.md
Normal file
216
docs/refactor/object-storage-production-runbook.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# 对象存储生产验收 Runbook
|
||||
|
||||
更新时间:2026-06-30
|
||||
|
||||
本系统的题图、PDF、视频、音频、资料包、题库导出文件都必须进入 `content_assets` 台账,并由 `apps/api` 做权限判断、短期签名、水印和审计。前端不得直接持有云厂商密钥、service role key、私有 bucket 路径或长期私有资源 URL。
|
||||
|
||||
## 官方能力依据
|
||||
|
||||
- 阿里云 OSS 官方文档说明可由服务端生成预签名 URL,客户端用该 URL 上传或下载对象,并通过有效期限制访问时间。阿里云也建议在更复杂的大文件场景中评估 STS 授权直传。参考:[OSS 预签名 URL 上传](https://help.aliyun.com/zh/oss/user-guide/upload-files-using-presigned-urls)、[OSS 预签名 URL 下载/预览](https://www.alibabacloud.com/help/zh/oss/user-guide/how-to-obtain-the-url-of-a-single-object-or-the-urls-of-multiple-objects)。
|
||||
- 腾讯云 COS 官方文档说明预签名 URL 可用于临时上传/下载私有对象,URL 中携带签名和有效期;官方也建议签名有效期设置为完成本次操作所需的最短期限。参考:[COS 预签名 URL 访问](https://cloud.tencent.com/document/product/436/68284)、[COS 预签名授权上传](https://cloud.tencent.com/document/product/436/14114)。
|
||||
- Supabase Storage 官方文档说明 `createSignedUrl` 可以按秒设置下载 URL 有效期,访问私有对象需要相应 `select` 权限;本项目由后端 service key 集中签名,前端只拿短期 URL。参考:[Supabase Storage signed URL](https://supabase.com/docs/reference/javascript/storage-from-createsignedurl)、[Supabase Storage downloads](https://supabase.com/docs/guides/storage/serving/downloads)。
|
||||
|
||||
## 生产阻断项
|
||||
|
||||
上线前必须满足:
|
||||
|
||||
- `NODE_ENV=production` 下 API 和 worker 都不能使用 `STORAGE_DEFAULT_PROVIDER=local_dev`。
|
||||
- `STORAGE_DEFAULT_BUCKET` 必须配置。
|
||||
- `STORAGE_REQUIRE_TENANT_PREFIX=true` 必须保持开启。
|
||||
- 阿里云 OSS 必须配置 `ALIYUN_OSS_REGION` 或 `ALIYUN_OSS_ENDPOINT`,以及 AccessKey。
|
||||
- 腾讯云 COS 必须配置 `TENCENT_COS_REGION`、`TENCENT_COS_APP_ID`、`TENCENT_COS_SECRET_ID`、`TENCENT_COS_SECRET_KEY`。
|
||||
- Supabase Storage 必须配置 `SUPABASE_STORAGE_URL` 和 `SUPABASE_STORAGE_SERVICE_KEY`。
|
||||
- `WORKER_ASSET_SECURITY_SCANNER` 生产必须包含 `http`,例如 `metadata_rules,http`。
|
||||
- `WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT` 必须是 HTTPS 生产地址。
|
||||
- `WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN` 必须是强随机密钥。
|
||||
- `WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false`,外部扫描服务不可用时必须 fail-closed。
|
||||
|
||||
对应自动化命令:
|
||||
|
||||
```bash
|
||||
npm run test:readiness
|
||||
npm run readiness:production
|
||||
npm run readiness:production:db
|
||||
```
|
||||
|
||||
其中 `test:readiness` 会同时验证 production readiness 脚本和 API/worker 的生产配置 fail-fast。
|
||||
|
||||
## 推荐环境变量模板
|
||||
|
||||
阿里云 OSS:
|
||||
|
||||
```text
|
||||
NODE_ENV=production
|
||||
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
||||
STORAGE_DEFAULT_BUCKET=tiku-assets-prod
|
||||
STORAGE_REQUIRE_TENANT_PREFIX=true
|
||||
ALIYUN_OSS_REGION=cn-hangzhou
|
||||
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
||||
ALIYUN_OSS_ACCESS_KEY_ID=...
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET=...
|
||||
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.example.com/api/scan
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=...
|
||||
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
||||
```
|
||||
|
||||
腾讯云 COS:
|
||||
|
||||
```text
|
||||
NODE_ENV=production
|
||||
STORAGE_DEFAULT_PROVIDER=tencent_cos
|
||||
STORAGE_DEFAULT_BUCKET=tiku-assets-prod
|
||||
STORAGE_REQUIRE_TENANT_PREFIX=true
|
||||
TENCENT_COS_REGION=ap-shanghai
|
||||
TENCENT_COS_APP_ID=...
|
||||
TENCENT_COS_SECRET_ID=...
|
||||
TENCENT_COS_SECRET_KEY=...
|
||||
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.example.com/api/scan
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=...
|
||||
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
||||
```
|
||||
|
||||
Supabase Storage:
|
||||
|
||||
```text
|
||||
NODE_ENV=production
|
||||
STORAGE_DEFAULT_PROVIDER=supabase_storage
|
||||
STORAGE_DEFAULT_BUCKET=tiku-assets-prod
|
||||
STORAGE_REQUIRE_TENANT_PREFIX=true
|
||||
SUPABASE_STORAGE_URL=https://<project-ref>.supabase.co/storage/v1
|
||||
SUPABASE_STORAGE_SERVICE_KEY=...
|
||||
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.example.com/api/scan
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=...
|
||||
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
||||
```
|
||||
|
||||
## 云侧配置验收
|
||||
|
||||
以下配置无法仅靠仓库代码证明,必须在云控制台或云 API 上确认:
|
||||
|
||||
| 项目 | 验收标准 |
|
||||
| --- | --- |
|
||||
| Bucket 权限 | 默认私有;公开 bucket 只允许存放确认为公开的品牌素材 |
|
||||
| CORS | 只允许学生端 H5、租户后台 H5、平台后台 H5 域名和微信小程序合法域名;允许必要方法 `GET/HEAD/PUT` 和必要 headers |
|
||||
| RAM/CAM 权限 | 服务端密钥最小权限,仅允许目标 bucket 的指定前缀读写和 HEAD;不要使用主账号密钥 |
|
||||
| 租户前缀 | 对象 key 必须以 `{tenantId}/` 开头,bucket policy 不应允许跨前缀写入 |
|
||||
| CDN/防盗链 | 私有资料、SVIP 资料、视频、资料包禁止普通长效 CDN URL;如使用 CDN,必须由 provider 侧签名或回源鉴权 |
|
||||
| 生命周期 | 临时上传、导出中间文件、失败扫描资源设置清理规则;正式资料按业务保留周期设置归档或低频 |
|
||||
| 版本控制/备份 | 生产 bucket 开启版本控制、跨区域复制或定时备份,满足误删和容灾要求 |
|
||||
| 日志审计 | 开启对象访问日志或云审计,至少保留一个完整售后周期 |
|
||||
| 内容安全 | 外部 scanner 真实接入,并覆盖 PDF、图片、压缩包、视频封面或转码前文件 |
|
||||
|
||||
## 联调步骤
|
||||
|
||||
1. 启动 API 和 worker,确保不是 `local_dev` provider。
|
||||
|
||||
```bash
|
||||
npm run build:api
|
||||
npm run build:worker
|
||||
npm run readiness:production
|
||||
npm run readiness:production:db
|
||||
```
|
||||
|
||||
2. 租户后台申请上传签名:
|
||||
|
||||
```text
|
||||
POST /api/tenant-content/assets/sign-upload
|
||||
```
|
||||
|
||||
验收点:
|
||||
|
||||
- 返回 URL 为目标云厂商域名或 Supabase Storage 域名。
|
||||
- `headers` 包含上传所需 `content-type`。
|
||||
- `objectKey` 带当前 `tenantId/` 前缀。
|
||||
- `content_asset_access_events` 写入 `upload_sign`。
|
||||
|
||||
3. 前端直传云存储后调用确认上传:
|
||||
|
||||
```text
|
||||
POST /api/tenant-content/assets/confirm-upload
|
||||
```
|
||||
|
||||
验收点:
|
||||
|
||||
- 后端读取云对象元数据。
|
||||
- 大小、MIME、checksum 能对上。
|
||||
- 状态变为 `uploadStatus=verified`、`securityScanStatus=pending`。
|
||||
- 即使传 `publish=true`,也不能绕过扫描直接发布。
|
||||
|
||||
4. 运行 assets worker:
|
||||
|
||||
```bash
|
||||
npm --workspace @tiku-saas/worker run assets:once
|
||||
```
|
||||
|
||||
验收点:
|
||||
|
||||
- 外部 scanner 收到请求。
|
||||
- scanner token 未进入前端响应。
|
||||
- 扫描通过后 `securityScanStatus=passed`。
|
||||
- 扫描失败、超时或服务不可用时资源退回 `draft`,并记录 `content_asset_security_scan_events`。
|
||||
|
||||
5. 后台发布资源:
|
||||
|
||||
```text
|
||||
PUT /api/tenant-content/assets
|
||||
```
|
||||
|
||||
验收点:
|
||||
|
||||
- 只有 `verified + passed` 的托管对象能发布为 `active`。
|
||||
- `failed/skipped/pending` 资源发布被拒绝。
|
||||
|
||||
6. 学生端预览/下载:
|
||||
|
||||
```text
|
||||
GET /api/catalog/assets/preview?assetId=...
|
||||
GET /api/catalog/assets/download?assetId=...
|
||||
```
|
||||
|
||||
验收点:
|
||||
|
||||
- 未登录、无权益、跨租户、扫描未通过都被拒绝。
|
||||
- 私有/SVIP/视频/资料包 TTL 不超过 300 秒。
|
||||
- 响应包含 `watermark.mode=visible_overlay` 和 `traceId`。
|
||||
- `content_asset_access_events` 记录 granted/denied、TTL、signatureMode、watermark traceId。
|
||||
|
||||
7. 题库导出 worker:
|
||||
|
||||
```bash
|
||||
npm --workspace @tiku-saas/worker run exports:once
|
||||
```
|
||||
|
||||
验收点:
|
||||
|
||||
- PDF/Word/每日一练 ZIP 写入对象存储。
|
||||
- 自动创建 `content_assets`。
|
||||
- `securityScanProvider=trusted_export_worker`。
|
||||
- 后台通过 `sign-download` 或 `sign-preview` 取短签名 URL。
|
||||
|
||||
## 抽样清单
|
||||
|
||||
每次生产联调至少抽样:
|
||||
|
||||
- PDF 10 个,含大文件、中文文件名、水印预览。
|
||||
- 图片 20 张,含题图、手册图、品牌图。
|
||||
- 视频 10 个,含有会员限制和播放次数限制的题目视频。
|
||||
- ZIP/资料包 5 个,含每日一练导出包。
|
||||
- 扫描失败样本 3 个,确认不能发布、不能下载。
|
||||
- 跨租户访问 5 组,确认无法签名下载或预览。
|
||||
- 私有 CDN URL 样本 3 个,未标记 provider-managed 时必须拒绝。
|
||||
|
||||
## 准出标准
|
||||
|
||||
满足以下条件后,才允许把资料、题图、视频迁到生产对象存储:
|
||||
|
||||
- `npm run test:readiness` 通过。
|
||||
- `npm run readiness:production` 无 blocker。
|
||||
- `npm run readiness:production:db` 无 blocker。
|
||||
- `npm run test:worker:assets` 通过。
|
||||
- API/worker 在 `NODE_ENV=production` 下无法用 `local_dev` 启动。
|
||||
- 真实云存储上传、确认、扫描、发布、预览、下载、导出全链路通过。
|
||||
- 云侧 CORS、防盗链、生命周期、备份/版本控制、访问日志有截图或变更记录。
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
题库里的图片、PDF、视频、音频、资料包等媒体资源统一走 `content_assets` 台账和后端签名接口。前端不直接保存或读取云厂商密钥,也不直接拼接私有资源 URL。题目视频播放还需要经过 `POST /api/videos/play` 校验 SVIP 或视频次数权益后下发短期签名 URL。
|
||||
|
||||
生产上线验收请按完整 runbook 执行,包含 API/worker production fail-fast、readiness、云控制台配置、上传确认、安全扫描、短签名、水印、跨租户和导出资源抽样:
|
||||
|
||||
```text
|
||||
docs/refactor/object-storage-production-runbook.md
|
||||
```
|
||||
|
||||
已接入的 provider:
|
||||
|
||||
- `local_dev`:本地开发占位签名,便于前后端联调。
|
||||
|
||||
Reference in New Issue
Block a user