本文介绍如何使用 AccessToken 鉴权以及如何升级至 AccessToken2。
下图展示了鉴权的基本流程:
Token 在 app 服务器上生成,其最大有效期为 24 小时。当用户从你的 app 客户端连接至 Agora 频道时,Agora 平台会读取该 Token 中包含的信息,并进行校验。Token 包含以下信息:
开始前,请确保你的项目或使用的声网产品满足如下条件:
一个有效的声网账户。
已开启 App 证书的声网项目。
Golang 1.14 或以上版本,GO111MODULE 设置为开启。
本节介绍如何使用声网提供的代码生成并提供 Token,对用户及其权限进行校验。
本节介绍如何获取生成 Token 所需的安全信息,如你的项目的 App ID 及 App 证书。
声网会给每个项目自动分配一个 App ID 作为项目唯一标识。
在声网控制台的项目管理页面,找到你的项目,点击 App ID 右侧的 图标,即可获取项目的 App ID。
参考以下步骤获取 App 证书:
在声网控制台的项目管理页面,找到你的项目,点击配置。
点击主要证书下面的复制图标,即可获取项目的 App 证书。
Token Generator 创建客户端 app 请求的 token,从而实现对声网平台的安全访问。 为了提供 token,你需要在安全基础设施中部署一个 Token Generator。
为了展示鉴权的工作流程,本节介绍如何使用 Golang 在你的本地设备上搭建并运行一个 Token 服务器。
创建一个 server.go
文件,然后贴入如下代码。将其中的 <Your App ID>
和 <Your App Certificate>
替换为你的 App ID 和 App 证书。
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 mod init sampleServer
运行如下命令行安装依赖。你可以使用 Go 镜像进行加速,例如 https://goproxy.cn/ 。
$ go get
运行如下命令行启动服务器:
$ go run server.go
本节以 iOS 客户端为例,展示如何使用 Token 对客户端的用户进行鉴权。
为了展示鉴权的工作流程,本节介绍如何在你的本地开发环境上使用 iOS 模拟器搭建并运行一个 iOS 客户端。
将 ViewController.swift
中的内容替换为如下代码。 将 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 会触发 tokenPrivilegeWillExpire
回调。收到该回调后,客户端需要从服务器获取新的 Token 并调用 renewToken
将新生成的 Token 传给 SDK。
Token 过期时,SDK 会触发 rtcEngineRequestToken
回调。收到该回调后,客户端需要从服务器获取新的 Token 并调用 joinChannel
方法,再使用新的 Token 重新加入频道。
//
// ViewController.swift
// RteQuickstart
//
// Created by macoscatalina on 2021/8/12.
// Copyright © 2021 macoscatalina. All rights reserved.
//
import UIKit
import AgoraRtcKit
import Foundation
public enum TokenError: Error{
case noData
case invalidData
}
class ViewController: UIViewController {
var localView: UIView!
var remoteView: UIView!
var agoraKit: AgoraRtcEngineKit!
override func viewDidLoad() {
super.viewDidLoad()
// 加载完视图后,可进行其他设置
initView()
initializeAgoraEngine()
setClientRole()
setupLocalVideo()
fetchToken(channelName: "test", userId: 1234, role: 1){ result in
switch result {
case .success(let token):
print("token is: \(token)")
self.joinChannel(token: token)
case .failure(let err):
print("Could not fetch token: \(err)")
}
}
}
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
remoteView.frame = self.view.bounds
localView.frame = CGRect(x: self.view.bounds.width - 90, y: 0, width: 90, height: 160)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(true)
leaveChannel()
destroy()
}
func initView(){
remoteView = UIView()
self.view.addSubview(remoteView)
localView = UIView()
self.view.addSubview(localView)
}
func initializeAgoraEngine(){
let config = AgoraRtcEngineConfig()
config.appId = "Your App ID"
config.channelProfile = .liveBroadcasting
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
if agoraKit != nil{
print("Initialization successful")
}
else{
print("Initialization failed")
}
}
func setClientRole(){
agoraKit.setClientRole(.broadcaster)
}
func setupLocalVideo(){
agoraKit.enableVideo()
agoraKit.startPreview()
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = 0
videoCanvas.renderMode = .hidden
videoCanvas.view = localView
agoraKit.setupLocalVideo(videoCanvas)
}
func joinChannel(token:String){
let option = AgoraRtcChannelMediaOptions()
agoraKit.joinChannel(byToken: token, channelId: "test", uid: 123456, mediaOptions: option)
}
func leaveChannel(){
agoraKit.stopPreview()
agoraKit.leaveChannel(nil)
}
func destroy(){
AgoraRtcEngineKit.destroy()
}
func fetchToken(channelName: String, userId: UInt, role: UInt,
callback: @escaping (Result<String, Error>) -> Void
){
let url = URL(string: "http://<Your Host URL and port>/fetch_rtc_token")
let parameters = ["uid":userId,"channelName": channelName, "role": role] as [String : Any]
print(parameters.self)
var request = URLRequest(
url: url!,
timeoutInterval: 10
)
request.httpMethod = "POST"
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
}
catch let error {
print(error.localizedDescription)
}
URLSession.shared.dataTask(with: request){data, _, err in
guard let data = data else {
if let err = err {
callback(.failure(err))
}
else {
callback(.failure(TokenError.noData))
}
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseDict = responseJSON as? [String: Any], let token = responseDict["token"] as? String {
callback(.success(token))
} else {
callback(.failure(TokenError.invalidData))
}
}.resume()
}
}
extension ViewController: AgoraRtcEngineDelegate{
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int){
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
videoCanvas.renderMode = .hidden
videoCanvas.view = remoteView
agoraKit.setupRemoteVideo(videoCanvas)
}
func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
self.fetchToken(channelName: "test", userId: 1234, role: 1){ result in
switch result {
case .success(let token):
print("token is: \(token)")
self.agoraKit.renewToken(token)
print("Renewed the token")
case .failure(let err):
print("Could not fetch token: \(err)")
}
}
}
func rtcEngine(_ engine: AgoraRtcEngineKit, connectionStateChanged state: AgoraConnectionState, reason: AgoraConnectionChangedReason) {
print("Connection state changed to")
print(state.rawValue)
}
func rtcEngineRequestToken(_ engine: AgoraRtcEngineKit) {
fetchToken(channelName: "test", userId: 1234, role: 1){ result in
switch result {
case .success(let token):
print("token is: \(token)")
self.joinChannel(token: token)
case .failure(let err):
print("Could not fetch token: \(err)")
}
}
}
}
在本地设备的 iOS 模拟器中构建并运行项目,app 会执行如下操作:
本节介绍 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 |
本节介绍生成 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 小时。 |
将导入 rtctokenbuilder
声明替换为以下代码,并且删除导入 "time" 的声明:
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
相关代码:
// expireTimeInSeconds := uint32(40)
// 获取当前时间戳
// currentTimestamp := uint32(time.Now().UTC().Unix())
// Timestamp when the token expires.
// expireTimestamp := currentTimestamp + expireTimeInSeconds
tokenExpireTimeInSeconds := uint32(40)
privilegeExpireTimeInSeconds := uint32(40)
更新 tokenbuilder
方法代码:
// 更新前代码
// 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)
删除不支持的角色:
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 服务器是否正常运行,在本地设备的 iOS 模拟器中构建并运行项目,app 会执行如下操作: