无需在 Bird 中配置任何权限模型。私有或在线状态订阅由您编写的端点审批,使用您已有的会话,并用只有您的服务器持有的密钥进行签名。Bird 验证签名;策略由您决定。
// Your endpoint. The only place the app secret lives.
app.post("/bird/auth", async (req, res) => {
const { connection_id, channel_name } = req.body;
const user = await session(req);
if (!mayJoin(user, channel_name)) return res.sendStatus(403);
// Signs <connection_id>:<channel_name>[:<member_data>] with the app
// secret, and adds shared_secret on an encrypted channel.
res.json(
await bird.realtime.authorizeChannel({
connectionId: connection_id,
channelName: channel_name,
memberData: JSON.stringify({
member_id: user.id,
member_info: { name: user.name },
}),
}),
);
});
一个密钥是公开的,另一个不是。
一切源于这个区分。
Bird Realtime API 上的每个应用都有一个密钥和一个密钥密文。密钥用于嵌入客户端代码;密钥密文用于验证服务器调用和签名订阅,且仅在创建时显示一次。任何持有它的人都可以向您的应用发布消息并伪造在线身份,因此请像对待数据库密码一样妥善保管。密钥轮换是叠加式而非中断式的:创建第二个密钥,部署后再撤销旧密钥。
四项控制,四个问题
谁可以订阅,谁可以保持连接,谁可以读取消息负载,谁仍然被允许访问。
- 01
签名订阅。
客户端将其连接 ID 和通道名称发送到您的端点。您验证调用者身份,如果不允许加入则返回 403 拒绝,或返回对连接 ID 和通道名称的 HMAC-SHA256 签名(以应用密钥为前缀)。由于连接 ID 包含在签名中,一次授权仅覆盖一个连接,无法在另一个连接上重放。在线状态通道还会签名成员身份,且必须是您返回的完全一致的字符串:对同一对象重新序列化可能会重新排列键名并使签名失效。
- 02
授权连接。
应用密钥是公开的,因此任何能加载您页面的人都可以用它打开一个 WebSocket 连接。开启授权连接后,每个新连接有 30 秒时间证明持有密钥密文的一方为其担保——通过私有订阅或登录。未完成验证的连接将以代码 4009 关闭,且未授权连接不计入您的配额。订阅公共通道不能证明任何身份,也不会授权连接。
- 03
端到端加密。
private-encrypted- 通道在请求离开您的进程之前由服务器密封,使用一个永远不会出现在 Realtime API 请求中的 32 字节主密钥。每个通道派生自己的密钥,因此授权客户端访问一个加密通道并不意味着它能读取另一个。Bird 无法恢复丢失的密钥,密钥轮换保护的是未来的消息负载而非过去的。
- 04
立即撤销访问权限。
用户登出、修改密码、账户被封禁:断开该成员的连接,该身份持有的所有连接——在所有设备上——都将关闭。客户端将此关闭视为终态而不会重试,且您的端点停止为其签名,因此他们无法再次连接。
加密通道
Bird 无法读取的消息负载,运行在 Bird 管理的基础设施上。
服务器 SDK 识别通道前缀,从您的主密钥派生该通道的密钥,并在本地密封消息负载。边缘节点转发密文,您的授权端点仅将派生密钥分发给已批准的客户端。设计时需考虑两个限制:通道名称和事件名称以明文传输,因此选择不会泄露所保护内容的名称;加密不能与在线状态或客户端事件组合使用。缓存可以这样实现:private-encrypted-cache- 通道以密封状态存储其缓存事件。
const bird = new BirdClient({
apiKey: process.env.BIRD_API_KEY!,
realtime: {
key: process.env.BIRD_REALTIME_KEY,
secret: process.env.BIRD_REALTIME_SECRET,
// 32 random bytes, yours alone. Never sent to Bird.
encryptionMasterKey: process.env.BIRD_REALTIME_MASTER_KEY,
},
});
// Sealed in your process. The edge forwards ciphertext.
await bird.realtime.publish(APP_ID, {
event: "order.updated",
channels: ["private-encrypted-orders"],
data: { order_id: "ord_123", status: "shipped" },
});
授权不能做什么。
要求授权连接控制的是谁可以保持 WebSocket 连接打开,而不是谁可以读取某个通道:公共通道对每个已授权连接仍然可读,因此属于单个客户的事件应放在私有通道中,由您的端点检查通道名称。由于您的端点是权威来源,一个过于宽松的端点和泄露的密钥一样会大量分发访问权限。客户端事件同理——边缘节点不做校验:仅将其用于信号,任何需要权威性的内容都应通过您的服务器路由。
数据存储位置。
应用在创建时选择其所在区域,且终生不变,因此请选择离您用户最近的区域;应用的区域可以与您工作区的主区域不同。应用也是隔离边界:两个应用永远看不到彼此的通道,这正是每个环境使用一个应用、让测试流量远离生产环境的最佳方式。
付诸实践。
继续查阅此主题的文档、指南和示例。资源为英文。