第 9 章:安全、权限与内容治理
一、安全是多层防线,不是一个 before_action
Mastodon 同时面对三类主体:本地用户、第三方 OAuth client、远端 ActivityPub server。安全检查必须分布在不同边界:
HTTP middleware / OAuth
→ Controller authentication + scope
→ Policy / current account authorization
→ Service invariant
→ Serializer field visibility
→ ActivityPub signature / audience
→ Worker re-check at execution time
每层解决的问题不同,不能用其中一层替代全部。
二、OAuth 与 access token
第三方客户端使用 OAuth application 和 access token 调用 REST/Streaming。token 不只是“登录成功证明”,还包含 scope 和 resource owner。读 API 时要检查:
- endpoint 要求哪些 scope;
- token 是否过期/撤销;
- current user 与 current account 如何映射;
- streaming 是否使用同一套 scope 语义;
- serializer 是否根据应用/用户隐藏字段。
发帖、修改账户、读取私密通知、管理操作都不应仅凭“用户已经登录”判断。
三、Policy 与 service invariant 的区别
Policy
回答“这个主体对这个对象能否做某动作”。例如当前账户能否删除 status、读取 private status、接受 follow request。
Service invariant
回答“无论调用方是谁,这个用例本身必须满足什么条件”。例如媒体必须属于账户且 ready、引用私密状态必须满足 mention 条件、状态编辑不能跨越允许窗口。
Controller policy:挡住普通用户的非法入口
Service invariant:防止内部调用/异步重放绕过规则
Database constraint:防止并发和数据层破坏不变量
四、可见性矩阵
| visibility | 作者 | followers | 被提及者 | 公共流 | 远端联邦 |
|---|---|---|---|---|---|
| public | 是 | 是 | 是 | 通常是 | 按 audience |
| unlisted | 是 | 是 | 是 | 详情/用户流,避免公共推荐 | 按 audience |
| private | 是 | 是 | 相关关系 | 否 | followers/inbox |
| direct | 是 | 指定收件人 | 指定收件人 | 否 | 指定 inbox |
| limited | 受限 | 按关系 | 按规则 | 否 | 按协议语义 |
具体实现会叠加 block、mute、silence、suspend、domain block、quote policy、list visibility 和保护账户状态。表格是心智模型,源码中的 policy/filter 才是最终事实。
五、ActivityPub 安全
远端请求要经过多项防护:
- HTTP Signature / Linked Data Signature 验证;
- actor 与 key 的关联;
- content type 和 JSON-LD context;
- canonical URI 和来源域名;
- 远端 dereference 的 SSRF/内网防护;
- authorized actor 与对象 owner 检查;
- 防止同一 activity 被重复应用;
- 防止恶意超大 payload、深层 JSON 或无界媒体抓取。
签名验证证明“请求由持有私钥的一方发出”,不等于“活动内容一定有权限在本地执行”。授权和内容规则仍需独立检查。
六、反垃圾与内容安全
Antispam、敏感标记、内容警告、关键词过滤、域名阻断、silence/suspend 是不同机制:
- 反垃圾决定是否允许写入或静默丢弃;
- 内容警告决定 UI 展示方式;
- phrase filter 决定 viewer 是否隐藏;
- domain block 决定实例级联邦与可见性;
- silence/suspend 决定账户传播与登录/互动能力。
“用户看不到”可能是作者权限、viewer filter、实例 moderation 或联邦失败,排障时要区分。
七、速率限制、锁和幂等
高风险/高成本操作需要不同的保护:
- 登录、搜索、API 写入的 rate limit;
Lockable防止同一账户并发发帖/编辑冲突;- Redis mutex 防止重复重建或重复 delivery;
- idempotency key 防止客户端重试创建重复状态;
- Sidekiq unique/业务唯一约束防止通知重复。
分布式锁必须有过期时间和失败处理;锁不是数据一致性的替代品,真正的不变量还需要事务和约束。
八、Worker 为什么必须重新检查权限
用户点击删除后,job 可能几秒后执行;期间账户可能被 suspend、status 可能被修改、远端对象可能已经删除。worker 不应假设 enqueue 时的状态永久有效。
def perform(status_id)
status = Status.find_by(id: status_id)
return if status.nil? || status.destroyed?
# 基于当前状态决定是否继续
end
实际实现会更复杂,但原则一致:payload 传 ID,执行时重新读取并判断。
九、管理后台是高权限域
管理 controller、policy、navigation visibility、审计日志和后台 worker 应一起读。隐藏 Sidekiq/管理菜单只是 UI 优化,不是授权;真正权限要落在 policy、controller authorization 和管理 action 级别。
十、常见安全误区
误区 1:前端不显示按钮就安全
攻击者可以直接调用 REST/ActivityPub 入口。后端必须拒绝。
误区 2:远端签名通过就可信
签名只证明密钥控制,payload 仍需 schema、授权、对象和域名验证。
误区 3:数据库查询到对象就能返回
所有 viewer-dependent API 都必须经过 policy/filter/serializer。
误区 4:异步 job 不需要再检查
队列延迟改变了执行时世界,worker 必须面向当前事实。
十一、源码入口清单
app/policiesapp/lib/rate_limiter.rbapp/lib/activitypub/linked_data_signature.rbapp/lib/activitypub/dereferencer.rbapp/lib/antispam.rbapp/services/delete_account_service.rbapp/models/concerns/account/suspensions.rb
十二、小结
Mastodon 的安全设计是“协议、身份、领域、投影、异步”多边界共同守护。下一章把所有结论落回部署与测试:如何启动这些进程、验证变更、观察失败并安全扩展。