频道授权
任何持有 app key 的客户端都可以订阅公共频道。有两个频道名前缀要求你的后端对订阅进行授权。你无需单独配置频道。
名为 private-… 的频道要求你的后端批准每次订阅。名为 presence-… 的频道同样如此,并且还会为订阅者附加身份标识,使频道上的所有人都能看到其他成员。其他名称的频道均为公共频道。
只有你的后端持有 app secret。客户端请求你的服务器为特定订阅签名,Realtime 边缘节点在接受订阅前验证该签名。你的服务器在不向客户端暴露 secret 的情况下决定调用方是否可以订阅。
将客户端指向你的端点
在你自己的后端为客户端提供一个 authEndpoint:
import { BirdRealtime } from "@messagebird/realtime";
const bird = new BirdRealtime({
appKey: "your-app-key",
region: "us1",
authEndpoint: "/bird/auth",
});
const room = bird.subscribe("presence-room-1");import BirdRealtime
let bird = BirdRealtime(options: .init(
appKey: "your-app-key",
region: "us1",
authEndpoint: URL(string: "https://your-backend.example.com/bird/auth")
))
let room = bird.subscribe("presence-room-1")import com.bird.realtime.BirdRealtime
import com.bird.realtime.BirdRealtimeOptions
val bird = BirdRealtime(
BirdRealtimeOptions(
appKey = "your-app-key",
region = "us1",
authEndpoint = "https://your-backend.example.com/bird/auth",
)
)
val room = bird.subscribe("presence-room-1")客户端会在每次 private 或 presence 订阅时调用此端点,包括重连后恢复的订阅。授权仅适用于单个连接,因为签名中包含了该连接的 connection ID。
浏览器客户端默认要求同源端点。设置 allowCrossOriginAuth: true 可使用跨域 authEndpoint。浏览器客户端仅向同源端点发送已配置的 authHeaders。
端点的接收与返回
客户端 POST JSON:
代码示例
{ "connection_id": "26896.319537", "channel_name": "presence-room-1" }返回签名:
代码示例
{ "auth": "your-app-key:8f9a…" }对于 presence 频道,还需以 JSON 字符串形式返回成员身份标识,即你签名的同一个字符串:
代码示例
{
"auth": "your-app-key:8f9a…",
"member_data": "{\"member_id\":\"u_42\",\"member_info\":{\"name\":\"Ada\"}}"
}member_id 是其他成员看到的身份标识,也是断开连接操作所针对的值。member_info 是可选的 JSON 数据,会分发给频道中的每位成员。它有 1 KB 的大小限制,因此只应包含少量、非敏感的个人资料数据。
在此端点中使用调用方的 session cookie 或 bearer token 对其进行授权。当调用方不得加入频道时返回 403 Forbidden。对于 presence 频道,在同一响应中分配身份标识。
签名字符串
用冒号拼接,然后使用 app secret 进行 HMAC-SHA256 运算并十六进制编码。在结果前加上 app key 和一个冒号作为前缀。
| 频道类型 | 签名字符串 |
|---|---|
| private-… | <connection_id>:<channel_name> |
| private-encrypted-… | <connection_id>:<channel_name> |
| presence-… | <connection_id>:<channel_name>:<member_data> |
对于 presence 频道,签名时使用你返回的完全相同的 member_data 字符串。重新序列化同一对象可能改变键顺序或空格,导致签名失效。
加密频道的签名方式与 private 频道相同,其授权响应还会以 shared_secret 返回频道的解密密钥。SDK 辅助方法会自动添加该密钥;加密频道详细介绍了密钥派生和频道行为。
每个服务端 SDK 都提供一个 authorizeChannel 辅助方法。它使用已配置的 app 凭据进行签名并返回响应体,无需发起网络请求。对于加密频道,该辅助方法还会添加 shared_secret。
app.post("/bird/auth", async (req, res) => {
const { connection_id, channel_name } = req.body;
// Your own authorization decision goes here.
const user = getUserFromSession(req);
if (!user || !mayJoin(user, channel_name)) return res.sendStatus(403);
const memberData = channel_name.startsWith("presence-")
? JSON.stringify({ member_id: user.id, member_info: { name: user.name } })
: undefined;
res.json(
await bird.realtime.authorizeChannel({
connectionId: connection_id,
channelName: channel_name,
memberData,
}),
);
});import json
@app.post("/bird/auth")
def bird_auth():
body = request.get_json()
channel_name = body["channel_name"]
# Your own authorization decision goes here.
user = get_user_from_session()
if user is None or not may_join(user, channel_name):
abort(403)
member_data = None
if channel_name.startswith("presence-"):
member_data = json.dumps({"member_id": user.id, "member_info": {"name": user.name}})
return client.realtime.authorize_channel(
connection_id=body["connection_id"],
channel_name=channel_name,
member_data=member_data,
)func birdAuth(w http.ResponseWriter, r *http.Request) {
var body struct {
ConnectionID string `json:"connection_id"`
ChannelName string `json:"channel_name"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// Your own authorization decision goes here.
user, ok := userFromSession(r)
if !ok || !mayJoin(user, body.ChannelName) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
params := bird.RealtimeChannelAuthorizationParams{
ConnectionID: body.ConnectionID,
ChannelName: body.ChannelName,
}
if strings.HasPrefix(body.ChannelName, "presence-") {
memberData, _ := json.Marshal(map[string]any{
"member_id": user.ID,
"member_info": map[string]string{"name": user.Name},
})
params.MemberData = string(memberData)
}
auth, err := client.Realtime.AuthorizeChannel(params)
if err != nil {
http.Error(w, "authorization failed", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(auth)
}function birdAuth(string $connectionId, string $channelName, User $user): array
{
// Your own authorization decision goes here.
if (!mayJoin($user, $channelName)) {
http_response_code(403);
exit;
}
$memberData = null;
if (str_starts_with($channelName, 'presence-')) {
$memberData = json_encode(['member_id' => $user->id, 'member_info' => ['name' => $user->name]]);
}
return $bird->realtime->authorizeChannel($connectionId, $channelName, $memberData);
}在没有 SDK 的语言中,签名契约是相同的:使用 app secret 对字符串进行 HMAC-SHA256 运算,十六进制编码,然后在结果前加上 app key 和一个冒号。
成员与连接
成员是一个身份标识,而连接是一个打开的 WebSocket。如果某人在三个标签页中打开你的应用,则一个成员持有三个连接。member_added 在第一个连接订阅时触发,member_removed 在最后一个连接离开时触发。其他连接的变化会改变频道的连接计数,但不会产生成员事件。
常见故障
被拒绝的订阅会以客户端错误的形式到达。检查以下常见原因:
- 签名无效。 你签名的字符串不匹配。几乎总是因为重新序列化了 member_data,或者签名时使用的频道名缺少 private- 或 presence- 前缀。
- 密钥无效。auth 中的密钥属于另一个应用,或已被撤销。轮换密钥意味着需要同时更新客户端的 appKey 和端点签名所用的 secret。
- 缺少成员数据。 收到的 presence 订阅未包含 member_data。Presence 频道不允许匿名加入。
- 来自你自己端点的 403。 你的授权逻辑拒绝了请求,这对于不允许加入的用户来说是预期行为。
后续步骤
- 发送你的第一个实时事件是本指南所基于的端到端演练。
- 加密频道在此签名的基础上,还会向已批准的订阅者分发解密密钥。
- Presence 频道涵盖成员列表、成员事件以及从服务端读取在线状态。
- 终止成员连接是你的后端计算的另一个签名,它允许你关闭某个成员的所有连接。
- Webhooks 与事件介绍了 realtime.* 事件,包括成员加入和离开等事件在你自己端点上的处理。