rtcToken生成算法
<h2>小度云rtcToken v2生成算法</h2>
<h3>一、rtcToken版本为002,token信息解析出来后包含如下几个字段,生成token除以下信息外,还需要appSecret信息:</h3>
<table>
<thead>
<tr>
<th>信息</th>
<th>类型</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>appId</td>
<td>string</td>
<td>这个token是哪个appId使用的,对应的由相应的业务服务器生成</td>
</tr>
<tr>
<td>uid</td>
<td>string</td>
<td>用户业务系统中唯一的uid</td>
</tr>
<tr>
<td>createTime</td>
<td>uint32</td>
<td>这个token的生成时间,unix时间戳,单位为秒。在rtc系统里面,同一个用户新创建的rtcToken会导致之前创建的token失效</td>
</tr>
<tr>
<td>expireTime</td>
<td>uint32</td>
<td>这个token的过期时间,unix时间戳,单位为秒,expireTime必须比createTime大。在连接rtc长连接服务时,rtc服务器会判断这个token是否超时,超时也拒绝连接。已经连接上来的长连接在到达这个超时时间时不会被断开。</td>
</tr>
<tr>
<td>random</td>
<td>uint32</td>
<td>一个随机数,在生产token时带上这个随机数</td>
</tr>
</tbody>
</table>
<h2> 二、token组成:</h2>
<h3>2.1 token字符串分为header长度(headerLength),header,token信息(tokenInfo)三部分:</h3>
<table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>headerLength</td>
<td>定长为两个字节,表明header字段的长度(长度计算不包括headerLength)。所以header最长为99个字节</td>
</tr>
<tr>
<td>header</td>
<td>header信息以-分隔,第一个字段固定为version,第二个字段固定为appid,后续按版本可以扩展其他字段。</td>
</tr>
<tr>
<td>tokenInfo</td>
<td>token信息,是做了base64urlSafe编码的字符串,具体包含createTime,expireTime,random,uid,签名等信息</td>
</tr>
</tbody>
</table>
<h3>2.2 tokenInfo部分生成:</h3>
<p>tokenInfo按照下面方式首先生成一个byte流(buffer):</p>
<pre><code class="language-html">createTime按照uin32 big endian方式写入buffer。
expireTime按照uin32 big endian方式写入buffer。
random按照uin32 big endian方式写入buffer。
uid字符串写入buffer之前,先写入一个uint16 big endian的长度(字符串的长度),然后再把uid字符串的byte流写入。
sign信息,为下面的字符串进行拼接,然后取md5后的byte数组(这里是byte数组,不是16进制显示的ascii字符串)。先写入一个uint16 big endian的长度到buffer,然后把生成后的md5 byte流写入buffer。
createTime转换为10进制字符串
expireTime转换为10进制字符串
random转换为10进制字符串
uid字符串
appId字符串
appSecret字符串</code></pre>
<p><code>上面的字节流做base64 urlSafe编码,即可生成tokenInfo。</code></p>
<h2>三、例子:</h2>
<p>假设appId是10000, appSecret是thisisaexample, uid是hellotom, token createTime 1579412009, token expireTime是1606752000,random是1277422310,则生成token信息如下:</p>
<p>09002-10000XiPqKV_FFwBMI-rmAAhoZWxsb3RvbQAQ5zpBq_FGwR2A7cMmfxYZAw==
其中前两个字节是headerLength,数值09表明header占9个字节。
根据headerLength,后面九个字节为header,内容为 002-10000, 其中002为版本号,10000为appId
剩下部分为tokenInfo, base64 urlSafe编码:XiPqKV_FFwBMI-rmAAhoZWxsb3RvbQAQ5zpBq_FGwR2A7cMmfxYZAw==,解码后即为上面说明的tokenInfo的组成字段。</p>
<h2>四、调试工具和demo</h2>
<h3>4.1 调试工具</h3>
<p><a href="https://dueros.baidu.com/business/emp/view/restdebug?pageType=getRtcToken&toolsType=restApi">https://dueros.baidu.com/business/emp/view/restdebug?pageType=getRtcToken&toolsType=restApi</a></p>
<h3>4.2 demo</h3>
<h4>4.2.1 PHP版本</h4>
<pre><code class="language-php">
&lt;?php
class RTCToken {
private $rtcAppId;
private $rtcAppSecret;
private $duration;
private $tokenVersion = &quot;002&quot;;
function __construct($rtcAppId, $rtcAppSecret, $duration) {
$this-&gt;rtcAppId = $rtcAppId;
$this-&gt;rtcAppSecret = $rtcAppSecret;
$this-&gt;duration = $duration;
}
public function buildToken($uid) {
$binaryString = &quot;&quot;;
$createTime = time();
$expireTime = $createTime + $this-&gt;duration;
$random = mt_rand(1, 1294967295);
$binaryString .= pack(&quot;N&quot;, $createTime);
$binaryString .= pack(&quot;N&quot;, $expireTime);
$binaryString .= pack(&quot;N&quot;, $random);
$binaryString .= pack(&quot;n&quot;, strlen($uid));
for($i=0; $i&lt;strlen($uid); $i++) {
$binaryString .= pack(&quot;C&quot;, ord($uid[$i]));
}
$signStr = &quot;$createTime$expireTime$random$uid&quot;;
$sign = $this-&gt;generateSignature($signStr);
$binaryString .= pack(&quot;n&quot;, strlen($sign));
for($i=0; $i&lt;strlen($sign); $i++) {
$binaryString .= pack(&quot;C&quot;, ord($sign[$i]));
}
$header = $this-&gt;tokenVersion . &quot;-&quot; . $this-&gt;rtcAppId;
$headerLen = strlen($header);
$token = sprintf(&quot;%02d&quot;,$headerLen) . $header . $this-&gt;base64url_encode($binaryString, true);
return $token;
}
private function generateSignature($string) {
$str = $string . $this-&gt;rtcAppId . $this-&gt;rtcAppSecret;
return hash(&quot;md5&quot;, $str, true);
}
private function base64url_encode($data, $pad = null) {
$data = str_replace(array(&quot;+&quot;, &quot;/&quot;), array(&quot;-&quot;, &quot;_&quot;), base64_encode($data));
if (!$pad) {
$data = rtrim($data, &quot;=&quot;);
}
return $data;
}
}
$tokenObj = new RTCToken(&quot;thisisappId&quot;, &quot;thisisappSecret&quot;, 3*30*24*3600);
echo $tokenObj-&gt;buildToken(&quot;thisisuserid&quot;) . &quot;\n&quot;;
</code></pre>
<h4>4.2.2 GOLANG版本</h4>
<pre><code class="language-go">
package main
import (
&quot;bytes&quot;
&quot;crypto/md5&quot;
&quot;encoding/base64&quot;
&quot;encoding/binary&quot;
&quot;fmt&quot;
&quot;io&quot;
&quot;math/rand&quot;
&quot;strconv&quot;
&quot;testing&quot;
&quot;time&quot;
)
const (
RtcTokenVersion2 = &quot;002&quot;
)
// RtcTokenV2 token结构,此结构只在服务器使用,不暴露给客户端
type RtcTokenV2 struct {
AppID string
//AppSecret string // 此appid对应的secret,不能提供给客户端,只能在服务器保存
UID string // 用户系统中唯一的uid,由调用方提供
CreateTime uint32 // token生成时间,unix时间戳,秒
ExpireTime uint32 // token过期时间,unix时间戳,秒
Random uint32 // 随机数
}
// GetVersion 实现RtcToken接口方法
func (token *RtcTokenV2) GetVersion() string {
return RtcTokenVersion2
}
// GetAppId 实现RtcToken接口方法
func (token *RtcTokenV2) GetAppId() string {
return token.AppID
}
// GetUid 实现RtcToken接口方法
func (token *RtcTokenV2) GetUid() string {
return token.UID
}
// GetCreateTime 实现RtcToken接口方法
func (token *RtcTokenV2) GetCreateTime() uint32 {
return token.CreateTime
}
// GetExpireTime 实现RtcToken接口方法
func (token *RtcTokenV2) GetExpireTime() uint32 {
return token.ExpireTime
}
// NewRtcTokenV2 创建rtc token
// aliveSeconds是token的保活时长,最好不要太长,代码中没做限制
func NewRtcTokenV2(AppID, uid string, aliveSeconds uint32) RtcTokenV2 {
// 缺省过期时间为1天
createTime := uint32(time.Now().Unix())
expireTime := createTime + aliveSeconds
rand := uint32(random(1, 1294967295))
return RtcTokenV2{AppID, uid, createTime, expireTime, rand}
}
// BuildToken 生成token字符串
func (token *RtcTokenV2) BuildToken(appSecret string) (string, error) {
ret := &quot;&quot;
version := RtcTokenVersion2
buf := new(bytes.Buffer)
if err := packUint32(buf, token.CreateTime); err != nil {
return ret, err
}
if err := packUint32(buf, token.ExpireTime); err != nil {
return ret, err
}
if err := packUint32(buf, token.Random); err != nil {
return ret, err
}
if err := packString(buf, token.UID); err != nil {
return ret, err
}
// 计算签名
sign := token.generateSignature(appSecret)
// 写入签名部分
if err := packString(buf, sign); err != nil {
return ret, err
}
bytesContent := buf.Bytes()
// 前两个字节是header长度,header中不同字段以-分割,版本1第一个字段是version,第二个字段为appid,header长度要小于100字节
header := version + &quot;-&quot; + token.AppID
headerLen := len(header)
headerLenStr := fmt.Sprintf(&quot;%02d&quot;, headerLen)
ret = headerLenStr + header + base64.URLEncoding.EncodeToString(bytesContent)
fmt.Sprintf(&quot;generate token %s&quot;, ret)
return ret, nil
}
// util方法,生成签名
func (token *RtcTokenV2) generateSignature(appSecret string) string {
val := strconv.FormatUint(uint64(token.CreateTime), 10) + strconv.FormatUint(uint64(token.ExpireTime), 10) + strconv.FormatUint(uint64(token.Random), 10) + token.UID + token.AppID + appSecret
md5Bytes := md5SumBytes(val)
return string(md5Bytes)
}
// pack
func packUint16(w io.Writer, n uint16) error {
return binary.Write(w, binary.BigEndian, n)
}
func packUint32(w io.Writer, n uint32) error {
return binary.Write(w, binary.BigEndian, n)
}
func packString(w io.Writer, s string) error {
err := packUint16(w, uint16(len(s)))
if err != nil {
return err
}
_, err = w.Write([]byte(s))
return err
}
func random(min int, max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}
// md5SumBytes 计算string的md5值,二进制字节格式
func md5SumBytes(orig string) []byte {
srcBuf := new(bytes.Buffer)
srcBuf.Write([]byte(orig))
SrcBytes := srcBuf.Bytes()
bytesSig := md5.Sum(SrcBytes)
return bytesSig[:]
}
func TestExample(t *testing.T) {
// token := RtcTokenV2{&quot;10000&quot;, &quot;hellotom&quot;, 1579412009, 1606752000, 1277422310}
token := NewRtcTokenV2(&quot;10000&quot;, &quot;hellotom&quot;, 3*30*24*3600) // 推荐过期时间三个月
tokenStr, err := token.BuildToken(&quot;thisisaexample&quot;)
t.Log(&quot;token &quot;, tokenStr, &quot;, error &quot;, err)
}
</code></pre>
<h4>4.2.3 java版本</h4>
<pre><code class="language-go">// 通过如下链接下载完整的java版demo代码
https://dumi-dueros-bj-tob.cdn.bcebos.com/amis/2022-11/1669348498814/rtcToken_java.zip</code></pre>
<h4>4.2.4 C++ 版本</h4>
<pre><code class="language-go">// 通过如下链接下载完整的c++版demo代码
https://dumi-dueros-bj-tob.cdn.bcebos.com/amis/2022-11/1669348449751/rtcToken_cpp.zip</code></pre>