23 KiB
服务端消息加密与内容审核改造文档
文档状态:待开发
编制日期:2026-09-13
适用项目:im_manager(Admin Server)、im_server(IM Server)
本文档是后续数据库设计、后端开发、联调、迁移和验收的共同依据。
1. 建设目标
本次改造同时建设两项能力:
- 内容审核:IM Server 在保存或修改内容前,根据商户策略执行本地敏感词检查和阿里云文本内容审核。
- 消息加密存储:审核通过的消息在 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. 总体架构
客户端
| 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 结构固定为:
{
"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 字节认证标签。
- 明文编码:将原
contentJSON 规范化序列化为 UTF-8 字节。 - 不允许固定 Nonce,不允许 AES-ECB,不使用未认证的 AES-CBC/CTR。
5.2 AAD
每条消息加密时绑定以下 AAD:
tenant_id | topic | seqid | from | client_msg_id | key_version | content
content 是字面标识,用于与未来可能加密的 head 区分。AAD 按固定字段顺序和长度前缀编码,禁止使用可产生拼接歧义的简单字符串。
5.3 密钥层级
HotGo字典根密钥(KEK)
-> 加密每个商户、每个版本的DEK
-> DEK加密该商户消息
根密钥使用 HotGo hg_sys_dict_data 保存,但必须满足:
- 使用独立的内部字典类型,不出现在通用字典管理页。
- 后端字典列表、选项和导出接口必须对该类型做服务端拒绝,不能仅依赖菜单隐藏。
- 根密钥为 32 字节强随机值的 Base64 编码,不使用口令、UUID 或可读文本。
- 根密钥只由 Admin Server 密钥服务读取,不通过通用配置接口返回。
- 根密钥不写日志,不在后台页面回显,不进入 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,按原逻辑读取。 - 解密成功后还原为原
anyJSON 结构,不修改对客户端的协议。 head第一阶段不加密,但禁止在head中放置消息正文、联系方式等敏感明文。
im_group_messages 是群消息派生查询表,不得另存一份明文。实现时二选一:
- 推荐:仅保存索引、状态和统计字段,通过
topic + seqid关联im_messages解密。 - 兼容:同步增加密文字段,但加密输入和密文必须与主消息表保持一致。
不允许 im_messages 已加密而 im_group_messages.content 仍保留明文。
5.6 加密和解密时机
新消息
校验消息
-> 提取审核文本
-> 内容审核
-> 分配seqid/组装AAD
-> 序列化content
-> 使用当前active DEK加密
-> 事务写入密文和消息元数据
-> 向在线订阅者广播原内存明文
历史消息和离线同步
从数据库读取密文
-> 按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 密钥轮换
- Admin Server 生成新 DEK,使用 KEK 加密后以
pending状态入库。 - 通知 IM Server 拉取并验证新密钥。
- 验证成功后,在事务中将旧密钥设为
retired,新密钥设为active。 - 新消息立即使用新版本,旧消息仍按原版本解密。
- 历史转密钥作为单独低速任务执行,不阻塞即时通信。
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 新增内部模块
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 之前执行:
- 过滤 WebRTC/系统信令等不审核内容。
- 提取文本。
- 根据商户策略执行审核。
- 拦截时通过 Tinode ctrl 回复业务错误,不进入保存和广播。
- 放行时进入加密保存。
进程内广播使用本次请求已有明文,不为广播执行“加密后立即解密”。
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%'。第一阶段采用:
- 后台查询必须先提供商户、时间范围,并尽量提供用户或群号。
- IM Server 先用明文元数据缩小候选集。
- 对有上限的候选集批量解密,再在内存中匹配关键词。
- 限制查询最大时间范围和候选消息数,超限返回明确提示。
不建议在第一阶段建设明文分词索引,否则会降低加密保护效果。若后续必须支持全库高性能检索,再单独评审 HMAC 盲索引方案。
10. 历史数据迁移
10.1 发布顺序
- 发布 HotGo 表结构、根密钥和密钥管理能力。
- 为默认商户初始化第 1 版 DEK,但不开启加密。
- 发布 IM Server 双读兼容版本,验证密钥获取、加解密和内容审核。
- 开启新消息加密写入,保持旧数据明文可读。
- 启动历史数据分批迁移。
- 对比数量、抽样解密和 GCM 验签,完成后清空明文
content。 - 观察期结束后,再决定是否删除原明文字段。
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 文本审核
- 阿里云
TextModerationPlusGo 客户端。 - 策略缓存、文本提取、消息和资料修改接入。
- 审核日志、错误码、告警和回归测试。
批次三:新消息加密
- 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 返回库内内容 | 增加权限校验、解密和查看审计 |