im 管理后台

This commit is contained in:
richjohnrambo
2026-09-13 23:39:18 +08:00
parent 8b30b8241c
commit 7aee265721
@@ -0,0 +1,570 @@
# 服务端消息加密与内容审核改造文档
> 文档状态:待开发
> 编制日期:2026-09-13
> 适用项目:`im_manager`(Admin Server)、`im_server`(IM Server)
> 本文档是后续数据库设计、后端开发、联调、迁移和验收的共同依据。
## 1. 建设目标
本次改造同时建设两项能力:
1. 内容审核:IM Server 在保存或修改内容前,根据商户策略执行本地敏感词检查和阿里云文本内容审核。
2. 消息加密存储:审核通过的消息在 IM Server 内部使用 AES-256-GCM 加密,国内 IM 业务库只保存密文;经授权的后台查询由 IM Server 解密后返回明文。
本次不做以下内容:
- 不做客户端 AES 二次加密。
- 不做端到端加密(E2EE)。
- 不改变现有 HTTPS/WSS 传输协议。
- 不允许 Admin Server 直接读取 IM 数据库并自行解密。
- 不在日志、Kafka 事件、死信或监控标签中保存消息明文。
## 2. 安全边界和部署前提
### 2.1 部署分工
| 区域 | 系统 | 保存数据 |
| --- | --- | --- |
| 国外 | HotGo / Admin Server | 商户审核策略、阿里云配置、字典根密钥、商户消息密钥 |
| 国内 | IM Server | 用户、会话、消息密文、审核结果和处置记录 |
该方案主要防护目标是:任意一侧数据库单独泄露时,不能直接恢复消息明文。
已知边界:HotGo 数据库、IM 数据库和完整代码同时泄露不在本方案的主要防护范围内。
### 2.2 传输安全
- 客户端调用必须使用 `https://` 和 `wss://`。
- 阿里云审核请求必须使用 HTTPS。
- Admin Server 与 IM Server 的内部配置和查询接口必须使用 HTTPS,并保留现有签名鉴权;跨公网部署时建议叠加 mTLS 或固定出口 IP 白名单。
- TLS 只解决传输保护,AES-256-GCM 解决消息落库后的静态保护。
## 3. 总体架构
```text
客户端
| HTTPS/WSS明文业务数据(TLS通道内不可见)
v
IM Server
|-- 实时权限、宵禁、用户状态等校验
|-- 获取商户审核策略
|-- 本地敏感词补充检查
|-- 阿里云 TextModerationPlus 审核
|-- AES-256-GCM 加密
`-- 密文写入国内IM库
Admin Server(国外HotGo)
|-- 管理阿里云配置
|-- 管理商户审核策略
|-- 管理商户消息密钥
|-- 通过内部接口下发配置/密钥
`-- 通过IM内部查询接口查看解密后消息
```
## 4. 内容审核设计
### 4.1 审核范围
| 场景 | 阿里云 Service | 审核时机 |
| --- | --- | --- |
| 用户昵称 | `nickname_detection_pro` | 更新用户资料前 |
| 群昵称 | `nickname_detection_pro` | 更新群成员资料前 |
| 群名称 | `nickname_detection_pro` | 创建群或修改群资料前 |
| 用户签名 | `nickname_detection_pro` | 更新用户资料前 |
| 单聊文本 | `chat_detection_pro` | 消息入库及广播前 |
| 群聊文本 | `chat_detection_pro` | 消息入库及广播前 |
| 编辑后消息 | `chat_detection_pro` | 新版本入库前 |
| 群公告 | `chat_detection_pro` | 发布或修改前 |
图片、视频和文件继续走文件审核链路,不属于本文本审核接口范围。
### 4.2 商户范围
- 第一阶段只为 `tenant.code=default` 的上架默认商户开启阿里云内容审核。
- 其他团队商户默认关闭,不调用阿里云,不产生审核费用。
- 数据模型保留按商户开启的能力,但只有平台管理员可变更。
- 商户端管理员不得看到阿里云 AK/SK,不得修改平台级审核服务配置。
### 4.3 后台配置项
#### 4.3.1 阿里云服务配置
复用 `im_external_config` 的 `service=moderation`,由平台管理员维护:
| 配置 | 说明 |
| --- | --- |
| `provider` | 固定为 `aliyun_green` |
| `endpoint` | 默认 `green-cip.cn-shanghai.aliyuncs.com` |
| `enabled` | 平台总开关 |
| `config_json.region_id` | 默认 `cn-shanghai` |
| `config_json.nickname_service` | 默认 `nickname_detection_pro` |
| `config_json.chat_service` | 默认 `chat_detection_pro` |
| `config_json.timeout_ms` | 单次请求超时,建议 1000~2000 ms |
| `config_json.failure_mode` | `allow` 或 `block` |
| `secret_json.access_key_id` | 只写不回显 |
| `secret_json.access_key_secret` | 只写不回显 |
“检查配置”必须真实调用阿里云测试文本,不能只检查字段非空。返回仅包含成功与否、请求耗时、RequestID 和脱敏错误。
#### 4.3.2 商户审核策略
可在 `im_security_policy.sensitive_word_policy` 中保存,避免新增重复策略表。JSON 结构固定为:
```json
{
"enabled": true,
"aliyun_enabled": true,
"local_words_enabled": true,
"scenes": {
"user_nickname": true,
"user_signature": true,
"group_name": true,
"group_nickname": true,
"private_message": true,
"group_message": true,
"group_announcement": true
},
"risk_actions": {
"high": "block",
"medium": "block",
"low": "audit_only",
"none": "allow"
},
"failure_mode": "allow",
"block_message": "内容不符合平台规范,请修改后重试"
}
```
策略发布后使用现有 `im.config.event` 发布配置事件,IM Server 按商户清理本地缓存。
### 4.4 文本提取
审核输入不能直接对任意 JSON 执行字符串化,应按 Drafty 或消息类型提取用户可见文本:
- 普通文本:提取完整文本。
- Drafty 富文本:提取 `txt` 及按钮、链接等用户可见标题。
- 图片/文件/语音:审核文件名、说明和附带文本,文件本体走文件审核。
- 系统消息、音视频信令和已签名服务端消息不调用文本审核。
- 空文本直接放行。
### 4.5 阿里云结果处置
IM Server 统一映射为:
| 内部结果 | 动作 |
| --- | --- |
| `allow` | 继续加密、入库和广播 |
| `block` | 不入库、不广播,返回统一业务错误 |
| `audit_only` | 允许发送,但写入审核命中日志 |
| `retryable_error` | 根据 `failure_mode` 放行或拦截 |
不只依赖 `RiskLevel/Label` 一个字段,需保留与验证阿里云原始结果中的扩展判断,针对引流、诈骗等样本建立回归测试集。
### 4.6 超时与降级
- 阿里云请求不得无限等待。
- 建议默认 `failure_mode=allow`,阿里云异常时允许发送并记录告警,防止第三方故障造成 IM 全局不可用。
- 连续失败达到阈值后进入短时熔断;熔断期间按失败策略处理。
- 阿里云调用不得在数据库事务中执行。
## 5. 消息加密设计
### 5.1 算法
- 算法:AES-256-GCM。
- DEK 长度:32 字节。
- Nonce:每次加密使用 `crypto/rand` 生成的独立 12 字节随机值。
- GCM 标签:使用 Go 标准库默认 16 字节认证标签。
- 明文编码:将原 `content` JSON 规范化序列化为 UTF-8 字节。
- 不允许固定 Nonce,不允许 AES-ECB,不使用未认证的 AES-CBC/CTR。
### 5.2 AAD
每条消息加密时绑定以下 AAD:
```text
tenant_id | topic | seqid | from | client_msg_id | key_version | content
```
`content` 是字面标识,用于与未来可能加密的 `head` 区分。AAD 按固定字段顺序和长度前缀编码,禁止使用可产生拼接歧义的简单字符串。
### 5.3 密钥层级
```text
HotGo字典根密钥(KEK)
-> 加密每个商户、每个版本的DEK
-> DEK加密该商户消息
```
根密钥使用 HotGo `hg_sys_dict_data` 保存,但必须满足:
1. 使用独立的内部字典类型,不出现在通用字典管理页。
2. 后端字典列表、选项和导出接口必须对该类型做服务端拒绝,不能仅依赖菜单隐藏。
3. 根密钥为 32 字节强随机值的 Base64 编码,不使用口令、UUID 或可读文本。
4. 根密钥只由 Admin Server 密钥服务读取,不通过通用配置接口返回。
5. 根密钥不写日志,不在后台页面回显,不进入 Kafka 消息。
### 5.4 商户密钥管理表
在 HotGo 库新增 `im_message_encryption_keys`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | bigint | 主键 |
| `tenant_id` | bigint | 商户 ID |
| `key_version` | int | 密钥版本,从 1 递增 |
| `algorithm` | varchar(32) | 固定 `AES-256-GCM` |
| `encrypted_dek` | blob | KEK 加密后的 DEK |
| `dek_nonce` | varbinary(12) | 加密 DEK 时的随机 Nonce |
| `status` | varchar(16) | `pending` / `active` / `retired` / `disabled` |
| `created_at` | datetime | 创建时间 |
| `activated_at` | datetime | 启用时间 |
| `retired_at` | datetime | 退役时间 |
| `created_by` | bigint | 操作人 |
| `remark` | varchar(255) | 备注 |
约束:
- 唯一键:`tenant_id + key_version`。
- 每个商户同时只能有一把 `active` 密钥。
- `retired` 密钥仍可用于解密历史消息,不得再用于新消息加密。
- `disabled` 只用于安全事故应急,禁用前必须完成历史消息转密钥。
### 5.5 IM 消息表改造
`im_messages` 和 `im_message_revisions` 新增:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `content_ciphertext` | longblob | 消息密文,包含 GCM 认证标签 |
| `content_nonce` | varbinary(12) | 本条消息随机 Nonce |
| `key_version` | int | 商户 DEK 版本 |
| `encryption_alg` | varchar(32) | `AES-256-GCM` |
| `encrypted` | tinyint | `0` 明文兼容,`1` 密文 |
迁移完成前保留现有 `content JSON`,规则如下:
- 新写入:`content=NULL`、`encrypted=1`,只保存密文。
- 旧数据:`content IS NOT NULL`、`encrypted=0`,按原逻辑读取。
- 解密成功后还原为原 `any` JSON 结构,不修改对客户端的协议。
- `head` 第一阶段不加密,但禁止在 `head` 中放置消息正文、联系方式等敏感明文。
`im_group_messages` 是群消息派生查询表,不得另存一份明文。实现时二选一:
1. 推荐:仅保存索引、状态和统计字段,通过 `topic + seqid` 关联 `im_messages` 解密。
2. 兼容:同步增加密文字段,但加密输入和密文必须与主消息表保持一致。
不允许 `im_messages` 已加密而 `im_group_messages.content` 仍保留明文。
### 5.6 加密和解密时机
#### 新消息
```text
校验消息
-> 提取审核文本
-> 内容审核
-> 分配seqid/组装AAD
-> 序列化content
-> 使用当前active DEK加密
-> 事务写入密文和消息元数据
-> 向在线订阅者广播原内存明文
```
#### 历史消息和离线同步
```text
从数据库读取密文
-> 按tenant_id + key_version获取DEK
-> 重建AAD
-> GCM验签并解密
-> JSON反序列化
-> 按原Tinode协议返回
```
任何验签失败均必须当作数据篡改或密钥错误,禁止返回部分内容或尝试忽略错误。
### 5.7 密钥获取和缓存
- IM Server 不得每发送一条消息都跨境请求 HotGo。
- IM Server 按 `tenant_id + key_version` 在内存中缓存已解密 DEK。
- 新消息使用当前 `active` 密钥,历史消息按记录中的 `key_version` 取密钥。
- 密钥变更发布 `message.encryption.key.updated` 配置事件,IM Server 按商户刷新。
- HotGo 短时不可用时继续使用内存密钥。
- IM Server 重启后如无法取得当前密钥,该商户禁止写入新消息,不得降级为明文落库。
### 5.8 密钥轮换
1. Admin Server 生成新 DEK,使用 KEK 加密后以 `pending` 状态入库。
2. 通知 IM Server 拉取并验证新密钥。
3. 验证成功后,在事务中将旧密钥设为 `retired`,新密钥设为 `active`。
4. 新消息立即使用新版本,旧消息仍按原版本解密。
5. 历史转密钥作为单独低速任务执行,不阻塞即时通信。
## 6. Admin Server 改造
### 6.1 内容审核页面
将现有“审核插件”配置补齐为可用功能:
- 阿里云基础配置。
- 真实连通性检查。
- 商户审核开关和场景开关。
- 风险等级与处置动作映射。
- 异常降级策略。
- 配置发布版本和生效时间。
非默认商户显示“未开启内容审核”,不显示阿里云密钥编辑项。
### 6.2 消息加密管理页面
增加平台级“消息加密”页面,只允许最高权限平台管理员使用:
- 查看商户加密状态、当前密钥版本和最后轮换时间。
- 初始化商户密钥。
- 启用/暂停商户消息加密。
- 轮换密钥。
- 查看历史迁移进度。
- 密钥明文、根密钥和 `encrypted_dek` 均不展示。
### 6.3 内部接口
建议增加:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/internal/im-config/tenants/{code}/moderation-policy` | IM Server 拉取商户审核策略 |
| GET | `/internal/im-config/moderation/provider` | IM Server 拉取阿里云配置 |
| GET | `/internal/im-config/tenants/{code}/message-keys/active` | 获取当前消息 DEK |
| GET | `/internal/im-config/tenants/{code}/message-keys/{version}` | 获取指定版本 DEK |
| POST | `/internal/im-config/tenants/{code}/message-keys/ack` | IM Server 确认新密钥可用 |
密钥接口返回的是服务端使用的 DEK,不允许从浏览器或通用 Admin API 调用。接口必须校验服务身份、时间窗、签名、请求 ID 和重放。
### 6.4 后台消息查询
- Admin Server 继续调用 IM Server internal admin-query,不直连 IM 业务库解密。
- IM Server 在商户数据权限通过后批量解密当页消息。
- 每次查看明文记录管理员 ID、商户、IP、查询条件、返回数量、时间和 TraceID。
- 列表只解密当页,禁止一次加载全部历史消息。
## 7. IM Server 改造
### 7.1 新增内部模块
```text
server/moderation/
provider.go 审核Provider接口
policy.go 商户策略和缓存
text.go 文本提取与结果映射
aliyun/text.go TextModerationPlus实现
server/messagecrypto/
cipher.go AES-256-GCM加解密
key_provider.go 密钥获取接口
key_cache.go tenant/version内存缓存
serializer.go content规范序列化
```
不将加密和阿里云调用直接写进 MySQL adapter;adapter 只保存已加密的存储模型和解密所需元数据。
### 7.2 消息发送接入点
在 `Topic.handlePubBroadcast` 中,于 `saveAndBroadcastMessage` 之前执行:
1. 过滤 WebRTC/系统信令等不审核内容。
2. 提取文本。
3. 根据商户策略执行审核。
4. 拦截时通过 Tinode ctrl 回复业务错误,不进入保存和广播。
5. 放行时进入加密保存。
进程内广播使用本次请求已有明文,不为广播执行“加密后立即解密”。
### 7.3 资料修改接入点
用户昵称/签名、群名称/公告、群昵称的 HTTP 和 Tinode meta 修改通道都必须接入同一 moderation service,不能只修改某一个 HTTP API 而留下协议绕过路径。
### 7.4 存储层改造
- `types.Message` 和 `types.MessageRevision` 增加密文及密钥元数据字段。
- MySQL 的普通库和共享库/多商户 adapter 必须同时改造。
- 新建库 schema、启动 ensure 迁移和手工 SQL 脚本必须保持一致。
- Save、GetOne、GetAll、编辑、撤回、删除、历史修订、管理后台查询均需覆盖。
- 解密层应在业务存储服务边界统一执行,避免上层遗漏。
### 7.5 缓存与多 Pod
- 每个 Pod 保持本地密钥和审核策略缓存。
- Kafka 配置事件使所有 Pod 同时失效。
- 事件处理必须幂等,重复消息不得导致重复创建密钥。
- 新旧密钥切换期间,所有 Pod 必须同时具备旧版本解密和新版本加密能力。
## 8. 审核和解密日志
在 IM 业务库增加 `im_content_moderation_logs`:
| 字段 | 说明 |
| --- | --- |
| `id` | 主键 |
| `request_id` | 审核请求 ID |
| `scene` | 昵称、单聊、群聊等 |
| `topic` / `seqid` | 关联消息,拦截时 `seqid` 可为空 |
| `actor_id` | 内容提交人 |
| `provider` | `local` / `aliyun_green` |
| `provider_request_id` | 第三方 RequestID |
| `risk_level` | 统一风险等级 |
| `risk_labels` | 命中标签 JSON |
| `action` | `allow` / `block` / `audit_only` |
| `content_hash` | SHA-256 摘要,不保存明文 |
| `latency_ms` | 审核耗时 |
| `error_code` | 脱敏错误码 |
| `created_at` | 创建时间 |
后台明文查看继续写入统一审计事件,必须区分“列表解密”、“详情解密”和“导出解密”。默认不提供明文批量导出;需要开放时必须增加独立权限和审批。
## 9. 关键词查询限制
消息加密后,MySQL 无法对密文执行原有 `LIKE '%keyword%'`。第一阶段采用:
1. 后台查询必须先提供商户、时间范围,并尽量提供用户或群号。
2. IM Server 先用明文元数据缩小候选集。
3. 对有上限的候选集批量解密,再在内存中匹配关键词。
4. 限制查询最大时间范围和候选消息数,超限返回明确提示。
不建议在第一阶段建设明文分词索引,否则会降低加密保护效果。若后续必须支持全库高性能检索,再单独评审 HMAC 盲索引方案。
## 10. 历史数据迁移
### 10.1 发布顺序
1. 发布 HotGo 表结构、根密钥和密钥管理能力。
2. 为默认商户初始化第 1 版 DEK,但不开启加密。
3. 发布 IM Server 双读兼容版本,验证密钥获取、加解密和内容审核。
4. 开启新消息加密写入,保持旧数据明文可读。
5. 启动历史数据分批迁移。
6. 对比数量、抽样解密和 GCM 验签,完成后清空明文 `content`。
7. 观察期结束后,再决定是否删除原明文字段。
### 10.2 迁移任务要求
- 按主键或 `topic + seqid` 分批,不长时锁表。
- 每批使用独立事务,记录游标、成功数、失败数和最后错误。
- 重复执行必须幂等,`encrypted=1` 的记录直接跳过。
- 先写入密文并验证可解密,再将明文字段置空。
- 失败记录保留明文并进入重试列表,不得丢失原数据。
- 迁移速率可配置,并监控数据库负载和消息延迟。
## 11. 错误码和客户端兼容
客户端不参与加解密,不修改消息协议。新增业务错误建议:
| 错误 | 说明 | 客户端处理 |
| --- | --- | --- |
| `CONTENT_REJECTED` | 内容审核拒绝 | 保留未发送文本并提示修改 |
| `CONTENT_REVIEW_UNAVAILABLE` | 审核不可用且策略要求拦截 | 提示稍后重试 |
| `MESSAGE_ENCRYPTION_UNAVAILABLE` | 未取得加密密钥 | 提示服务暂时不可用 |
| `MESSAGE_DECRYPT_FAILED` | 历史消息验签/解密失败 | 展示“消息暂时无法加载”,不展示乱码 |
不向客户端返回阿里云原始错误、风险标签详情、密钥版本内部信息或解密堆栈。
## 12. 监控指标
### 12.1 内容审核
- 按商户/场景统计请求量、放行数、拦截数、降级数。
- 阿里云耗时 P50/P95/P99。
- 超时率、错误率、熔断状态。
- 各风险等级和标签命中数。
### 12.2 消息加密
- 加密/解密成功数、失败数和耗时。
- 各商户当前密钥版本。
- 密钥获取成功率、缓存命中率和缓存最后刷新时间。
- 明文历史记录剩余数、迁移速率和迁移失败数。
- GCM 验签失败必须产生高优先级告警。
监控标签不得使用消息正文、密文、Nonce、DEK 或 KEK。
## 13. 验收标准
### 13.1 审核验收
- `default` 商户开启后,单聊、群聊、用户昵称、群名称和群昵称均实际调用对应阿里云 Service。
- 其他商户关闭时不调用阿里云。
- 拦截内容不进入 `im_messages` 和 `im_group_messages`,也不向订阅者广播。
- 本地敏感词与阿里云开关可独立启停。
- 阿里云超时严格按 `failure_mode` 处理。
- 配置发布后所有 IM Pod 在规定时间内生效。
### 13.2 加密验收
- 开启商户的新消息在数据库中不存在可读正文。
- 相同内容连续发送两次,Nonce 和密文必须不同。
- 修改密文、Nonce、AAD 关联字段的任意一处后,解密必须失败。
- 在线发送、离线同步、多端漫游、编辑、回复、撤回、删除保持原有业务行为。
- Admin 有权限账号可查看解密明文,无权限账号被拒绝。
- 查看明文必须形成完整审计记录。
- 旧密钥退役后仍可解密旧版本消息,新消息只使用新版本。
- HotGo 暂时断开时,已有内存密钥的 IM Pod 仍可工作;无密钥时绝不允许明文降级落库。
### 13.3 性能验收
- 纯 AES 加密不得成为消息链路主要瓶颈。
- 密钥缓存命中时不发生跨境 HotGo 请求。
- 审核关闭的商户不增加阿里云网络耗时。
- 审核开启时应分别统计本地处理、阿里云调用和消息入库耗时。
- 历史迁移期间即时消息发送和数据库负载不得超过上线阈值。
## 14. 开发分批建议
### 批次一:配置和密钥基础
- Admin 阿里云内容审核配置、商户策略、密钥表和密钥管理页。
- 字典根密钥隔离和专用密钥服务。
- Admin internal config/key API 及 Kafka 失效事件。
### 批次二:IM 文本审核
- 阿里云 `TextModerationPlus` Go 客户端。
- 策略缓存、文本提取、消息和资料修改接入。
- 审核日志、错误码、告警和回归测试。
### 批次三:新消息加密
- AES-256-GCM 模块、密钥缓存和存储模型。
- 新消息加密写入和历史消息解密读取。
- 单聊、群聊、修订、后台查询的一致性验证。
### 批次四:历史迁移和轮换
- 历史明文分批加密。
- 迁移仪表盘、少量失败重试和数据对账。
- 密钥轮换和历史转密钥测试。
- 完成观察后清理明文。
## 15. 开发前必须确认的决策
| 决策 | 文档默认值 |
| --- | --- |
| 阿里云异常时放行还是拦截 | 放行并告警 |
| 是否审核系统消息 | 不审核 |
| 是否加密 `head` | 第一阶段不加密 |
| 第一阶段启用商户 | 仅 `code=default` |
| 是否允许无密钥时明文降级 | 不允许 |
| 后台是否允许明文导出 | 默认不允许 |
| 历史消息是否迁移 | 是,分批迁移 |
| 根密钥位置 | HotGo 内部字典数据,严禁通用接口暴露 |
## 16. 现有实现对照
| 能力 | 现状 | 本次改造 |
| --- | --- | --- |
| 阿里云文本审核 | 文档已人工验证,IM Server 无 Go 调用实现 | 增加 TextModerationPlus 客户端和业务接入 |
| 本地敏感词 | Admin 已有 CRUD,IM Server 未消费 | 作为可选补充审核 |
| 审核配置 | `im_external_config` 已有 `moderation` 占位 | 增加真实检查、商户策略和发布 |
| 配置同步 | 已有 internal config 客户端、Kafka 配置事件和缓存失效 | 复用并扩展新的策略/密钥类型 |
| 消息存储 | `im_messages.content` 和修订表 `content` 为 JSON 明文 | 改为 AES-256-GCM 密文且双读迁移 |
| 群消息派生表 | `im_group_messages.content` 为 JSON 明文 | 去明文副本或同步密文化 |
| 后台消息查询 | IM internal-query 返回库内内容 | 增加权限校验、解密和查看审计 |