AccessToken 升级指南
- Android
更新时间不可用
了解实时互动 RTC 在不同平台上的 AccessToken 升级指南方式,包括接入流程、关键配置和平台差异。
鉴权原理
本文介绍如何使用 AccessToken 鉴权以及如何升级至 AccessToken2。下图展示了鉴权的基本流程:
Token 在 App 服务器上生成,其最大有效期为 24 小时。当用户从你的 App 客户端连接声网频道时,声网平台会读取该 Token 中包含的信息,并进行校验。Token 包含以下信息:
- 你在声网控制台创建项目时生成的 App ID
- 你的项目的 App 证书
- 频道名
- 用户 ID
- 用户权限,如是否能发流或收流
- Token 过期的 Unix 时间戳
前提条件
开始前,请确保你的项目或使用的声网产品满足如下条件:
-
一个有效的声网账户及已开启 App 证书的声网项目。请参考开通 RTC 服务。
-
Golang 1.14 或以上版本,GO111MODULE 设置为开启。
注意
如果你使用的是 Go 1.16 或以上版本,GO111MODULE 已默认开启。详情请参考 New module changes in Go 1.16。
实现鉴权流程
本节介绍如何使用声网提供的代码生成并提供 Token,对用户及其权限进行校验。
获取 App ID 及 App 证书
本节介绍如何获取生成 Token 所需的安全信息,如你的项目的 App ID 及 App 证书。
获取 App ID
声网会给每个项目自动分配一个 App ID 作为项目唯一标识。
在声网控制台的项目管理页面,找到你的项目,点击 App ID 右侧的图标,即可获取项目的 App ID。
获取 App 证书
参考以下步骤获取 App 证书:
-
在声网控制台的项目管理页面,找到你的项目,点击配置。
-
点击主要证书下面的复制图标,即可获取项目的 App 证书。
部署 Token 服务器
Token Generator 会为客户端 App 生成请求所需的 Token,从而实现对声网平台的安全访问。为了提供 Token,你需要在安全基础设施中部署一个 Token Generator。
为了展示鉴权的工作流程,本节介绍如何使用 Golang 在你的本地设备上搭建并运行一个 Token 服务器。
-
创建一个
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) } } -
go.mod文件定义导入路径及依赖项。运行如下命令行来为你的 Token 服务器创建go.mod文件:Go $ go mod init sampleServer -
运行如下命令行安装依赖。你可以使用 Go 镜像进行加速,例如 https://goproxy.cn。
Go $ go get -
运行如下命令行启动服务器:
Go $ go run server.go
使用 Token 对用户鉴权
本节以 Android 客户端为例,展示如何使用 Token 对客户端的用户进行鉴权。
为了展示鉴权的工作流程,本节介绍如何在你的本地开发环境上使用 Android 模拟器搭建并运行一个 Android 客户端。
-
基于你在实现互动直播时创建的项目,在
/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' ... } -
将
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 会执行如下操作:
- 加入频道。
- 每隔 10 秒更新 Token。
参考
本节介绍 Token 生成器代码库、使用 Token 的版本要求等相关文档。
Token 生成器代码
声网在 GitHub 上提供一个开源的 AgoraDynamicKey 仓库,支持使用 C++、Java、Go 等语言在你自己的服务器上生成 Token。
| 语言 | 算法 | 核心方法 | 示例代码 |
|---|---|---|---|
| C++ | HMAC-SHA256 | buildTokenWithUid | RtcTokenBuilderSample.cpp |
| Go | HMAC-SHA256 | buildTokenWithUid | sample.go |
| Java | HMAC-SHA256 | buildTokenWithUid | RtcTokenBuilderSample.java |
| Node.js | HMAC-SHA256 | buildTokenWithUid | RtcTokenBuilderSample.js |
| PHP | HMAC-SHA256 | buildTokenWithUid | RtcTokenBuilderSample.php |
| Python | HMAC-SHA256 | buildTokenWithUid | RtcTokenBuilderSample.py |
| Python3 | HMAC-SHA256 | buildTokenWithUid | RtcTokenBuilderSample.py |
API 参考
本节介绍生成 Token 的 API 参数和描述。 以 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 个字节以内。以下为支持的字符集范围:
|
uid | 待鉴权用户的用户 ID 32 位无符号整数,范围为 1 到 (2³² - 1), 并保证唯一性。 如不需对用户 ID 进行鉴权,即客户端使用任何 uid 都可加入频道,请把 uid 设为 0。 |
role | 用户权限,分为发流用户和接收用户。参数决定用户是否能在频道中发流。
|
privilegeExpiredTs | Token 过期的 Unix 时间戳,单位为秒。该值为当前时间戳和 Token 有效期的总和。 例如,如果你将 privilegeExpiredTs 设为当前时间戳再加 600 秒,则 Token 会在 10 分钟内过期。Token 的最大有效期为 24 小时。 如果你将此参数设为 0,或时间长度超过 24 小时,Token 有效期依然为 24 小时。 |
升级至 AccessToken2
升级 Token 服务器
-
将导入
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" ) -
删除生成时间戳的声明,添加
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) -
更新
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) -
删除不支持的角色:
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。