AccessToken 升级指南

  • Android

更新时间不可用

了解实时互动 RTC 在不同平台上的 AccessToken 升级指南方式,包括接入流程、关键配置和平台差异。

鉴权原理

本文介绍如何使用 AccessToken 鉴权以及如何升级至 AccessToken2。下图展示了鉴权的基本流程:

Token 在 App 服务器上生成,其最大有效期为 24 小时。当用户从你的 App 客户端连接声网频道时,声网平台会读取该 Token 中包含的信息,并进行校验。Token 包含以下信息:

  • 你在声网控制台创建项目时生成的 App ID
  • 你的项目的 App 证书
  • 频道名
  • 用户 ID
  • 用户权限,如是否能发流或收流
  • Token 过期的 Unix 时间戳

前提条件

开始前,请确保你的项目或使用的声网产品满足如下条件:

实现鉴权流程

本节介绍如何使用声网提供的代码生成并提供 Token,对用户及其权限进行校验。

获取 App ID 及 App 证书

本节介绍如何获取生成 Token 所需的安全信息,如你的项目的 App ID 及 App 证书。

获取 App ID

声网会给每个项目自动分配一个 App ID 作为项目唯一标识。

在声网控制台的项目管理页面,找到你的项目,点击 App ID 右侧的图标,即可获取项目的 App ID。

获取 App 证书

参考以下步骤获取 App 证书:

  1. 在声网控制台的项目管理页面,找到你的项目,点击配置。

  2. 点击主要证书下面的复制图标,即可获取项目的 App 证书。

部署 Token 服务器

Token Generator 会为客户端 App 生成请求所需的 Token,从而实现对声网平台的安全访问。为了提供 Token,你需要在安全基础设施中部署一个 Token Generator。

