跳到主要内容

三,安全与签名、玩转聊天室和临时对话

本章导读

在前一篇消息收发的更多方式,离线推送与消息同步,多设备登录中,我们演示了与消息相关的更多特殊需求的实现方法,现在,我们会更进一步,从系统安全和成员权限管理的角度,给大家详细说明:

  • 如何通过第三方鉴权来控制客户端登录与操作
  • 如何对成员权限进行限制,以保证聊天流程能被运营人员很好管理起来
  • 如何实现一个不限人数的直播聊天室
  • 如何对大型群聊中的消息进行实时内容过滤
  • 如何使用临时对话

安全与签名

即时通讯服务有一大特色就是让应用账户系统和聊天服务解耦,终端用户只需要登录应用账户系统就可以直接使用即时通讯服务,同时从系统安全角度出发,我们还提供了第三方操作签名的机制来保证聊天通道的安全性。

该机制的工作架构是,在客户端和即时通讯云端之间,增加应用自己的鉴权服务器(也就是即时通讯服务之外的「第三方」),在客户端开始一些有安全风险的操作命令(如登录聊天服务、建立对话、加入群组、邀请他人等)之前,先通过鉴权服务器获取签名,之后即时通讯云端会依据它和第三方鉴权服务之间的协议来验证该签名,只有附带有效签名的请求才会被执行,非法请求全部会被阻止下来。

使用操作签名可以保证聊天通道的安全,这一功能默认是关闭的,可以在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置 中进行开启:

  • 登录启用签名认证,用于控制所有的用户登录
  • 对话操作启用签名认证,用于控制新建或加入对话、邀请/踢出对话成员等操作
  • 聊天记录查询启用签名认证,用于控制聊天记录查询操作

开发者可根据实际需要进行选择。一般来说,登录认证 是最基本的安全机制,我们强烈建议开发者开启登录认证。

sequenceDiagram 终端->>应用鉴权服务器: 1. 携登录、新建会话、加入群组、邀请他人、踢出成员等行为请求签名 应用鉴权服务器-->>终端: 2. 生成时间戳、随机字符串和签名返回给客户端 终端->>即时通讯服务云端: 3. 将签名编码到请求中发给即时通讯服务器 即时通讯服务云端-->>终端: 4. 对请求的内容和签名进行验证,执行后续操作
  1. 客户端进行登录或新建对话等操作,SDK 会调用 SignatureFactory 的实现,并携带用户信息和用户行为(登录、新建对话或群组操作)请求签名;
  2. 应用自有的权限系统,或应用在云引擎上的签名程序收到请求,进行权限验证,如果通过则利用下文所述的 签名算法 生成时间戳、随机字符串和签名返回给客户端;
  3. 客户端获得签名后,编码到请求中,发给即时通讯服务器;
  4. 即时通讯服务器对请求的内容和签名做一遍验证,确认这个操作是被应用服务器允许的,进而执行后续的实际操作。

签名采用 HMAC-SHA1 算法,输出字节流的十六进制字符串(hex dump)。针对不同的请求,开发者需要拼装不同组合的字符串,加上 UTC timestamp 以及随机字符串作为签名的消息(参见后续格式说明)。总体上,签名就是使用特定的密钥(在这里我们使用应用的 Master Key),对输入的消息(即「签名的消息」)进行哈希计算,得到一串十六进制的字符串,这就是最终的「签名」。

对于使用 LCUser 的应用,可使用 REST API 获取登录签名进行登录认证。

签名格式说明

下面我们详细说明一下不同操作的签名消息格式。

用户登录签名

签名的消息格式如下,注意 clientidtimestamp 之间是两个冒号

appid:clientid::timestamp:nonce
参数说明
appid应用的 ID。
clientid登录时使用的 clientId
timestamp当前的 UTC 时间距离 Unix epoch 的 毫秒数
nonce随机字符串。

