forked from wangziqi/gongxue-base
feat: harden content asset access
This commit is contained in:
@@ -91,14 +91,16 @@
|
||||
| --- | --- | --- |
|
||||
| 内容资源台账 | 可联调 | `content_assets` |
|
||||
| 租户后台资源维护 | 可联调 | `/api/tenant-content/assets` |
|
||||
| 学生端资源列表/下载签名 | 可联调 | `/api/catalog/assets`、`/api/catalog/assets/download` |
|
||||
| 学生端资源列表/下载签名 | 可联调 | `/api/catalog/assets`、`/api/catalog/assets/download`;学生端锁定资源使用短 TTL,访问事件写入 `content_asset_access_events` |
|
||||
| 阿里云 OSS 签名 | 可联调 | `aliyun_oss` provider |
|
||||
| 腾讯 COS 签名 | 可联调 | `tencent_cos` provider |
|
||||
| Supabase Storage 签名 | 可联调 | `supabase_storage` provider |
|
||||
| 上传后对象校验 | 可联调 | `/api/tenant-content/assets/confirm-upload`;托管对象必须 verified 后才能发布/下载 |
|
||||
| PDF/图片预览签名 | 可联调 | `/api/catalog/assets/preview`、`/api/tenant-content/assets/sign-preview`;使用 inline 短期签名 |
|
||||
| PDF/图片预览签名 | 可联调 | `/api/catalog/assets/preview`、`/api/tenant-content/assets/sign-preview`;使用 inline 短期签名,学生预览默认短 TTL |
|
||||
| 托管资源 worker 复检 | 可联调 | `apps/worker --job assets` 定期复检 pending/verified 对象元数据;异常资源会标记 failed 并从 active 退回 draft,写入审计和 `security_flags` |
|
||||
| 深度防盗链/水印/杀毒 | 待补齐 | 商用上线前继续补 CDN 防盗链、动态水印、安全扫描和对象生命周期策略 |
|
||||
| 资源访问审计 | 可联调 | `content_asset_access_events` + `GET /api/tenant-content/assets/access-events`;记录上传签名/确认、学生下载/预览、后台下载/预览的 granted/denied、TTL、签名模式、IP 和 UA |
|
||||
| CDN 访问边界 | 可联调 | `members/svip/private` 外部 CDN URL 默认拒绝,必须显式 `metadata.providerManagedAccess=true` 或 `cdnAccessMode=signed_by_provider`;视频绑定资源也复用该规则 |
|
||||
| 深度防盗链/水印/杀毒 | 待补齐 | 商用上线前继续补动态水印、安全扫描、CDN 刷新和对象生命周期策略 |
|
||||
|
||||
## 订单、会员、营销
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
- 已接阿里云 OSS、腾讯云 COS、Supabase Storage 的上传/下载签名 provider。
|
||||
- 已补上传后对象确认接口、托管对象发布前 verified 校验、PDF/图片 inline 预览签名。
|
||||
- 已补 assets worker 复检,异常托管对象会自动下架并记录审计。
|
||||
- 继续补视频深度防盗链、动态水印、杀毒扫描、CDN 刷新和对象生命周期策略。
|
||||
- 已补资源访问事件 `content_asset_access_events`,覆盖上传签名/确认、学生下载/预览、后台下载/预览的 granted/denied、短 TTL、签名模式、IP 和 UA。
|
||||
- 已收紧锁定资源 CDN 边界:`members/svip/private` 外链默认拒绝,必须显式 provider-managed 才允许;视频绑定资源也复用该策略。
|
||||
- 继续补动态水印、杀毒扫描、CDN 刷新和对象生命周期策略。
|
||||
- `content_assets` 继续作为资源台账,不允许前端绕过台账直接访问私有资源。
|
||||
|
||||
3. 真实导入 dry-run
|
||||
@@ -222,5 +224,5 @@
|
||||
3. 补平台后台增强:租户详情/编辑、平台审计报表、自动计费、账单批量操作和更细平台权限点。
|
||||
4. 云服务器部署 Supabase/PostgreSQL 和 API,配置对象存储生产环境变量,跑 `check:refactor` 的远程等价测试。
|
||||
5. 导出现有 PocketBase 数据,做完整 dry-run 迁移。
|
||||
6. 并行补对象存储、真实登录、完整资金流水对账、题库导出模板精排/操作台、公共题库生产定时调度和失败告警。
|
||||
6. 并行补真实登录、完整资金流水对账、对象存储杀毒/水印/生命周期、题库导出模板精排/操作台、公共题库生产定时调度和失败告警。
|
||||
7. 前后端联调通过后,再做支付、权限、数据导入、资料下载、视频播放的商用验收。
|
||||
|
||||
@@ -52,6 +52,12 @@ GET /api/catalog/assets/preview?assetId=...
|
||||
POST /api/videos/play
|
||||
```
|
||||
|
||||
后台查看资源访问事件:
|
||||
|
||||
```text
|
||||
GET /api/tenant-content/assets/access-events?assetId=...
|
||||
```
|
||||
|
||||
后台管理员下载:
|
||||
|
||||
```text
|
||||
@@ -86,8 +92,9 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
4. 调用 `POST /api/tenant-content/assets/confirm-upload`,由后端读取对象元数据并比对大小、MIME、SHA-256。
|
||||
5. 校验通过且 `publish=true` 时,后端将资源置为 `status=active`、`uploadStatus=verified`。
|
||||
6. 学生端只能下载或预览 `active + verified` 的托管对象资源。
|
||||
7. 生产环境定时运行 assets worker,复检 `pending/verified` 托管对象的大小、MIME、SHA-256 等元数据。
|
||||
8. 如果复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致,worker 会把资源置为 `uploadStatus=failed`,并将 `active` 资源退回 `draft`,同时写入 `security_flags.assetRecheckFailed=true` 和 `audit_logs`。
|
||||
7. 学生端和后台管理员下载/预览都会写入 `content_asset_access_events`,包含 `assetId/userId/accessType/result/expiresInSec/signatureMode/ip/userAgent` 等审计字段。
|
||||
8. 生产环境定时运行 assets worker,复检 `pending/verified` 托管对象的大小、MIME、SHA-256 等元数据。
|
||||
9. 如果复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致,worker 会把资源置为 `uploadStatus=failed`,并将 `active` 资源退回 `draft`,同时写入 `security_flags.assetRecheckFailed=true` 和 `audit_logs`。
|
||||
|
||||
## 题库导出生成资源
|
||||
|
||||
@@ -131,11 +138,45 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
- 上传会校验 MIME 类型和文件大小。
|
||||
- 托管对象资源未确认前不能发布为 `active`,学生端不可下载。
|
||||
- PDF/图片预览使用 `inline` 签名,不等同于长期公开 URL。
|
||||
- 下载和视频播放必须先经过 API 权限判断,再下发短期签名 URL。
|
||||
- 下载、预览和视频播放必须先经过 API 权限判断,再下发短期签名 URL。
|
||||
- 学生端 `preview`、锁定资料下载、视频和资料包默认使用更短 TTL。当前策略是学生 inline 预览、`members/svip/private` 资料、`video/package` 资源最多 300 秒;租户后台预览最多 3600 秒,后台下载最多 86400 秒。
|
||||
- `visibility in ('members','svip','private')` 的外部 CDN/直链资源默认会被拒绝,除非资源 `metadata.providerManagedAccess=true` 或 `metadata.cdnAccessMode='signed_by_provider'`。商用建议这类资源优先登记为 `objectKey`,由 API 生成 OSS/COS/Supabase Storage 私有签名 URL。
|
||||
- 每次上传签名、上传确认、学生下载/预览、后台下载/预览都会写入 `content_asset_access_events`。授权失败也会记录 `result=denied` 和 `denyCode`,用于租户后台排查资源访问问题。
|
||||
- 云厂商 AccessKey、SecretKey、Service Role Key 只存在服务端环境变量,不返回前端。
|
||||
- `content_assets` 是资源唯一台账,前端不得绕过台账直接访问私有 bucket。
|
||||
- 前端不能把 `uploadStatus=failed` 或 `status=draft` 的资源继续展示为可下载;列表仍返回时应展示“资料处理中”或“资源异常已下架”,真正下载/预览会被后端拒绝。
|
||||
|
||||
## 访问事件字段
|
||||
|
||||
资源访问事件表:
|
||||
|
||||
```text
|
||||
public.content_asset_access_events
|
||||
```
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `tenant_id` | 租户隔离维度 |
|
||||
| `asset_id` | 资源 ID,上传签名阶段可能为空 |
|
||||
| `user_id` | 学生或后台操作者 |
|
||||
| `actor_role` | `anonymous/student/tenant_content_editor/tenant_admin/system` |
|
||||
| `access_type` | `download/preview/admin_download/admin_preview/upload_sign/upload_confirm` |
|
||||
| `result` | `granted` 或 `denied` |
|
||||
| `deny_code` | 拒绝原因,例如 `ASSET_CDN_ACCESS_NOT_ALLOWED` |
|
||||
| `expires_in_sec` | 下发签名有效期 |
|
||||
| `signature_mode` | `local-placeholder`、`supabase-storage-signed-url`、`aliyun-oss-signature-url-v1`、`tencent-cos-signature-url-v5`、`public-or-provider-managed` |
|
||||
| `metadata` | 签名摘要、对象位置、文件名等排查信息;不保存云厂商密钥 |
|
||||
|
||||
后台接口:
|
||||
|
||||
```text
|
||||
GET /api/tenant-content/assets/access-events?assetId=<assetId>&limit=100
|
||||
```
|
||||
|
||||
前端只用于后台审计和排查,不要把 `content_asset_access_events` 当成学生端下载列表来源。
|
||||
|
||||
## 环境变量
|
||||
|
||||
通用:
|
||||
@@ -198,6 +239,6 @@ WORKER_ASSET_REQUEST_TIMEOUT_MS=10000
|
||||
|
||||
## 官方依据
|
||||
|
||||
- 阿里云 OSS Node.js SDK 支持通过 `signatureUrl` 为上传/下载生成带过期时间的签名 URL,并可通过对象 HEAD 读取元数据。
|
||||
- 腾讯云 COS XML API V5 签名由 `q-sign-algorithm`、`q-ak`、`q-sign-time`、`q-key-time`、`q-header-list`、`q-url-param-list`、`q-signature` 等字段组成,可用于预签名 URL 和 HEAD Object。
|
||||
- Supabase Storage 提供 `createSignedUploadUrl` 和 `createSignedUrl`,分别用于签名上传和签名下载;私有 bucket 仍应配合 RLS、服务端权限控制和资源台账。
|
||||
- Supabase Storage JavaScript `createSignedUrl` 用于为文件创建固定有效期的签名 URL,并要求对象具备 `select` 权限;本项目由服务端集中处理权限与签名。
|
||||
- 阿里云 OSS 官方文档建议服务端生成 signed URL 后让客户端直传对象,也说明私有对象可通过 presigned URL 在有效期内授权下载或预览。
|
||||
- 腾讯云 COS 文档给出的签名 URL 格式包含 `q-sign-algorithm`、`q-ak`、`q-sign-time`、`q-key-time`、`q-header-list`、`q-url-param-list`、`q-signature` 等字段;本项目服务端生成签名,不把 SecretKey 下发前端。
|
||||
|
||||
@@ -270,6 +270,37 @@ tenant:<tenantId>:theme
|
||||
| 公共题库采纳/同步 | `GET /api/tenant-content/public-question-banks`、`POST /api/tenant-content/public-question-banks/adopt`、`POST /api/tenant-content/public-question-banks/sync`、`GET /api/tenant-content/public-question-banks/conflicts?adoptionId=...`、`POST /api/tenant-content/public-question-banks/conflicts/resolve`、`POST /api/tenant-content/public-question-banks/conflicts/resolve-batch` |
|
||||
| 题库导出 | `POST /api/tenant-content/exports/questions`、`GET /api/tenant-content/exports/jobs` |
|
||||
|
||||
## 资料、PDF 和视频资源契约
|
||||
|
||||
前端必须把 `content_assets` 当成资源唯一台账。学生端资料、PDF 预览和题目视频播放都不能直接拼接私有 OSS/COS/Supabase Storage URL,也不能把后台配置的 `cdnUrl` 持久缓存成长期可访问地址。
|
||||
|
||||
学生端资料流程:
|
||||
|
||||
1. 列表页调用 `GET /api/catalog/assets`,只展示后端返回的 active 资源。
|
||||
2. 预览 PDF/图片时调用 `GET /api/catalog/assets/preview?assetId=...`。
|
||||
3. 下载资料时调用 `GET /api/catalog/assets/download?assetId=...`。
|
||||
4. 使用响应里的 `preview.url` 或 `download.url` 立即打开;不要写入本地长期缓存。
|
||||
|
||||
签名有效期规则:
|
||||
|
||||
- 学生 inline 预览、SVIP/会员资料、视频和资料包通常只有 300 秒左右有效期。
|
||||
- 后台预览有效期也不是永久 URL,租户后台应在用户点击时重新请求签名。
|
||||
- 响应里的 `expiresInSec/expiresAt/signatureMode` 只用于 UI 提示和排查,不要自行延长有效期。
|
||||
|
||||
锁定资源 CDN 规则:
|
||||
|
||||
- `visibility=members/svip/private` 的外部 `cdnUrl` 默认会被后端拒绝,返回 `ASSET_CDN_ACCESS_NOT_ALLOWED`。
|
||||
- 只有后台明确登记 `metadata.providerManagedAccess=true` 或 `metadata.cdnAccessMode='signed_by_provider'`,后端才允许把外部 URL 作为 provider-managed 资源返回。
|
||||
- 商用环境更推荐把锁定资料登记为 `objectKey`,由后端生成 OSS/COS/Supabase Storage 私有签名 URL。
|
||||
|
||||
租户后台排查:
|
||||
|
||||
```text
|
||||
GET /api/tenant-content/assets/access-events?assetId=<assetId>&limit=100
|
||||
```
|
||||
|
||||
该接口返回资源访问事件,包括学生下载、学生预览、后台下载、后台预览、上传签名、上传确认以及 denied 原因。租户后台可以在资源详情页增加“访问记录/异常记录”面板。
|
||||
|
||||
## 练习访问控制契约
|
||||
|
||||
前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 `POST /api/learning/practice-sessions`,后端会根据 `content_entries.accessRules`、`content_nodes.accessRules`、`question_collections.accessRules`、`practice_blueprints.accessRules` 和当前用户权益决定最终题目快照。
|
||||
|
||||
Reference in New Issue
Block a user