为了展示鉴权的工作流程,本节介绍如何使用 Golang 在你的本地设备上搭建并运行一个 Token 服务器。

  1. 创建一个 server.go 文件,然后贴入如下代码。将其中的 <Your App ID> 和 <Your App Certificate> 替换为你的 App ID 和 App 证书。

    Go
    package main
    
    import (
        rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/RtcTokenBuilder"
        "fmt"
        "log"
        "net/http"
        "time"
        "encoding/json"
        "errors"
        "strconv"
    )
    
    type rtc_int_token_struct struct{
        Uid_rtc_int uint32 `json:"uid"`
        Channel_name string `json:"ChannelName"`
        Role uint32 `json:"role"`
    }
    
    var rtc_token string
    var int_uid uint32
    var channel_name string
    
    var role_num uint32
    var role rtctokenbuilder.Role
    
    // 使用 RtcTokenBuilder 来生成 RTC Token
    func generateRtcToken(int_uid uint32, channelName string, role rtctokenbuilder.Role){
    
        appID := "<Your App ID>"
        appCertificate := "<Your App Certificate>"
        // Token 过期的时间,单位为秒
        // 为作演示,在此将过期的时间戳设为 40 秒。当 Token 即将过期时,你可以看到客户端自动更新 Token 的过程
        expireTimeInSeconds := uint32(40)
        // 获取当前时间戳
        currentTimestamp := uint32(time.Now().UTC().Unix())
        // Token 过期的 Unix 时间戳
        expireTimestamp := currentTimestamp + expireTimeInSeconds
    
        result, err := rtctokenbuilder.BuildTokenWithUID(appID, appCertificate, channelName, int_uid, role, expireTimestamp)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("Token with uid: %s\n", result)
            fmt.Printf("uid is %d\n", int_uid )
            fmt.Printf("ChannelName is %s\n", channelName)
            fmt.Printf("Role is %d\n", role)
        }
        rtc_token = result
    }
    
    func rtcTokenHandler(w http.ResponseWriter, r *http.Request){
        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS");
        w.Header().Set("Access-Control-Allow-Headers", "*");
    
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
    
        if r.Method != "POST" && r.Method != "OPTIONS" {
            http.Error(w, "Unsupported method. Please check.", http.StatusNotFound)
            return
        }
    
        var t_int rtc_int_token_struct
        var unmarshalErr *json.UnmarshalTypeError
        int_decoder := json.NewDecoder(r.Body)
        int_err := int_decoder.Decode(&t_int)
        if (int_err == nil) {
    
                    int_uid = t_int.Uid_rtc_int
                    channel_name = t_int.Channel_name
                    role_num = t_int.Role
                    switch role_num {
                    case 0:
                        // 已废弃。RoleAttendee 和 RolePublisher 的权限相同
                        role = rtctokenbuilder.RoleAttendee
                    case 1:
                        role = rtctokenbuilder.RolePublisher
                    case 2:
                        role = rtctokenbuilder.RoleSubscriber
                    case 101:
                        // 已废弃。RoleAdmin 和 RolePublisher 的权限相同
                        role = rtctokenbuilder.RoleAdmin
                    }
        }
        if (int_err != nil) {
    
            if errors.As(int_err, &unmarshalErr){
                    errorResponse(w, "Bad request. Wrong type provided for field " + unmarshalErr.Value  + unmarshalErr.Field + unmarshalErr.Struct, http.StatusBadRequest)
                    } else {
                    errorResponse(w, "Bad request.", http.StatusBadRequest)
                }
            return
        }
    
        generateRtcToken(int_uid, channel_name, role)
        errorResponse(w, rtc_token, http.StatusOK)
        log.Println(w, r)
    }
    
    func errorResponse(w http.ResponseWriter, message string, httpStatusCode int){
        w.Header().Set("Content-Type", "application/json")
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.WriteHeader(httpStatusCode)
        resp := make(map[string]string)
        resp["token"] = message
        resp["code"] = strconv.Itoa(httpStatusCode)
        jsonResp, _ := json.Marshal(resp)
        w.Write(jsonResp)
    
    }
    
    func main(){
        // 处理路由
        // 使用 int 型 uid 生成 RTC Token
        http.HandleFunc("/fetch_rtc_token", rtcTokenHandler)
        fmt.Printf("Starting server at port 8082\n")
    
        if err := http.ListenAndServe(":8082", nil); err != nil {
            log.Fatal(err)
        }
    }
  2. go.mod 文件定义导入路径及依赖项。运行如下命令行来为你的 Token 服务器创建 go.mod 文件:

    Go
    $ go mod init sampleServer
  3. 运行如下命令行安装依赖。你可以使用 Go 镜像进行加速,例如 https://goproxy.cn。

    Go
    $ go get
  4. 运行如下命令行启动服务器:

    Go
    $ go run server.go

使用 Token 对用户鉴权

本节以 Android 客户端为例,展示如何使用 Token 对客户端的用户进行鉴权。

