高级 Token 鉴权
更新时间: 2024/08/20 15:20:22
网易云信音视频通话和互动直播产品中,鉴权方式分为安全模式和调试模式。如果您在控制台中为指定应用开启了安全模式,则对应应用的用户在加入房间时,需要通过 Token 进行身份校验;您可以选择在 App 中实现基础 Token 鉴权或者高级 Token 鉴权。如果您的应用中存在对安全性要求较高的语音或视频通话场景,或者对观众上麦有权限控制的场景,建议选择高级 Token 鉴权模式,以有效避免客户端遭遇破解攻击的问题。两种鉴权模式的区别如下表:
身份校验项 | 基础 Token 鉴权 | 高级 Token 鉴权 |
---|---|---|
检查 App ID | ✔ | ✔ |
校验用户加入房间的权限 | ✔ | ✔ |
校验用户创建房间的权限 | ✖ | ✔ |
校验用户发送音、视频流的权限 | ✖ | ✔ |
鉴权原理
开启用户权限控制后,当用户加入房间时,云信服务器会在校验 Token 的同时也校验权限密钥(permissionKey
),若符合约定的算法规则,则会允许用户加入房间且赋予指定的发流权限。
PermissionKey
中的权限使用了一个 byte 的前六个比特位来表示,其中每个比特位均代表一个权限,权限列表如下:
位数 | 二进制表示 | 十进制数字 (privilege 参数的值) |
权限含义 |
---|---|---|---|
第 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鉴权),并单击保存。
-
在弹出对话框中单击确定。
-
获取
permSecret
的值。单击子功能配置,单击 permKeySecret 右侧的按钮复制 permKeySecret。
在您的服务器生成 permissionKey 时,需要用到该 permKeySecret,对应
GetPermissionKey
中的permSecret
参数的值。具体请参见下文步骤二的示例代码。
步骤二 在您的服务器生成 permissionKey 并下发给客户端
为了防止客户端遭遇破解攻击的问题,由 permissionKey
定义的权限控制只能由您的服务器计算并返回给您的客户端。
请参考云信在 GitHub 上提供的示例代码,在您的应用服务器上生成 NERtc Token 和 permissionKey。示例代码的地址如下:
语言 | 示例代码 | 关键函数 |
---|---|---|
Java | 生成 Token-Java | getPermissionKey |
Go | 生成 Token-Go | GetPermissionKey |
Node.js | 生成 Token-Nodejs | GetPermissionKey |
PHP | 生成 Token-PHP | getPermissionKey |
Python | 生成 Token-Python | get_permission_key |
C++ | 生成 Token-C++ | getPermissionKey |
C#(dotnet) | 生成 Token-C# | GetPermissionKey |
生成 NERtc permissionKey 的关键参数说明如下表所示。
参数 | 类型 | 描述 |
---|---|---|
channelName | String | RTC 房间名称。channelName 可以为空, 表示该 uid 可以使用这个 token 加入任意房间。 |
permSecret | String | 权限密钥。请从云信控制台获取对应 permKeySecret,具体请参见获取 permSecret 的值。 |
uid | Long | 用户在您应用中的 ID,请在您的业务服务器上自行管理并维护。 |
privilege | Integer | 权限等级。取值范围[1,63],具体参数的含义请参见鉴权原理。例如,设置为 63 表示拥有全部权限。 |
ttlSec | Integer | permissionKey 过期时间,单位为秒,最大为 86400 秒(1 天)。 |
appKey | String | 请登录网易云信控制台查看您的应用对应的AppKey,具体请参见创建应用并获取 AppKey。 |
以 Dart 语言为例, token
和permissionKey
的计算参考示例代码如下:
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart';
import 'package:archive/archive.dart';
class TokenServer {
final String appKey;
final String appSecret;
final int defaultTTLSec;
TokenServer(this.appKey, this.appSecret, this.defaultTTLSec) {
if (appKey.isEmpty || appSecret.isEmpty) {
throw ArgumentError('appKey or appSecret is empty');
}
if (defaultTTLSec <= 0) {
throw ArgumentError('defaultTTLSec must be positive');
}
}
String getToken(String channelName, int uid, int ttlSec) {
final curTimeMs = DateTime.now().millisecondsSinceEpoch;
return getTokenWithCurrentTime(channelName, uid, ttlSec, curTimeMs);
}
String getTokenWithCurrentTime(
String channelName,
int uid,
int ttlSec,
int curTimeMs,
) {
if (ttlSec <= 0) {
ttlSec = defaultTTLSec;
}
final signature = mySha1(
'$appKey$uid$curTimeMs$ttlSec$channelName$appSecret',
);
final tokenModel = DynamicToken(signature, curTimeMs, ttlSec);
final signatureJson = jsonEncode(tokenModel.toJson());
final base64Signature = base64.encode(utf8.encode(signatureJson));
return base64Signature;
}
String getPermissionKey(
String channelName,
String permSecret,
int uid,
int privilege,
int ttlSec,
) {
final curTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
return getPermissionKeyWithCurrentTime(
channelName,
permSecret,
uid,
privilege,
ttlSec,
curTime,
);
}
String getPermissionKeyWithCurrentTime(
String channelName,
String permSecret,
int uid,
int privilege,
int ttlSec,
int curTime,
) {
final permKey = PermissionKey(
appkey: appKey,
uid: uid,
cname: channelName,
privilege: privilege,
expireTime: ttlSec,
curTime: curTime);
final checksum = hmacsha256(
appKey,
uid.toString(),
curTime.toString(),
ttlSec.toString(),
channelName,
permSecret,
privilege.toString(),
);
permKey.checksum = checksum;
final jsonStr = jsonEncode(permKey.toJson());
final encodedData = utf8.encode(jsonStr);
final compressedData = compress(Uint8List.fromList(encodedData));
final base64EncodedData = base64EncodeUrl(compressedData);
return utf8.decode(base64EncodedData);
}
Uint8List compress(List<int> data) {
ZLibEncoder zlibEncoder = const ZLibEncoder();
List<int> compressedData = zlibEncoder.encode(data, level: 6);
return Uint8List.fromList(compressedData);
}
String hmacsha256(
String appidStr,
String uidStr,
String curTimeStr,
String expireTimeStr,
String cname,
String permSecret,
String privilegeStr) {
final contentToBeSigned =
'appkey:$appidStr\nuid:$uidStr\ncurTime:$curTimeStr\nexpireTime:$expireTimeStr\ncname:$cname\nprivilege:$privilegeStr\n';
final keySpec = utf8.encode(permSecret);
final mac = Hmac(sha256, keySpec);
final result = mac.convert(utf8.encode(contentToBeSigned));
final encodedResult = base64.encode(result.bytes);
return encodedResult;
}
Uint8List base64EncodeUrl(Uint8List input) {
final base64Encoded = base64.encode(input);
final modifiedBase64 = base64Encoded
.replaceAll('+', '*')
.replaceAll('/', '-')
.replaceAll('=', '_');
final decodedBytes = utf8.encode(modifiedBase64);
return Uint8List.fromList(decodedBytes);
}
String mySha1(String input) {
var bytes = utf8.encode(input);
var digest = sha1.convert(bytes);
var hexString = digest.bytes
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join('');
return hexString;
}
}
class DynamicToken {
final String signature;
final int curTime;
final int ttl;
DynamicToken(this.signature, this.curTime, this.ttl);
Map<String, dynamic> toJson() {
return {
'signature': signature,
'curTime': curTime,
'ttl': ttl,
};
}
}
class PermissionKey {
String appkey;
int uid;
String cname;
int privilege;
int expireTime;
int curTime;
String checksum;
PermissionKey(
{this.appkey = '',
this.uid = 0,
this.cname = '',
this.privilege = 0,
this.expireTime = 0,
this.curTime = 0,
this.checksum = ''});
Map<String, dynamic> toJson() {
return {
'appkey': appkey,
'uid': uid,
'cname': cname,
'privilege': privilege,
'expireTime': expireTime,
'curTime': curTime,
'checksum': checksum,
};
}
}
PermissionKey
的有效期默认为 24 小时,您可以根据业务调整,区间为 [1s,24h]。
步骤三 将 token 和 permissionKey 传递给 SDK 以对用户进行鉴权
您可以在用户加入房间、用户角色变更或用户权限密钥需要更新时,将权限控制参数传递给 SDK 以对用户进行鉴权,三种场景下鉴权的具体实现方式如下。
场景一 用户加入房间
在用户调用 joinChannel
方法加入房间时,需要设置 token
和 NERtcJoinChannelOptions
中的 permissionKey
。
适用于加入房间前就明确用户权限的情况。
示例代码如下:
//加入房间
String channelName;
String token;
int uid;
String permissionKey;
String customInfo;
NERtcJoinChannelOptions options =
NERtcJoinChannelOptions(customInfo, permissionKey); //customInfo如果不需要可以传空
int code = await _engine.joinChannel(token, channelName, uid, options);
加入房间时,用户 ID 和房间名称需要与申请 Token 时使用的用户 ID 和房间名称一致。
场景二 用户角色变更
在用户需要连麦时,需要将自己的角色从观众切换到主播,此时需要再次校验用户的发流权限。因此在用户调用 setClientRole
方法切换角色时,需要调用 updatePermissionKey
方法设置新的权限密钥。
示例代码如下:
//更新权限密钥
tokenServer = TokenServer(appkey, appsecret, 7200);
String permissionKey = tokenServer.getPermissionKey(channelName, permSecret, userId, privilege, ttlSec);
_engine.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
void onPermissionKeyWillExpire() {
Log.i(TAG, "密钥即将过期");
String permissionKey = tokenServer.getPermissionKey(channelName, permSecret, userId, privilege, ttlSec);//向业务服务器重新申请一个 permissionKey
_engine.updatePermissionKey(permissionKey);
}
- 若在
permissionKey
过期前仍未完成上述操作,则 SDK 会触发onDisconnect
回调,返回ENGINE_ERROR_CHANNEL_PERMISSION_KEY_TIMEOUT
错误码,同时客户端会与音视频服务器断开连接。若用户需要再次加入房间,则需要从您的业务服务器获取新的token
和permissionKey
并调用joinChannel
方法,再使用新的token
和permissionKey
重新加入房间。
示例代码如下:
//收到 onDisconnect(channelPermissionKeyTimeout)回调时,向业务服务器重新申请一个 permissionKey,并调用joinChannel重新加入房间
void onDisconnect(int reason) {
Log.i(TAG, "onDisconnect reason: $reason");
if(reason == NERtcErrorCode.channelPermissionKeyTimeout) {
String permissionKey = tokenServer.getPermissionKey(channelName, permSecret, userId, privilege, ttlSec);//向业务服务器重新申请一个 permissionKey
await _engine.updatePermissionKey(permissionKey);
}
}
相关参考
权限密钥的生命周期图
错误码
错误码(ErrorCode | 错误原因 |
---|---|
userPermKeyAuthFailed = 30121 |
可能原因包括:
|
channelPermissionKeyError = 901 | 权限密钥错误。 |
channelPermissionKeyTimeout = 902 | 权限密钥超时。 |
channelNoPublishPermission = 1620 | 用户无发流权限。 |
prtmissionKeyEngineErrorNoSubcribePermission = 2803 | 用户无订阅权限。 |