diaspora* 0.9.1.0 联邦与 REST API 集成指南
diaspora* 0.9.1.0 进阶实测:WebFinger/hCard/Salmon 联邦三链路与双 pod TLS 端到端中继闭环实录,OAuth2 scope 白名单、REST API 端点与 OIDC 集成全链路,附七条实测踩坑。
diaspora* 0.9.1.0 联邦与 REST API 集成指南
本文是 diaspora* 专栏的进阶篇,基于 0.9.1.0 双 pod 联邦端到端实测与 REST API 全链路实测。前置认知见 《diaspora* 介绍与选型指南》,单 pod 部署见 《diaspora* Docker Compose 部署指南》,日常维护见 《diaspora* 日常运维手册》。文中命令与回显均为 2026-11-02 逐字实录;pod 域名以
pod.example.com/pod-b.example.com占位,示例管理员账号admin@pod.example.com/q1w2e3r4。
一、环境信息
1.1 双 pod 实测拓扑
| 项 | pod A | pod B |
|---|---|---|
| compose 项目 | diaspora-0.9.1.0-stack | diaspora-b-0.9.1.0 |
| 镜像 | diaspora-app:0.9.1.0(同一自建镜像) | 同左 |
| URL | https://pod.example.com/(宿主 8143:443) | https://pod-b.example.com/(宿主 8144:443) |
| 数据库 | diaspora_production | diaspora_b_production(DATABASE_NAME 区分) |
| 网络 | diaspora-0910-stack_diaspora(subnet 10.89.68.0/24) | external 加入 pod A 网络,别名 pod-b.example.com |
两 pod 各自独立 web/sidekiq/db/redis 四容器;通过 compose 网络别名让容器内 DNS 直接解析对方域名,模拟真实跨域名联邦。
1.2 联邦栈版本
联邦层由 diaspora_federation gem 1.2.0(+ diaspora_federation-rails 引擎)实现,源码 Gemfile.lock 可查;nodeinfo 实测 protocols: ["diaspora"]——diaspora* 走自有协议,不与 ActivityPub 直接互通(选型背景见认知篇)。
二、联邦协议原理
2.1 发现链(Discovery)
远程实体定位固定三步,全部走 HTTPS:
- WebFinger:
GET https://<pod域名>/.well-known/webfinger?resource=acct:<user>@<pod域名>→ 返回 hcard/salmon/seed/profile-page/atom 链接集合(实测 200,链接为 pod 绝对 URL); - hCard:
GET /hcard/users/:guid→ 公开档案 + RSA 公钥(验签基础); - 实体抓取:
GET /fetch/:type/:guid→ 返回 Salmon MagicEnvelope(base64url 内嵌签名 XML)。
2.2 投递链(Delivery)
- 推送:发送侧由 sidekiq 作业(
Workers::SendPrivate、Workers::Public等)把签名 XML POST 到接收侧POST /receive/public或POST /receive/users/:guid; - 补拉:接收侧遇到未知实体时按 2.1 第 3 步回源抓取;
- 订阅:
GET /public/:username.atom提供公开 Atom 流。
联邦引擎路由逐字(diaspora_federation-rails 1.2.0 config/routes.rb):
POST /receive/public
POST /receive/users/:guid
GET /fetch/:type/:guid
GET /.well-known/webfinger
GET /hcard/users/:guid
2.3 TLS 是联邦前提(源码实锤)
diaspora_federation 1.2.0 discovery.rb:webfinger 永远先请求 https://<domain>/.well-known/webfinger,仅当 DiasporaFederation.webfinger_http_fallback 为 true 才回退 http;而应用 initializer 写死:
config.webfinger_http_fallback = Rails.env == "development"
结论:生产 http-only 的 pod 之间无法联邦(实测 pod B 解析 pod A 后 443 拒连 → DiscoveryError)。官方文档同样要求 TLS。
三、双 pod 联邦环境搭建
3.1 证书(自签 CA,实测命令逐字)
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout ca.key -out ca.crt -subj "/CN=S8 Diaspora Test CA"
for h in pod.example.com pod-b.example.com; do
openssl req -newkey rsa:2048 -nodes -keyout $h.key -out $h.csr -subj "/CN=$h"
printf "subjectAltName=DNS:%s\n" $h > san.ext
openssl x509 -req -in $h.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 825 -sha256 -extfile san.ext -out $h.crt
done
# 联邦出站信任库 = 测试CA + 系统CA(两 pod 共用)
cat ca.crt /etc/ssl/certs/ca-certificates.crt > ca-combined.pem
3.2 pod A 切换 https
compose 环境变量(puma 原生 ssl:// bind,无需 nginx):
ENVIRONMENT_URL: "https://pod.example.com/"
ENVIRONMENT_REQUIRE_SSL: "true"
SERVER_LISTEN: "ssl://0.0.0.0:443?key=/diaspora/config/certs/pod.example.com.key&cert=/diaspora/config/certs/pod.example.com.crt"
ENVIRONMENT_CERTIFICATE_AUTHORITIES: "/diaspora/config/certs/ca-combined.pem"
端口 8143:443,挂载 ./data/certs:/diaspora/config/certs:ro。web 启动日志实测:
Rack::SSL is enabled
* Listening on ssl://0.0.0.0:443?key=/diaspora/config/certs/pod.example.com.key&cert=...
官方口径:http→https 改协议无后果;pod 域名上线后不可更改(它固化在每个 diaspora_id 与已签名实体里)。
3.3 pod B(docker-compose-b.yml 关键差异)
同镜像、独立 db-b/redis-b/数据目录,与 pod A 的四处差异:
name: diaspora-b-0.9.1.0
services:
web:
ports: ["8144:443"]
environment:
ENVIRONMENT_URL: "https://pod-b.example.com/"
DATABASE_NAME: diaspora_b_production
SERVER_LISTEN: "ssl://0.0.0.0:443?key=/diaspora/config/certs/pod-b.example.com.key&cert=/diaspora/config/certs/pod-b.example.com.crt"
# ENVIRONMENT_CERTIFICATE_AUTHORITIES 同 pod A
networks:
diaspora:
aliases: [pod-b.example.com]
networks:
diaspora:
external: true
name: diaspora-0910-stack_diaspora # 复用 pod A 网络
sidekiq/db/redis 服务同理(sidekiq 必须同样挂 certs 卷,见 6.1)。pod B 注册 admin@pod-b.example.com 并 Role.add_admin 提权(流程与 pod A 完全一致,见部署篇 4.8)。
四、联邦实测记录
4.1 跨 pod 用户发现
在 pod B 容器内按 diaspora_id 抓取 pod A 用户:
$ docker compose -f docker-compose-b.yml exec -T web bin/rails runner /dev/stdin <<'RUBY'
p = Person.find_or_fetch_by_identifier("admin@pod.example.com")
puts "FETCHED handle=#{p.diaspora_handle} guid=#{p.guid} pod=#{p.pod.slice(:host,:ssl,:status)} public_key?=#{!p.public_key.blank?}"
RUBY
FETCHED handle=admin@pod.example.com guid=2ab71dd0b885013fc1726ec7298bc224 pod={"host"=>"pod.example.com","ssl"=>true,"status"=>"unchecked"} public_key?=true
链路走 https://pod.example.com/.well-known/webfinger → hcard → 实体抓取,别名 DNS 命中 pod A 容器 443,TLS 以 ca-combined.pem 校验通过。
4.2 pod B 关注 pod A(Contact 模型坑)
必须用 User#share_with:Contact 模型没有 aspect/aspect_id 字段,面向关系走 AspectMembership(has_many :aspects, through: :aspect_memberships),直接给 Contact 塞 aspect_id 会报错:
u = User.first
person = Person.find_or_fetch_by_identifier("admin@pod.example.com")
aspect = u.aspects.first
u.share_with(person, aspect)
pod B 侧结果:sharing=false receiving=true(我发起了分享,对方尚未回关)。share_with 触发 pod B sidekiq 的 SendPrivate/SendContact 作业推送 pod A。
4.3 pod A 自动接受回关
PODA sees remote="admin@pod-b.example.com" guid=d95abc90b887013ff28672cd1b5135e7
PODA contact sharing=true receiving=false
pod A 默认 interactions.following=accept(自动接受关注),于是 pod A 侧 sharing=true——双向通道打通。
4.4 帖子中继闭环(核心成果)
pod A 用 REST API 发公开帖:
{"body": "Federation relay test from pod A", "public": true}
→ 201,guid=617b4990b889013fe328121320dd5457。8 秒后在 pod B 查库:
PODB RECEIVED: guid=617b4990b889013fe328121320dd5457 text=Federation relay test from pod A author=admin@pod.example.com
跨 pod Salmon 推送闭环成立:pod A sidekiq 出站投递 → pod B /receive/... 入站 → 验签入库,guid 全链一致。
4.5 实体抓取端点(type 命名坑)
GET /fetch/StatusMessage/<guid> → 500 InvalidEntityName
GET /fetch/status_message/<guid> → 200 Salmon MagicEnvelope XML
/fetch/:type/:guid 的 type 必须是 snake_case 实体名(ENTITY_NAME_REGEX 校验),返回体是 base64url 内嵌签名 XML 的 MagicEnvelope。
五、REST API 集成
5.1 认证模型链
0.9.0.0 起 API 为官方支持面(文档:https://diaspora.github.io/api-documentation/)。源码模型链:
OAuthApplication(client_id/secret)
└─ Authorization(scopes 白名单校验,绑定 user+application)
└─ OAuthAccessToken(token = SecureRandom.hex(32),expires_at = 24h)
Authorization::SCOPES 白名单逐字(app/models/api/openid_connect/authorization.rb):
contacts:modify contacts:read conversations email interactions
name nickname notifications openid picture
private:modify private:read profile profile:modify profile:read_private
public:modify public:read sub tags:modify tags:read
请求以 Authorization: Bearer <token> 携带;控制器按端点声明所需 scope(require_access_token %w[...]),不足返回 403 insufficient_scope,token 无效返回 401。
5.2 实测造 token(rails runner 脚本)
生产应走 OAuth 授权流程(用户在 /api/applications 注册应用 + 授权码换 token);服务端自动化测试可直接建链:
app = OAuthApplication.create!(name: "s8-test", redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
scopes: Api::OpenidConnect::Authorization::SCOPES)
u = User.first
auth = Authorization.create!(user: u, o_auth_application: app,
scopes: Api::OpenidConnect::Authorization::SCOPES)
tok = auth.o_auth_access_tokens.create!
puts "CLIENT_ID=#{app.uid} TOKEN=#{tok.token}"
实测输出:CLIENT_ID=83997982f890b31eaad80342f51f54fe 同型 32 位 hex;TOKEN 为 64 位 hex(b79084e5e1638c3f...),expires_at 24 小时。
5.3 端点实测(pod A,https://localhost:8143)
| 请求 | scope 要求 | 实测 |
|---|---|---|
GET /api/v1/streams/main?limit=1 | contacts:read + private:read | 200,JSON 数组含本地与远程帖 |
GET /api/v1/posts/:guid | public:read | 200,帖子 JSON(含 author diaspora_id) |
POST /api/v1/posts | public:modify | 201,guid=02f39260b887013fc1726ec7298bc224 |
GET /api/v1/user | profile | 403 insufficient_scope(见 5.4) |
| 任意端点 + 错误 token | — | 401 |
发帖 payload 与网页表单不同构,这是集成最常见的坑:
curl -sk -X POST https://localhost:8143/api/v1/posts \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"body": "Hello from API", "public": true}'
控制器 normalized_create_params 把 body 映射为 status_message.text,public 必填(params.require(:public)),可选 aspects/location/poll/photos(photo 需先经上传接口且属当前用户 pending 状态)。
5.4 scope 粒度陷阱
GET /api/v1/user(当前用户档案)要求 profile scope(users_controller.rb)——它不在「public/private 读写」常见组合里,漏配时 403 且错误体明确 insufficient_scope。集成时按最小权限申请:只读公开流给 public:read,机器人发帖给 public:modify,需要档案再加 profile。
5.5 OIDC 发现端点
GET /.well-known/openid-configuration → 200,含 authorization_endpoint / token_endpoint / jwks_uri 等,第三方应用可走标准 OIDC 登录。注意:0.9.1.0 正是修复 OIDC 实现中恶意客户端注册触发 pod 内网请求(SSRF)漏洞的安全版本,低于 0.9.1.0 的 pod 接入第三方 OIDC 应用前应先升级。
六、集成踩坑与建议(实测排障实录)
- sidekiq 忘挂 certs 卷 → 出站联邦全停:pod A 切 https 后 sidekiq 因
ENVIRONMENT_CERTIFICATE_AUTHORITIES指向不存在的路径直接FATAL崩溃循环,web 一切正常、帖子「发出去了」但远端永远收不到——积压作业滞留 Redis。补- ./data/certs:/diaspora/config/certs:ro重启后积压立刻消费。排查口诀:入站看 web,出站看 sidekiq。 - http-only 无法联邦:见 2.3,别在测试环境浪费时间,直接上 TLS。
- API 与网页表单参数不同构:网页
status_message[text]嵌套 vs APIbody扁平,混用会得到 422Failed to create the post。 - token 24h 过期:长驻集成用 refresh_token 机制或定期重建授权;测试脚本造 token 时记住有效期。
- 域名不可迁移:联邦身份(diaspora_id、签名实体、hcard URL)全部固化域名,pod 上线前确定域名。
- rails runner 多语句经
docker compose exec拆词:报Please specify a valid ruby command;用/dev/stdin+ heredoc 喂脚本文件(本文 4.1/5.2 均此法)。 /fetch实体名 snake_case:StatusMessage会 500InvalidEntityName。
七、参考链接
- 《diaspora* 介绍与选型指南》
- 《diaspora* Docker Compose 部署指南》
- 《diaspora* 日常运维手册》
- 官方 API 文档:https://diaspora.github.io/api-documentation/
- 联邦协议库(协议实现与 discovery 逻辑):https://github.com/diaspora/diaspora_federation
- 源码仓库(routes.rb / authorization.rb / diaspora_federation.rb 等本文引用处):https://github.com/diaspora/diaspora
- Changelog 0.9.0.0(API 官方支持公告):https://github.com/diaspora/diaspora/blob/main/Changelog.md
- 官方 wiki:https://wiki.diasporafoundation.org/