注意:签名的 key 必须 是应用的 Master Key,你可以在 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 里找到。请保护好 Master Key,不要泄露给任何无关人员。

开发者可以实现自己的 SignatureFactory,调用远程服务器的签名接口获得签名。如果你没有自己的服务器,可以直接在云引擎上通过 网站托管 来实现自己的签名接口。在移动应用中直接进行签名的做法 非常危险,它可能导致你的 Master Key 泄漏。

签名的有效期是 6 个小时,强制下线后签名立即失效。 签名失效不影响当前在线的 client。

开启对话签名

新建一个对话的时候,签名的消息格式为:

appid:clientid:sorted_member_ids:timestamp:nonce
  • appidclientidtimestampnonce 的含义 同上
  • sorted_member_ids 是以半角冒号(:)分隔、升序排序clientId,即邀请参与该对话的成员列表。

群组功能的签名

在群组功能中,我们对 加群邀请踢出群 这三个动作也允许加入签名,签名的消息格式是:

appid:clientid:convid:sorted_member_ids:timestamp:nonce:action
  • appidclientidsorted_member_idstimestampnonce 的含义同上。对创建群的情况,这里 sorted_member_ids 是空字符串。
  • convid 是此次行为关联的对话 ID。
  • action 是此次行为的动作,invite 表示加群和邀请,kick 表示踢出群。

查询聊天记录的签名

appid:client_id:convid:nonce:timestamp

各参数的含义同上。

注意,此签名仅用于通过 REST API 查询历史消息,客户端 SDK 不适用。

云引擎签名范例

为了帮助开发者理解云端签名的算法,我们开源了一个用「Node.js + 云引擎」实现签名的云端,供开发者学习和使用:即时通讯云引擎签名 Demo

客户端如何支持操作签名

上面的签名算法,都是对第三方鉴权服务器如何进行签名的协议说明,在开启了操作签名的前提下,客户端这边的使用流程需要进行相应的改变,增加请求签名的环节,才能让整套机制顺利运行起来。

即时通讯 SDK 为每一个 AVIMClient 实例都预留了一个 Signature 工厂接口,这个接口默认不设置就表示不使用签名,启动签名的时候,只需要在客户端实现这一接口,调用远程服务器的签名接口获得签名,并把它绑定到 AVIMClient 实例上即可:

public class LocalSignatureFactory : ILCIMSignatureFactory {
const string MasterKey = "pyvbNSh5jXsuFQ3C8EgnIdhw";

public Task<LCIMSignature> CreateConnectSignature(string clientId) {
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, string.Empty, timestamp.ToString(), nonce);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

public Task<LCIMSignature> CreateStartConversationSignature(string clientId, IEnumerable<string> memberIds) {
string sortedMemberIds = string.Empty;
if (memberIds != null) {
List<string> sortedMemberList = memberIds.ToList();
sortedMemberList.Sort();
sortedMemberIds = string.Join(":", sortedMemberList);
}
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, sortedMemberIds, timestamp.ToString(), nonce);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

public Task<LCIMSignature> CreateConversationSignature(string conversationId, string clientId, IEnumerable<string> memberIds, string action) {
string sortedMemberIds = string.Empty;
if (memberIds != null) {
List<string> sortedMemberList = memberIds.ToList();
sortedMemberList.Sort();
sortedMemberIds = string.Join(":", sortedMemberList);
}
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

private static string SignSHA1(string key, string text) {
HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
byte[] bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(text));
string signature = BitConverter.ToString(bytes).Replace("-", string.Empty);
return signature;
}

private static string NewNonce() {
byte[] bytes = new byte[10];
using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) {
generator.GetBytes(bytes);
}
return Convert.ToBase64String(bytes);
}

private static string GenerateSignature(params string[] args) {
string text = string.Join(":", args);
string signature = SignSHA1(MasterKey, text);
return signature;
}
}

// 设置签名工厂
LCIMClient tom = new LCIMClient("tom", signatureFactory: new LocalSignatureFactory());