为了展示鉴权的工作流程,本节介绍如何在你的本地开发环境上使用 Android 模拟器搭建并运行一个 Android 客户端。

  1. 基于你在实现互动直播时创建的项目,在 /Gradle Scripts/build.gradle(Module: <projectname>.app) 路径下添加如下依赖:

    Java
    dependencies {
     ...
     implementation 'com.squareup.okhttp3:okhttp:3.10.0'
     implementation 'com.google.code.gson:gson:2.8.4'
     ...
    }
  2. 将 MainActivity.java 中的内容替换为如下代码。将 Your App ID 替换为你的 App ID,必须与服务器中的 App ID 一致。你还需要将 <Your Host URL and port> 替换为你刚刚部署的本地 Golang 服务器的主机 URL 和端口,例如 10.53.3.234:8082。

    在如下代码示例中,你可以看到 Token 与客户端的如下代码逻辑有关:

  • 调用 joinChannel 方法,使用 Token、用户 ID 和频道名加入频道。用户 ID 和频道名必须和用于生成 Token 的用户 ID 和频道名一致。

  • 在 Token 过期前 30 秒,SDK 会触发 onTokenPrivilegeWillExpire 回调。收到该回调后,客户端需要从服务器获取新的 Token 并调用 renewToken 将新生成的 Token 传给 SDK。

  • Token 过期时,SDK 会触发 onRequestToken 回调。收到该回调后,客户端需要从服务器获取新的 Token 并调用 joinChannel 方法,再使用新的 Token 重新加入频道。

    Java
    package com.example.rtcquickstart;
    
    import androidx.appcompat.app.AppCompatActivity;
    
    import android.os.Bundle;
    
    import androidx.core.app.ActivityCompat;
    import androidx.core.content.ContextCompat;
    
    import android.Manifest;
    import android.content.pm.PackageManager;
    import android.view.SurfaceView;
    import android.widget.FrameLayout;
    import android.widget.Toast;
    import io.agora.rtc2.Constants;
    import io.agora.rtc2.IRtcEngineEventHandler;
    import io.agora.rtc2.RtcEngine;
    import io.agora.rtc2.video.VideoCanvas;
    import io.agora.rtc2.ChannelMediaOptions;
    
    import okhttp3.MediaType;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.RequestBody;
    import okhttp3.Response;
    import okhttp3.Call;
    import okhttp3.Callback;
    
    import com.google.gson.Gson;
    
    import java.util.Map;
    
    import org.json.JSONException;
    import org.json.JSONObject;
    
    import java.io.IOException;
    
    public class MainActivity extends AppCompatActivity {
    
        // 填入在声网控制台创建项目时生成的 App ID
        private String appId = "Your App ID";
        // 填入频道名称
        private String channelName = "1234";
    
        private String token = "";
    
        private RtcEngine mRtcEngine;
    
        private int joined = 1;
    
        private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
            @Override
            // 监听远端用户加入频道,并获取远端用户的用户 ID
            public void onUserJoined(int uid, int elapsed) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        // 在 onUserJoined 回调中获取用户 ID 后,调用 setupRemoteVideo 设置远端用户视图
                        setupRemoteVideo(uid);
                    }
                });
            }
    
            @Override
            public void onTokenPrivilegeWillExpire(String token) {
                fetchToken(1234, channelName, 1);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast toast = Toast.makeText(MainActivity.this, "Token renewed", Toast.LENGTH_SHORT);
                        toast.show();
                    }
                });
                super.onTokenPrivilegeWillExpire(token);
            }
    
            @Override
            public void onRequestToken() {
                joined = 1;
                fetchToken(1234, channelName, 1);
                super.onRequestToken();
            }
    
        };
    
        private static final int PERMISSION_REQ_ID = 22;
    
        private static final String[] REQUESTED_PERMISSIONS = {
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.CAMERA
        };
    
        private boolean checkSelfPermission(String permission, int requestCode) {
            if (ContextCompat.checkSelfPermission(this, permission) !=
                    PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode);
                return false;
            }
            return true;
        }
    
       // 获取 RTC Token
       private void fetchToken(int uid,String channelName,int tokenRole){
           OkHttpClient client = new OkHttpClient();
           MediaType JSON = MediaType.parse("application/json; charset=utf-8");
           JSONObject json = new JSONObject();
           try {
               json.put("uid", uid);
               json.put("ChannelName", channelName);
               json.put("role", tokenRole);
           } catch (JSONException e) {
               e.printStackTrace();
           }
    
            RequestBody requestBody = RequestBody.create(JSON, String.valueOf(json));
            Request request = new Request.Builder()
                    .url("http://<Your Host URL and port>/fetch_rtc_token")
                    .header("Content-Type", "application/json; charset=UTF-8")
                    .post(requestBody)
                    .build();
            Call call = client.newCall(request);
            call.enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
    
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    if(response.isSuccessful()){
                        Gson gson = new Gson();
                        String result = response.body().string();
                        Map map = gson.fromJson(result, Map.class);
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                token = map.get("token").toString();
                                // 如果用户未加入频道,使用 token 加入频道
                                ChannelMediaOptions options = new ChannelMediaOptions();
                                if (joined != 0){joined = mRtcEngine.joinChannel(token, channelName, 1234, options);}
                                // 如果用户已加入频道,调用 renewToken 重新生成 token
                                else {mRtcEngine.renewToken(token);}
                            }
                        });
                    }
                }
            });
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // 如果所有权限都被授予,则初始化 RtcEngine 对象并加入频道
            if (checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) &&
                    checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)) {
                initializeAndJoinChannel();
            }
        }
    
        protected void onDestroy() {
            super.onDestroy();
    
            mRtcEngine.leaveChannel();
            mRtcEngine.destroy();
        }
    
        private void initializeAndJoinChannel() {
            try {
                mRtcEngine = RtcEngine.create(getBaseContext(), appId, mRtcEventHandler);
            } catch (Exception e) {
                throw new RuntimeException("Check the error.");
            }
    
            // 在互动直播中,设置频道场景为 BROADCASTING
            mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);
            // 根据实际情况,设置用户角色为 BROADCASTER 或 AUDIENCE
            mRtcEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
    
            // SDK 默认关闭视频。调用 enableVideo 开启视频
            mRtcEngine.enableVideo();
    
            FrameLayout container = findViewById(R.id.local_video_view_container);
            // 调用 CreateRendererView 创建一个 SurfaceView 对象并将其作为子项添加到 FrameLayout
            SurfaceView surfaceView = new SurfaceView (getBaseContext());
            container.addView(surfaceView);
            // 将 SurfaceView 对象传给声网,渲染本地视频
            mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));
            // 开始本地视频预览
            mRtcEngine.startPreview();
            // 从 Token 服务器获取 Token
            fetchToken(1234, channelName, 1);
        }
    
        private void setupRemoteVideo(int uid) {
            FrameLayout container = findViewById(R.id.remote_video_view_container);
            SurfaceView surfaceView = new SurfaceView (getBaseContext());
            surfaceView.setZOrderMediaOverlay(true);
            container.addView(surfaceView);
            mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
        }
    
    }

