菊风已发布实时音视频2.0升级版(2.0版本说明),当前您正在访问1.0旧版本,点击此处可进入2.0升级版

# 强提醒 Token 鉴权

# 简介

Token 是一种动态密钥,通过 AppKey、AppSecret、用户名、有效时间戳等参数生成,安全性较高。在正式生产环境等对安全要求较高的场景中,我们推荐使用 Token 鉴权。

# 前提条件

# 开启 Token 鉴权模式

开启 Token 鉴权模式是生成 Token 的必要条件。只有应用开启 Token 鉴权模式后,您才可以在控制台获取 AppSecret,AppSecret 将用于生成 Token。创建应用后,默认开启 Token 鉴权,开启 Token 鉴权的应用下的所有设备终端都必须使用 Token 鉴权

# 获取 AppSecret

AppSecret 是生成 Token 的必要参数。应用开启鉴权模式后可获取 AppSecret,单个应用最多可同时生成 2 个 AppSecret。

image.png

# 生成 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

最后更新时间: 7/31/2026, 9:21:14 AM