需要强调的是:开发者切勿在客户端直接使用 Master Key 进行签名操作,因为 Master Key 一旦泄露,会造成应用的数据处于高危状态,后果不容小视。因此,强烈建议开发者将签名的具体代码托管在安全性高稳定性好的云端服务器上(例如云引擎)。

内建账户系统(User)的签名机制

User 是存储服务提供的默认账户系统,对于使用了它来完成用户注册、登录的产品来说,终端用户通过 User 账户系统的登录认证之后,转到即时通讯服务上,是无需再进行登录签名操作的。使用 User 账号系统登录即时通讯服务的示例如下:

LCUser user = await LCUser.Login("username", "password");
CIMClient client = new LCIMClient(user);
await client.Open();

内置账户系统与即时通讯服务可以共享登录签名信息,这里我们直接用 logIn 成功之后的 LCUser 实例来创建 IMClient,在即时通讯服务的用户登录环节,云端会自动关联账户系统来确认用户身份的合法性,这样可以省掉 SDK 向第三方申请登录签名的操作,进一步简化开发流程。

IMClient 完成即时通讯系统登录之后,其他功能的使用就和之前的介绍没有任何区别了。

玩转直播聊天室

在即时通讯服务总览中,我们比较了不同的业务场景与对话类型,现在就来看看如何使用「聊天室」完成一个直播弹幕的需求。

创建聊天室

IMClient 提供了专门的 createChatRoom 方法来创建聊天室:

// 最直接的方式,传入 name 即可
tom.CreateChatRoom("聊天室");

在创建聊天室的时候,开发者可以指定聊天室的名字和附加属性(非必须),与创建普通对话的接口相比,有如下差异:

  • 聊天室因为没有成员列表,所以创建的时候指定 members 是没有意义的
  • 同样的原因,创建聊天室的时候指定 unique 标志也是没有意义的(云端无需根据成员 ID 来去重)

尽管我们调用 createConversation 接口,通过传递合适的参数({ transient: true }),也可以创建一个聊天室,但是还是建议大家直接使用 createChatRoom 方法。

查找聊天室

在即时通讯开发指南第一篇中,我们已经了解了构造复杂条件来查询对话的方法,ConversationsQuery 依然适用于查询聊天室,只需要添加 transient = true 的限制条件即可。

LCIMConversationQuery query = new LCIMConversationQuery(tom);
query.WhereEqualTo("tr", true);

上面示例中 Java / Android SDK 专门提供了 LCIMClient#getChatRoomQuery 方法来生成聊天室查询对象,屏蔽了 transient 属性的细节,建议开发者优先使用这种高层 API。

加入和离开聊天室

查询到聊天室之后,加入和离开聊天室与普通对话的对应接口没有区别,详细请参考即时通讯开发指南第一篇《多人群聊》。

在成员管理与变更通知方面,聊天室与普通对话的最大区别就是:

  • 在聊天室内无法邀请或者踢出成员,只能由用户主动加入和退出;
  • 除了用户主动退出之外,客户端断线也会被认为是退出了聊天室。为了防止网络抖动,如果客户端临时异常断线,只要在半小时内重新上线,都会自动加入原聊天室(主动退出的除外);
  • 云端不会下发成员加入、退出的变更通知;
  • 不支持查询成员列表,但提供专门的 API 来查询实时在线人数。

另外,也请注意 聊天室也不支持离线推送通知、离线消息同步、消息回执等功能

查询成员数量

LCIMConversation#memberCount 方法可以用来查询普通对话的成员总数,在聊天室中,它返回的就是实时在线的人数:

int membersCount = await conversation.GetMembersCount();

消息等级

为了保证消息的时效性,当聊天室消息过多导致客户端连接堵塞时,服务器端会选择性地丢弃部分非高等级的消息。目前支持的消息等级有:

