目前,视频聊天应用的安全性是当下的热门话题,随着远程办公和线上活动变得越来越多,人们对安全的要求也随之提高。
在声网Agora 平台内,有一层安保是 Token(令牌)认证。Token 是用一组给定的输入值生成的动态密钥,声网Agora 平台使用 Token 对用户进行身份验证。
声网Agora 为 RTC 和 RTM SDK 提供 Token 鉴权,本指南将告诉大家如何用 Golang 和 Gin 框架来构建简单的微服务,从而生成声网Agora RTC 和 RTM Token。
前期准备
项目设置
首先,打开终端,为项目创建一个新文件夹,并使用 cd
命令进入文件夹。
mkdir agora-token-server
cd agora-token-server
项目创建好了,现在我们来初始化项目的 Go 模块。
go mod init agora-token-server
最后,我们用 go get
添加 Gin 和 Agora 依赖项。
go get github.com/gin-gonic/gin
go get github.com/AgoraIO-Community/go-tokenbuilder
构建 Gin Web 服务器
项目创建完成,现在大家可以用自己喜欢的代码编辑器打开文件夹,并创建一个 main.go
文件。
在 main.go
文件中,我们先声明程序包并添加 main
函数。
package main
func main() {
}
然后,我们导入 Gin 框架,创建 Gin app,设置一个简单的 GET
端点并将其设置成在 localhost
的 8080
端口上进行侦听和服务。对于简单的端点,我们会把它设置为接收请求上下文,并返回一个带有 200
状态标头的 JSON 响应。
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
api := gin.Default()
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.Run(":8080") // listen and serve on localhost:8080
}
准备测试服务器,返回终端窗口并运行:
go run main.go
为测试端点,请打开网页浏览器并访问:
localhost:8080/ping
你会看到服务器在正常运行:
{"message":"pong"}
确认端点正常运行后,返回终端窗口,点击 ctrl c
终止运行:
生成 Agora Token
既然 Gin 服务器构建完了,那我们就可以添加功能来生成 RTC 和 RTM token 了。
在生成 token 之前,我们需要添加 AppID
和 AppCertificate
。我们还要在 global
范围内将 appID
和 AppCertificate
声明为字符串。本指南使用环境变量来存储项目凭证,所以我们要对它们进行检索。我们在 main()
中使用 os.LookupEnv
来检索环境变量,os.LookupEnv
会为环境变量返回一个字符串以及一个用来标记变量是否存在的布尔值。我们将使用返回的布尔值来检查环境配置是否正确。如果正确,我们就可以将环境变量值分别分配给我们的全局 appID
和 AppCertificate
变量。
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
)
var appID, appCertificate string
func main() {
appIDEnv, appIDExists := os.LookupEnv("APP_ID")
appCertEnv, appCertExists := os.LookupEnv("APP_CERTIFICATE")
if !appIDExists || !appCertExists {
log.Fatal("FATAL ERROR: ENV not properly configured, check APP_ID and APP_CERTIFICATE")
} else {
appID = appIDEnv
appCertificate = appCertEnv
}
api := gin.Default()
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.Run(":8080")
}
接下来我们将添加 3 个端点,一个用于 RTC token,一个用于 RTM token,一个用来返回以上两个 token。
RTC token 需要一个频道名、UID、用户角色和 tokentype 类型,来区分基于字符串和整数的 UID,以及有效时间。RTM 端点仅需要一个 UID 和一个有效时间。能同时生成两个 Token 的端点需接受与 RTC token 端点相同的结构。
api.GET("rtc/:channelName/:role/:tokentype/:uid/", getRtcToken)
api.GET("rtm/:uid/", getRtmToken)
api.GET("rte/:channelName/:role/:tokentype/:uid/", getBothTokens)
为了尽量减少重复代码的数量,三个函数 getRtcToken
, getRtmToken
和 getBothTokens
将调用单独的函数( parseRtcParams
/ parseRtmParams
)来验证并提取传递给每个端点的值。然后,每个函数用返回的值来生成 token,并把它们作为响应 body
中的 JSON 返回。
可以使用两种类型的 UID( uint
/ string
)生成 RTC token,因此我们用一个函数( generateRtcToken
)来封装 声网Agora RTC Token 生成器 的函数 BuildTokenWithUserAccount
/ BuildTokenWithUID
。
以下是我们的 token 服务器的基础模板,我们将浏览每个函数并填写空白。
package main
import (
"log"
"os"
"github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder"
"github.com/gin-gonic/gin"
)
var appID, appCertificate string
func main() {
appIDEnv, appIDExists := os.LookupEnv("APP_ID")
appCertEnv, appCertExists := os.LookupEnv("APP_CERTIFICATE")
if !appIDExists || !appCertExists {
log.Fatal("FATAL ERROR: ENV not properly configured, check APP_ID and APP_CERTIFICATE")
} else {
appID = appIDEnv
appCertificate = appCertEnv
}
api := gin.Default()
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.GET("rtc/:channelName/:role/:tokenType/:uid/", getRtcToken)
api.GET("rtm/:uid/", getRtmToken)
api.GET("rte/:channelName/:role/:tokenType/:uid/", getBothTokens)
api.Run(":8080")
}
func getRtcToken(c *gin.Context) {
}
func getRtmToken(c *gin.Context) {
}
func getBothTokens(c *gin.Context) {
}
func parseRtcParams(c *gin.Context) (channelName, tokentype, uidStr string, role rtctokenbuilder.Role, expireTimestamp uint32, err error) {
}
func parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp uint32, err error) {
}
func generateRtcToken(channelName, uidStr, tokentype string, role rtctokenbuilder.Role, expireTimestamp uint32) (rtcToken string, err error) {
}
构建 RTC Token
我们从 getRtcToken
开始。此函数引用了 gin.Context
,用 gin.Context
来调用 parseRtcParams
,parseRtcParams
可以提取请求的值,然后使用返回的值调用 generateRtcToken
以生成 token 字符串 。另外,要有检查错误的环节,确保过程中没有任何问题。最后,我们将建立响应。
func getRtcToken(c *gin.Context) {
log.Printf("rtc token\n")
// get param values
channelName, tokentype, uidStr, role, expireTimestamp, err := parseRtcParams(c)
if err != nil {
c.Error(err)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + err.Error(),
"status": 400,
})
return
}
rtcToken, tokenErr := generateRtcToken(channelName, uidStr, tokentype, role, expireTimestamp)
if tokenErr != nil {
log.Println(tokenErr) // token failed to generate
c.Error(tokenErr)
errMsg := "Error Generating RTC token - " + tokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"status": 400,
"error": errMsg,
})
} else {
log.Println("RTC Token generated")
c.JSON(200, gin.H{
"rtcToken": rtcToken,
})
}
}
接下来,填写 parseRtcParams
。此函数还引用了 gin.Context
,我们将用 gin.Context
来提取并返回参数。大家会注意到 parseRtcParams
也会返回 error
,如果我们遇到问题,可以返回错误信息。
func parseRtcParams(c *gin.Context) (channelName, tokentype, uidStr string, role rtctokenbuilder.Role, expireTimestamp uint32, err error) {
// get param values
channelName = c.Param("channelName")
roleStr := c.Param("role")
tokentype = c.Param("tokentype")
uidStr = c.Param("uid")
expireTime := c.DefaultQuery("expiry", "3600")
if roleStr == "publisher" {
role = rtctokenbuilder.RolePublisher
} else {
role = rtctokenbuilder.RoleSubscriber
}
expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64)
if parseErr != nil {
// if string conversion fails return an error
err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr)
}
// set timestamps
expireTimeInSeconds := uint32(expireTime64)
currentTimestamp := uint32(time.Now().UTC().Unix())
expireTimestamp = currentTimestamp + expireTimeInSeconds
return channelName, tokentype, uidStr, role, expireTimestamp, err
}
最后,填写 generateRtcToken
函数,此函数使用频道名称和 UID 作为字符串、token 类型( uid
或 userAccount
)、角色和到期时间。
该函数使用这些值来调用适合的 声网Agora RTC Token 生成器 函数( BuildTokenWithUserAccount
/ BuildTokenWithUID
)来生成 token 字符串。当 token 生成器函数返回时,我们要先查看有没有错误,确保没有错误后才能返回 token 字符串值。
func generateRtcToken(channelName, uidStr, tokentype string, role rtctokenbuilder.Role, expireTimestamp uint32) (rtcToken string, err error) {
if tokentype == "userAccount" {
log.Printf("Building Token with userAccount: %s\n", uidStr)
rtcToken, err = rtctokenbuilder.BuildTokenWithUserAccount(appID, appCertificate, channelName, uidStr, role, expireTimestamp)
return rtcToken, err
} else if tokentype == "uid" {
uid64, parseErr := strconv.ParseUint(uidStr, 10, 64)
// check if conversion fails
if parseErr != nil {
err = fmt.Errorf("failed to parse uidStr: %s, to uint causing error: %s", uidStr, parseErr)
return "", err
}
uid := uint32(uid64) // convert uid from uint64 to uint 32
log.Printf("Building Token with uid: %d\n", uid)
rtcToken, err = rtctokenbuilder.BuildTokenWithUID(appID, appCertificate, channelName, uid, role, expireTimestamp)
return rtcToken, err
} else {
err = fmt.Errorf("failed to generate RTC token for Unknown Tokentype: %s", tokentype)
log.Println(err)
return "", err
}
}
构建 RTM Token
接下来,我们来构建 getRtmToken
函数。与上述代码一样, getRtmToken
引用了 gin.Context
,用 gin.Context
来调用 parseRtmParams
以提取请求的值,并使用返回的值来生成 RTM token。这里的区别在于,我们是直接调用 Agora RTM token 构建器来生成 token String,确认无误后再构建响应。
func getRtmToken(c *gin.Context) {
log.Printf("rtm token\n")
// get param values
uidStr, expireTimestamp, err := parseRtmParams(c)
if err != nil {
c.Error(err)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + err.Error(),
"status": 400,
})
return
}
rtmToken, tokenErr := rtmtokenbuilder.BuildToken(appID, appCertificate, uidStr, rtmtokenbuilder.RoleRtmUser, expireTimestamp)
if tokenErr != nil {
log.Println(err) // token failed to generate
c.Error(err)
errMsg := "Error Generating RTM token: " + tokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"error": errMsg,
"status": 400,
})
} else {
log.Println("RTM Token generated")
c.JSON(200, gin.H{
"rtmToken": rtmToken,
})
}
}
然后,填写 parseRtmParams
。这个函数也引用了 gin.Context
,然后提取并返回参数。
func parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp uint32, err error) {
// get param values
uidStr = c.Param("uid")
expireTime := c.DefaultQuery("expiry", "3600")
expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64)
if parseErr != nil {
// if string conversion fails return an error
err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr)
}
// set timestamps
expireTimeInSeconds := uint32(expireTime64)
currentTimestamp := uint32(time.Now().UTC().Unix())
expireTimestamp = currentTimestamp + expireTimeInSeconds
// check if string conversion fails
return uidStr, expireTimestamp, err
}
同时构建 RTC 和 RTM Token
现在,我们可以使用私人服务器请求生成 RTC 和 RTM token。我们将填写 getBothTokens
并允许从单个请求中生成两个 token。我们将使用与 getRtcToken
相似的代码,区别在于这次包含 RTM token。
func getBothTokens(c *gin.Context) {
log.Printf("dual token\n")
// get rtc param values
channelName, tokentype, uidStr, role, expireTimestamp, rtcParamErr := parseRtcParams(c)
if rtcParamErr != nil {
c.Error(rtcParamErr)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + rtcParamErr.Error(),
"status": 400,
})
return
}
// generate the rtcToken
rtcToken, rtcTokenErr := generateRtcToken(channelName, uidStr, tokentype, role, expireTimestamp)
// generate rtmToken
rtmToken, rtmTokenErr := rtmtokenbuilder.BuildToken(appID, appCertificate, uidStr, rtmtokenbuilder.RoleRtmUser, expireTimestamp)
if rtcTokenErr != nil {
log.Println(rtcTokenErr) // token failed to generate
c.Error(rtcTokenErr)
errMsg := "Error Generating RTC token - " + rtcTokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"status": 400,
"error": errMsg,
})
} else if rtmTokenErr != nil {
log.Println(rtmTokenErr) // token failed to generate
c.Error(rtmTokenErr)
errMsg := "Error Generating RTC token - " + rtmTokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"status": 400,
"error": errMsg,
})
} else {
log.Println("RTC Token generated")
c.JSON(200, gin.H{
"rtcToken": rtcToken,
"rtmToken": rtmToken,
})
}
}
测试 Token 服务器
返回终端窗口并运行 token 服务器。
run main.go
服务器实例运行后,我们将看到端点列表和消息:Listening and serving HTTP on :8080
。
现在我们的服务器实例正在运行,请大家打开 Web 浏览器并进行测试。我们在测试中将尝试一些省略各种查询参数的变化。
测试 Token 服务器
我们从 RTC token 开始:
http://localhost:8080/rtc/testing/publisher/userAccount/1234/
http://localhost:8080/rtc/testing/publisher/uid/1234/
端点能生成可在频道中使用的 token:由角色为 publisher
,UID(字符串或单位)为 1234
的用户进行 testing
。
{
"rtcToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIADJRwbbO8J93uIDi4J305xNXA0A+pVDTPLPavzwsLW3uAZa8+ij4OObIgDqFTEDoOMyXwQAAQAwoDFfAgAwoDFfAwAwoDFfBAAwoDFf"
}
大家可以使用 Agora 1:1 Web Demo来测试这个 token。
测试双 Token 端点
我们将使用双 Token 端点进行测试:
http://localhost:8080/rte/testing/publisher/userAccount/1234/
http://localhost:8080/rte/testing/publisher/uid/1234/
端点将同时生成 RTC 和 RTM token 供 UID(字符串或单位) 为 1234
的用户使用,且同样适用于视频频道:以 publisher
角色进行 testing
。
{
"rtcToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIAD33wY6pO+xp6iBY8mbYz2YtOIiRoTTrzdIPF9DEFlSIwZa8+ij4OObIgAQ6e0EX+UyXwQAAQDvoTFfAgDvoTFfAwDvoTFfBADvoTFf",
"rtmToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIABbCwQgl2te3rk0MEDZ2xrPoalb37fFhTqmTIbGeWErWaPg45sAAAAAEAD1WwYBX+UyXwEA6APvoTFf"
}
我们可以用 RTC token 的 Agora 1 对 1 通话 Web 演示 和 RTM token 的 Agora RTM 教程演示 来测试 token。
测试完端点后,你的终端窗口会显示所有请求。
完成啦!
感谢大家抽出宝贵的时间阅读我的教程,如有任何疑问,可以在下方发表评论,如果大家发现任何改进的空间,欢迎随时创建拉取请求!
原文作者:Hermes Frangoudis
原文链接:https://www.agora.io/en/blog/how-to-build-a-token-server-using-golang/