// 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
授权连接。
应用密钥是公开的,因此任何能加载你页面的人都可以用它打开一个 socket。启用授权连接后,每个新连接有 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" },
});
授权不能做什么。
要求授权连接控制的是谁可以保持 socket 打开,而不是谁可以读取频道:公共频道对每个授权连接都可读,因此属于单个客户的事件应放在你的端点检查名称的私有频道上。由于你的端点是权威来源,一个过于宽松的端点会像泄露密钥一样随意分发访问权限。同样的原则适用于客户端事件,边缘节点不会验证它们:将它们用于信号,将任何权威操作通过你的服务器路由。
数据存储位置。
应用在创建时选择其区域,且终生不变,因此请选择离你的用户最近的区域,应用的区域可以与工作区的主区域不同。应用也是隔离边界:两个应用永远看不到彼此的频道,这就是为什么每个环境一个应用是将测试流量隔离在生产之外的正确方式。
规模扩展
不失控。
在工作区中组织团队,控制 API 访问权限,通过审计日志追踪变更。
审计日志
生产环境- 工作区
- 生产环境
- 资源
- 发送代理
- 读取权限读写