运行项目

在本地设备的 Android 模拟器中构建并运行项目,App 会执行如下操作:

  1. 加入频道。
  2. 每隔 10 秒更新 Token。

参考

本节介绍 Token 生成器代码库、使用 Token 的版本要求等相关文档。

Token 生成器代码

声网在 GitHub 上提供一个开源的 AgoraDynamicKey 仓库,支持使用 C++、Java、Go 等语言在你自己的服务器上生成 Token。

语言算法核心方法示例代码
C++HMAC-SHA256buildTokenWithUidRtcTokenBuilderSample.cpp
GoHMAC-SHA256buildTokenWithUidsample.go
JavaHMAC-SHA256buildTokenWithUidRtcTokenBuilderSample.java
Node.jsHMAC-SHA256buildTokenWithUidRtcTokenBuilderSample.js
PHPHMAC-SHA256buildTokenWithUidRtcTokenBuilderSample.php
PythonHMAC-SHA256buildTokenWithUidRtcTokenBuilderSample.py
Python3HMAC-SHA256buildTokenWithUidRtcTokenBuilderSample.py
表格内容超出当前宽度,可横向滚动查看更多列。

API 参考

本节介绍生成 Token 的 API 参数和描述。 以 C++ 为例:

C++
static std::string buildTokenWithUid(
    const std::string& appId,
    const std::string& appCertificate,
    const std::string& channelName,
    uint32_t uid,
    UserRole role,
    uint32_t privilegeExpiredTs = 0);
参数描述
appId你在声网控制台创建项目时生成的 App ID。
appCertificate你的项目的 App 证书。
channelName