消息等级描述
MessagePriority.HIGH高等级,针对时效性要求较高的消息,比如直播聊天室中的礼物、打赏等。
MessagePriority.NORMAL中等级,比如普通非重复性的文本消息。
MessagePriority.LOW低等级,针对时效性要求较低的消息,比如直播聊天室中的弹幕。

消息等级默认为 NORMAL

消息等级在发送接口的参数中设置。以下代码演示了如何发送一个高等级的消息:

LCIMTextMessage message = new LCIMTextMessage("现在比分是 0:0,下半场中国队肯定要做出人员调整");
LCIMMessageSendOptions options = new LCIMMessageSendOptions {
Priority = LCIMMessagePriority.High
};
await chatRoom.Send(message, options);

注意:

此功能仅针对聊天室消息有效。普通对话的消息不需要设置等级,即使设置了也会被系统忽略,因为普通对话的消息不会被丢弃。

消息免打扰

假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。

比如 Tom 工作繁忙,对某个对话设置了静音:

await chatRoom.Mute();

设置静音之后,iOS 及启用混合推送的 Android 用户就不会收到推送消息了。与之对应的就是取消静音的操作(Conversation#unmute 方法),即取消免打扰模式。

使用建议:

  • 对话内消息的静音/取消静音操作不光对聊天室有效,普通的群聊对话也可以执行该操作。
  • muteunmute 操作会修改云端 _Conversation 里面的 mu 属性。开发者切勿在控制台中对 mu 随意进行修改,否则可能会引起即时通讯云端的离线推送功能失效。

消息内容的实时过滤

对于开放聊天室来说,内容的审核和实时过滤是产品运营上的一个基本要求。即时通讯服务提供了敏感词过滤的功能,多人的普通对话、聊天室和系统对话里面的消息都会进行实时过滤。开发者可以在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 中对一对一单聊启用敏感词过滤。

命中的敏感词将会被替换为 ***

消息内容实时过滤属于系统层面的修改消息,发送者会收到 MESSAGE_UPDATE 事件。应用可以在客户端监听该事件,实现相应的业务逻辑,相关代码示例可以参考即时通讯开发指南第二篇的《修改消息》一节。

过滤的词库由即时通讯服务统一提供。商用版应用支持开发者使用自定义敏感词词库,只需在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 中上传敏感词文件。敏感词文件为 UTF-8 编码的纯文本文件,一行一个敏感词。开发者上传的自定义敏感词词库会替换默认提供的词库。

敏感词的过滤规则:如果消息是富媒体消息类型(_lctype 属性有值),那么敏感词只过滤 _lctext 这个字段的内容。如果非富媒体消息类型(消息没有 _lctype 属性),则全部消息体过滤。

如果开发者有较为复杂的过滤需求,可以使用云引擎 hook _messageReceived 来实现过滤,在 hook 中开发者对消息的内容有完全的控制力。

使用临时对话

临时对话是一个全新的概念,它解决的是一种特殊的聊天场景:

  • 对话存续时间短
  • 聊天参与的人数较少(最多为 10 个 clientId
  • 聊天记录的存储不是强需求

临时对话最大的特点是 较短的有效期,这个特点可以解决对话的持久化存储在服务端占用的存储资源越来越大、开发者需要支付的成本越来越高的问题,也可以应对一些临时聊天的场景。诸如电商售前和售后在线聊天的客服系统,我们推荐使用临时对话。

临时对话实例

IMConversation 有专门的 createTemporaryConversation 方法用于创建临时对话:

LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" });

与其他对话类型不同的是,临时对话有一个 重要 的属性:TTL。它标记着这个对话的有效期,系统默认是 1 天,但是在创建对话的时候是可以指定这个时间的,最高不超过 30 天。如果你的需求是一定要超过 30 天,请使用普通对话。传入 TTL 创建临时对话的代码如下:

LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" },
ttl: 3600);

临时对话的其他操作与普通对话无异。

进一步阅读