# 强提醒 Token 鉴权
# 简介
Token 是一种动态密钥,通过 AppKey、AppSecret、用户名、有效时间戳等参数生成,安全性较高。在正式生产环境等对安全要求较高的场景中,我们推荐使用 Token 鉴权。
# 前提条件
# 开启 Token 鉴权模式
开启 Token 鉴权模式是生成 Token 的必要条件。只有应用开启 Token 鉴权模式后,您才可以在控制台获取 AppSecret,AppSecret 将用于生成 Token。创建应用后,默认开启 Token 鉴权,开启 Token 鉴权的应用下的所有设备终端都必须使用 Token 鉴权
# 获取 AppSecret
AppSecret 是生成 Token 的必要参数。应用开启鉴权模式后可获取 AppSecret,单个应用最多可同时生成 2 个 AppSecret。

# 生成 Token
正式 Token 需要由开发者自行部署服务器生成。
正式 Token 将用于正式上线阶段,Token 生成参数如下表。开发者需要在自己的服务端部署 Token 服务,参考示例代码自行生成 Token。
| 参数 | 描述 |
|---|---|
| AppKey | AppKey 是应用的唯一标识,类似应用的身份证。在控制台创建应用获取。 |
| AppSecret | 又称为 App 证书,AppSecret 为长度 20 的随机字符串,由数字和小写字母生成,在控制台应用基本信息中获取。由菊风提供,应用开启 Token 鉴权模式后生成。 |
| CallID | 自定义的唯一会话标识,要求每次通话都不同,最大64字节 |
| Salt | 随机生成的数,范围 1-254。 |
| Timestamp | 过期时间的 UNIX 时间戳,默认是当前时间 + 24 小时。 |
# 示例代码
- 生成 Token 的伪代码如下:
Ver = '01'
AppKey = <从 Portal 获取的字符串>
AppSecret = <从 Portal 获取的字符串>
CallID = <自定义的唯一会话标识, 要求每次通话都不同,最大64字节>
Salt = <随机生成的数,范围 1-254>
Timestamp = <过期时间的 UNIX 时间戳,默认是当前时间 + 24 小时>
base64 = <base64 编码函数>
byte1 = <编码 1 字节整数的函数,编码数值需要与 Salt 进行异或运算>
byte4 = <使用大端字节序编码 4 字节整数的函数,编码数值每个字节需要与 Salt 进行异或运算>
strx = <编码字符串的函数,先使用 byte1 函数编码字符串长度,然后编码与 Salt 异或之后的字符串字节序列>
hmac-sha256 = <使用 SHA256 算法的 HMAC 散列算法>
substr = <获取字符串子串的函数,指定起始偏移和长度>
Payload = base64( byte1(Salt) + byte4(xor(Timestamp, Salt)) + byte4(xor(crc32(AppKey), Salt)) + strx(CallID, Salt) )
Signature = base64( hmac-sha256(AppSecret, Payload) )
AppSecretDigest = substr(AppSecret, 0, 6)
Token = Ver + AppSecretDigest + Signature + Payload
Python 生成 Token:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import time
import hmac
import base64
import random
import struct
from zlib import crc32
from hashlib import sha256
def byte1w(val, salt=0):
return struct.pack('>B', int(val) ^ salt)
def byte2w(val, salt=0):
salt16 = (salt << 8) + salt
return struct.pack('>H', int(val) ^ salt16)
def byte4w(val, salt=0):
salt32 = (salt << 24) + (salt << 16) + (salt << 8) + salt
return struct.pack('>I', int(val) ^ salt32) # 确保整数运算
def xstrw(val, salt=0):
# 确保输入是字符串并编码为字节
if isinstance(val, str):
val = val.encode('utf-8')
ret = byte1w(len(val), salt)
for byte in val:
ret += byte1w(byte ^ salt, 0) # 直接处理字节
return ret
class AuthToken:
def __init__(self, AppKey="", AppSecret="", CallID=""):
self.Ver = '01'
self.AppKey = AppKey
self.AppSecret = AppSecret
self.CallID = CallID
self.Salt = random.randint(1, 254) # 使用系统随机源
self.Timestamp = int(time.time()) + 24 * 3600
self.Error = ""
def build(self):
if len(self.AppSecret) <= 6:
raise ValueError('Invalid AppSecret')
if not self.CallID:
raise ValueError('No CallID')
if len(self.CallID) > 64:
raise ValueError('CallID Too Long')
# 确保所有输入正确编码
app_key_bytes = self.AppKey.encode('utf-8')
call_bytes = self.CallID.encode('utf-8')
# 构建Payload
payload_parts = b''.join([
byte1w(self.Salt),
byte4w(self.Timestamp, self.Salt),
byte4w(crc32(app_key_bytes) & 0xffffffff, self.Salt),
xstrw(call_bytes, self.Salt)
])
Payload = base64.b64encode(payload_parts).decode('utf-8') # 直接解码为字符串
# 生成签名
secret_bytes = self.AppSecret.encode('utf-8')
signature_digest = hmac.new(
secret_bytes,
Payload.encode('utf-8'), # 注意Payload已是字符串需要重新编码
sha256
).digest()
Signature = base64.b64encode(signature_digest).decode('utf-8') # 解码为字符串
# 拼接最终令牌
AppSecretDigest = self.AppSecret[:6]
return self.Ver + AppSecretDigest + Signature + Payload
Java 生成 Token:
package com.juphoon.rcs.cloud.auth.util;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Base64;
import java.util.Random;
import java.util.zip.CRC32;
@Slf4j
public class AuthToken {
private String ver;
private String appKey;
private String appSecret;
private String CallID;
private int salt;
private long timestamp;
public AuthToken(String appKey, String appSecret, String CallID) {
Random random = new Random();
ver = "01";
this.appKey = appKey;
this.appSecret = appSecret;
this.CallID = CallID;
salt = random.nextInt(254) + 1;
timestamp = Instant.now().getEpochSecond() + 24 * 3600;
}
private byte[] byte1w(int val, int salt) {
return ByteBuffer.allocate(1).order(ByteOrder.BIG_ENDIAN).put((byte) (val ^ salt)).array();
}
public int byte1r2(byte[] val, int salt) {
return (int) val[0] ^ salt;
}
private byte[] byte2w(int val, int salt) {
int salt16 = (salt << 8) + salt;
return ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort((short) (val ^ salt16)).array();
}
private byte[] byte4w(long val, int salt) {
int salt32 = (salt << 24) + (salt << 16) + (salt << 8) + salt;
return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt((int) (val ^ salt32)).array();
}
private byte[] xstrw(String val, int salt) {
byte[] ret = byte1w(val.length(), salt);
for (int i = 0; i < val.length(); i++) {
ret = concatByteArrays(ret, byte1w((salt ^ (int) val.charAt(i)) & 0xff, 0));
}
return ret;
}
private byte[] concatByteArrays(byte[] a, byte[] b) {
byte[] result = new byte[a.length + b.length];
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
public String build() {
if (appSecret.length() <= 6) {
log.error("invalid appSecret");
return null;
}
if (CallID.length() == 0) {
log.error("no CallID");
return null;
}
if (CallID.length() > 64) {
log.error("CallID too long");
return null;
}
CRC32 crc32 = new CRC32();
crc32.update(appKey.getBytes(StandardCharsets.UTF_8));
long appKeyCrc = crc32.getValue() & 0xffffffff;
byte[] payload = Base64.getEncoder().encode(concatByteArrays(concatByteArrays(byte1w(salt, 0), byte4w(timestamp, salt)), concatByteArrays(byte4w(appKeyCrc, salt), xstrw(CallID, salt))));
String appSecretDigest = appSecret.substring(0, 6);
try {
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secretKeySpec);
byte[] signature = Base64.getEncoder().encode(hmacSha256.doFinal(payload));
return ver + appSecretDigest + new String(signature, StandardCharsets.UTF_8) + new String(payload, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
return null;
}
}
# 集成 Token
在调用 RTOS SDK jwxa1_config 接口时传入 Token