频道名称,长度在 64 个字节以内。以下为支持的字符集范围:

  • 26 个小写英文字母 a-z;
  • 26 个大写英文字母 A-Z;
  • 10 个数字 0-9;
  • 空格标点符号和其他符号, 包括: "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", "{", "}", "", "~", ","。
uid待鉴权用户的用户 ID 32 位无符号整数,范围为 1 到 (2³² - 1), 并保证唯一性。 如不需对用户 ID 进行鉴权,即客户端使用任何 uid 都可加入频道,请把 uid 设为 0。
role

用户权限,分为发流用户和接收用户。参数决定用户是否能在频道中发流。

  • Role_Publisher(1):(默认)用户有发流权限,即用户可在频道中发流。
  • Role_Subscriber(2):用户有接收权限,即用户可在频道中接收,但不可发流。该参数仅在开启连麦鉴权后才生效。详情参考如何使用连麦鉴权功能?
privilegeExpiredTsToken 过期的 Unix 时间戳,单位为秒。该值为当前时间戳和 Token 有效期的总和。 例如,如果你将 privilegeExpiredTs 设为当前时间戳再加 600 秒,则 Token 会在 10 分钟内过期。Token 的最大有效期为 24 小时。 如果你将此参数设为 0,或时间长度超过 24 小时,Token 有效期依然为 24 小时。
表格内容超出当前宽度,可横向滚动查看更多列。

升级至 AccessToken2

升级 Token 服务器

  1. 将导入 rtctokenbuilder 声明替换为以下代码,并且删除导入 "time" 的声明:

    Go
    import (
        // 替换
        // rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/RtcTokenBuilder"
        rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/rtctokenbuilder2"
        "fmt"
        "log"
        "net/http"
        // 删除
        // "time"
        "encoding/json"
        "errors"
        "strconv"
    )
  2. 删除生成时间戳的声明,添加 tokenExpireTimeInSeconds 和 privilegeExpireTimeInSeconds 相关代码:

    Go
    // expireTimeInSeconds := uint32(40)
    // 获取当前时间戳
    // currentTimestamp := uint32(time.Now().UTC().Unix())
    // Timestamp when the token expires.
    // expireTimestamp := currentTimestamp + expireTimeInSeconds
    tokenExpireTimeInSeconds := uint32(40)
    privilegeExpireTimeInSeconds := uint32(40)
  3. 更新 tokenbuilder 方法代码:

    Go
    // 更新前代码
    // result, err := rtctokenbuilder.BuildTokenWithUID(appID, appCertificate, channelName, int_uid, role, expireTimestamp)
    // 更新后代码
    // 将 BuildTokenWithUID 更改为 BuildTokenWithUid
    // 将 expireTimestamp 更改为 tokenExpireTimeInSeconds 和 privilegeExpireTimeInSeconds
    result, err := rtctokenbuilder.BuildTokenWithUid(appID, appCertificate, channelName, int_uid, role, tokenExpireTimeInSeconds, privilegeExpireTimeInSeconds)
  4. 删除不支持的角色:

    Go
    switch role_num {
        // 删除
        // case 0:
        // 已废弃。RoleAttendee 和 RolePublisher 的权限相同
        // role = rtctokenbuilder.RoleAttendee
        case 1:
            role = rtctokenbuilder.RolePublisher
        case 2:
            role = rtctokenbuilder.RoleSubscriber
        // 删除
        // case 101:
        // 已废弃。RoleAdmin 和 RolePublisher 的权限相同
        // role = rtctokenbuilder.RoleAdmin
    }

测试 AccessToken2 服务器

警告

如果你同时在使用 RTC 拓展产品或服务,如云端录制、旁路推流,声网推荐你在升级到 AccessToken2 前联系技术支持。

为了测试 AccessToken2 服务器是否正常运行,在本地设备的 Android 模拟器中构建并运行项目,App 会执行如下操作:

  • 成功加入频道。
  • 每 10 秒更新一次 Token。