如何用 GoLang 为声网Agora 应用构建 Token 服务器

目前,视频聊天应用的安全性是当下的热门话题,随着远程办公和线上活动变得越来越多,人们对安全的要求也随之提高。

在声网Agora 平台内,有一层安保是 Token(令牌)认证。Token 是用一组给定的输入值生成的动态密钥,声网Agora 平台使用 Token 对用户进行身份验证。

声网Agora 为 RTC 和 RTM SDK 提供 Token 鉴权,本指南将告诉大家如何用 GolangGin 框架来构建简单的微服务,从而生成声网Agora RTC 和 RTM Token。

前期准备

  • 了解 Golang 的基础知识

  • 了解网络服务器的功能

  • 一个声网Agora 开发者账号(详见声网注册指南)

项目设置

首先,打开终端,为项目创建一个新文件夹,并使用 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 端点并将其设置成在 localhost8080 端口上进行侦听和服务。对于简单的端点,我们会把它设置为接收请求上下文,并返回一个带有 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 之前,我们需要添加 AppIDAppCertificate。我们还要在 global 范围内将 appIDAppCertificate 声明为字符串。本指南使用环境变量来存储项目凭证,所以我们要对它们进行检索。我们在 main() 中使用 os.LookupEnv 来检索环境变量,os.LookupEnv 会为环境变量返回一个字符串以及一个用来标记变量是否存在的布尔值。我们将使用返回的布尔值来检查环境配置是否正确。如果正确,我们就可以将环境变量值分别分配给我们的全局 appIDAppCertificate 变量。

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)

为了尽量减少重复代码的数量,三个函数 getRtcTokengetRtmTokengetBothTokens 将调用单独的函数( 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 来调用 parseRtcParamsparseRtcParams 可以提取请求的值,然后使用返回的值调用 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 类型( uiduserAccount )、角色和到期时间。

该函数使用这些值来调用适合的 声网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/

推荐阅读
相关专栏
SDK 教程
164 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。