高级 Token 鉴权
更新时间: 2023/06/29 02:51:51
网易云信音视频通话和互动直播产品中,鉴权方式分为安全模式和调试模式。如果您在控制台中为指定应用开启了安全模式,则对应应用的用户在加入房间时,需要通过 Token 进行身份校验;其中您可以选择在您的 App 中实现基础 Token 鉴权或者高级 Token 鉴权,若您的应用中存在对安全性要求较高的语音或视频通话场景,或者对观众上麦有权限控制的场景,建议您选择高级 Token 鉴权模式,以有效避免客户端遭遇破解攻击的问题。两种鉴权模式的区别如下表。
身份校验项 | 基础 Token 鉴权 | 高级 Token 鉴权 |
---|---|---|
检查 App ID | √ | √ |
校验用户加入房间的权限 | √ | √ |
校验用户创建房间的权限 | × | √ |
校验用户发送音、视频流的权限 | × | √ |
鉴权原理
开启用户权限控制后,当用户加入房间时,云信服务器会在校验 Token 的同时也校验权限密钥(permissionKey
),若符合约定的算法规则,则会允许用户加入房间且赋予指定的发流权限。
PermissionKey
中的权限使用了一个 byte 的前六个比特位来表示,其中每个比特位均代表一个权限,权限列表如下:
位数 | 二进制表示 | 十进制数字(输入参数) | 权限含义 |
---|---|---|---|
第 1 位 | 0000 0001 | 1 | 仅有发送音频流的权限 |
第 2 位 | 0000 0010 | 2 | 仅有发送视频流的权限 |
第 3 位 | 0000 0100 | 4 | 仅有订阅音频流的权限 |
第 4 位 | 0000 1000 | 8 | 仅有订阅视频流的权限 |
第 5 位 | 0001 0000 | 16 | 仅有创建房间的权限 |
第 6 位 | 0010 0000 | 32 | 仅有加入房间的权限 |
因此可以推算出,表示无权限的十进制参数为 0,表示仅有订阅音、视频流权限的十进制参数为 12,表示既可以发送又可以订阅视频流权限的十进制参数为 15,表示拥有全部权限的十进制参数为 63。
- Token 由云信服务器或者由您自行计算生成,具体生成逻辑请参考获取 Token。
- 用户权限由您的应用服务器在生成
permissionKey
时确定,所以您需要在您的服务器管理好用户权限列表。
高级 Token 鉴权的流程如下:
sequenceDiagram
participant 应用层
participant 应用服务器
participant NERtcSDK
participant 云信服务器
Note over 应用层, 云信服务器: 申请 permissionKey
应用层 ->> 应用服务器: 请求 uid 对应的 permissionKey
应用服务器 -->> 应用层: 生成并返回 uid 对应的 permissionKey
Note over 应用层, 云信服务器: 加入房间时鉴权
应用层 ->> NERtcSDK: 调用 joinChannel 方法加入房间并传入 token 和 permissionKey
NERtcSDK ->> 云信服务器: 校验 token 和 permissionKey
云信服务器 -->> NERtcSDK: 校验通过并返回成功加入房间的回调
NERtcSDK -->> 应用层: 返回成功加入房间的回调
实现鉴权流程
步骤一 开通高级 Token 鉴权功能
您需要为指定应用设置用户权限控制,开通高级 Token 鉴权功能。
- 若您开启了用户权限控制开关,使用该 App Key 的所有用户都必须要在加入房间时传入权限密钥参数,否则无法正常加入房间。
- 若您关闭了用户权限控制开关,云信服务器默认不会校验权限密钥。
-
登录网易云信控制台。
-
在首页单击指定应用名称。
-
在产品总览区域,单击音视频通话 2.0 产品选项卡中的功能配置。
-
单击基础功能页签,在鉴权方式区域中,单击编辑,鉴权方式选择安全模式(高级token鉴权),并单击保存。
-
在弹出对话框中单击确定。
-
单击子功能配置,单击 permKeySecret 右侧的按钮复制 permKeySecret。
在您的服务器生成 permissionKey 时,需要用到该 permKeySecret。具体请参见下文步骤二示例代码中的
GenPermKey
。
步骤二 在您的服务器生成 permissionKey 并下发给客户端
为了防止客户端遭遇破解攻击的问题,由 permissionKey
定义的权限控制只能由您的服务器计算并返回给您的客户端。
网易云信提供 GO 语言版本的 permissionKey
的计算代码,您可以直接下载并集成至您的服务器,计算代码如下:
package permkey
import (
"bytes"
"compress/zlib"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
)
// GenPermKey 生成权限密钥函数,返回加密后的字符串和错误信息
func GenPermKey(uid, expireTime int64, appkey, cname, permSecret string, privilege uint32) (string, error) {
return genPermKey(uid, expireTime, appkey, cname, permSecret, privilege)
}
func genPermKey(uid, expireTime int64, appkey, cname, permSecret string, privilege uint32) (string, error) {
// 创建权限密钥映射
permKeyMap := make(map[string]interface{})
permKeyMap["appkey"] = appkey
permKeyMap["uid"] = uid
permKeyMap["cname"] = cname
permKeyMap["privilege"] = privilege
permKeyMap["expireTime"] = expireTime
curTime := time.Now().Unix()
permKeyMap["curTime"] = curTime
// 计算签名
permKeyMap["checksum"] = hmacsha256(appkey, fmt.Sprintf("%d", uid), fmt.Sprintf("%d", curTime), fmt.Sprintf("%d", expireTime), cname, permSecret, fmt.Sprintf("%d", privilege))
// 将映射转换为JSON格式
data, err := json.Marshal(permKeyMap)
if err != nil {
return "", err
}
// 压缩数据
var b bytes.Buffer
w := zlib.NewWriter(&b)
if _, err = w.Write(data); err != nil {
return "", err
}
if err = w.Close(); err != nil {
return "", err
}
// 编码数据并返回
return base64urlEncode(b.Bytes()), nil
}
// hmacsha256 计算签名函数,返回签名字符串
func hmacsha256(appidStr, uidStr, curTimeStr, expireTimeStr, cname, permSecret, privilegeStr string) string {
var contentToBeSigned string
contentToBeSigned = "appkey:" + appidStr + "\n"
contentToBeSigned += "uid:" + uidStr + "\n"
contentToBeSigned += "curTime:" + curTimeStr + "\n"
contentToBeSigned += "expireTime:" + expireTimeStr + "\n"
contentToBeSigned += "cname:" + cname + "\n"
contentToBeSigned += "privilege:" + privilegeStr + "\n"
h := hmac.New(sha256.New, []byte(permSecret))
h.Write([]byte(contentToBeSigned))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// base64urlEncode 编码数据函数,返回编码后的字符串
func base64urlEncode(data []byte) string {
str := base64.StdEncoding.EncodeToString(data)
str = strings.Replace(str, "+", "*", -1)
str = strings.Replace(str, "/", "-", -1)
str = strings.Replace(str, "=", "_", -1)
return str
}
PermissionKey
的有效期默认为 24 小时,您可以根据业务调整,区间为 [1s,24h]。
步骤三 将 token 和 permissionKey 传递给 SDK 以对用户进行鉴权
您可以在用户加入房间、用户角色变更或用户权限密钥需要更新时,将权限控制参数传递给 SDK 以对用户进行鉴权,三种场景下鉴权的具体实现方式如下。
场景一 用户加入房间
在用户调用 joinChannel
方法加入房间时,需要设置 token
和 NERtcJoinChannelOptions
中的 permissionKey
。
适用于加入房间前就明确用户权限的情况。
示例代码如下:
//加入房间
String channelName;
String token;
long uid;
String permissionKey;
NERtcJoinChannelOptions channelOptions = new NERtcJoinChannelOptions();
channelOptions.permissionKey = permissionKey;
int ret = NERtcEx.getInstance().joinChannel(token, channelName, uid, channelOptions);
加入房间时,用户 ID 和房间名称需要与申请 Token 时使用的用户 ID 和房间名称一致。
场景二 用户角色变更
在用户需要连麦时,需要将自己的角色从观众切换到主播,此时需要再次校验用户的发流权限。因此在用户调用 setClientRole
方法切换角色时,需要调用 updatePermissionKey
方法设置新的权限密钥。
示例代码如下:
//更新权限密钥
String permissionKey = getServerToken();
NERtcEx.getInstance().updatePermissionKey(permissionKey);
//SDK返回回调
//收到updatePermissionKey回调
void onUpdatePermissionKey(String key, int error, int timeout) {
}
场景三 用户权限密钥需要更新
- 在
permissionKey
过期前 30 s,SDK 会触发onPermissionKeyWillExpire
回调,此时用户客户端可以从您的业务服务器获取新的permissionKey
并调用updatePermissionKey
方法将新生成的permissionKey
传递给 SDK,更新成功后 SDK 会触发onUpdatePermissionKey
回调。
示例代码如下:
//收到 onPermissionKeyWillExpire 回调时,向业务服务器重新申请一个 permissionKey,并调用 updatePermissionKey 将新的 permissionKey 传递给 SDK
public void onPermissionKeyWillExpire() {
Log.i(TAG, "密钥即将过期");
String permissionKey = getServerToken(); //向业务服务器重新申请一个 permissionKey
NERtcEx.getInstance().updatePermissionKey(permissionKey);
}
- 若在
permissionKey
过期前仍未完成上述操作,则 SDK 会触发onDisconnect
回调,返回ENGINE_ERROR_CHANNEL_PERMISSION_KEY_TIMEOUT
错误码,同时客户端会与音视频服务器断开连接。若用户需要再次加入房间,则需要从您的业务服务器获取新的token
和permissionKey
并调用joinChannel
方法,再使用新的token
和permissionKey
重新加入房间。
示例代码如下:
//收到 onDisconnect(ENGINE_ERROR_CHANNEL_PERMISSION_KEY_TIMEOUT)回调时,向业务服务器重新申请一个 permissionKey,并调用joinChannel重新加入房间
public void onDisconnect(int reason) {
Log.i(TAG, "onDisconnect reason: " + reason);
if (reason == ENGINE_ERROR_CHANNEL_PERMISSION_KEY_TIMEOUT) {
String permissionKey = getServerToken(); //向业务服务器重新申请一个 permissionKey
channelOptions.permissionKey = permissionKey;
postToUI->(NERtcEx.getInstance().joinChannel(token, channelName, uid, channelOptions););
}
}
相关参考
权限密钥的生命周期图
错误码
错误码(ErrorCode | 错误原因 |
---|---|
ENGINE_ERROR_USER_PERM_KEY_AUTH_FAILED = 30121 |
可能原因包括:
|
ENGINE_ERROR_CHANNEL_PERMISSION_KEY_ERROR = 30901 | 权限密钥错误。 |
ENGINE_ERROR_CHANNEL_PERMISSION_KEY_TIMEOUT = 30902 | 权限密钥超时。 |
ENGINE_ERROR_ChannelNoPublishPermission = 30911 | 用户无发流权限。 |
ENGINE_ERROR_ChannelNoSubscribePermission = 30912 | 用户无订阅权限。 |