如何用GoLang为Agora App构建Token服务器


目前,视频聊天App中的安全性是一个热门话题。 随着远程办公和线上活动变得越来越多,人们对安全性的需求也就多了。

在Agora平台内,有一层安保是以Token认证形式出现的。 而Token是使用一组给定的输入值而生成的动态密钥。 Agora平台使用Token对用户进行身份验证。

Agora为其RTC和RTM SDK提供Token鉴权。 本指南将说明如何用GolangGin构建简单的微服务来生成Agora RTC和RTM Tokens。

要求

  • 掌握 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 端点并将其设置成在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 Tokens

既然Gin 服务器构建完了,那我们就可以添加功能生成RTC和RTM tokens了.

在生成tokens之前,我们需要添加 AppIDAppCertificate .我们还在 global 范围 内将 appIDAppCertificate 定义为字符串。对于本指南,我们需 要使用环境变量来存储项目凭证,因此我们还需要对它们进行检索。 main() 中我们使用 os.LookupEnv 检索环境变量。 os.LookupEnv 返回一个环境变量的字符串,并且还有一个布尔值标记变量是否存在。我们将使用后一个返回值来检查环境配置是否正确。如果正确,那我们就可以将环境变量值分别分配给我们的全局 appIDAppC ertificate 变量。

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 )验证并提取传给每个端点的值。然后,每个函数用返回的值来生成tokens,把它们作为响应主体中的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.Co ntext 有引用作用还能调用 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,我们将用它来提取参数并返回它们。你会注意到 parseRtcParams 也会返回一个错误,以防我们遇到任何问题,我们可以返回一个错误信息。

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令牌

接下来,我们需要继续构建 getRtmToken 函数。像上面介绍的一样, getRtmToken 引用 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 Tokens

现在,我们能够使用私人服务器请求生成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服务器。

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:由具有 publisher1234 中UID( 字符串或uint )角色的用户进行测试。

{
“rtcToken”: “0062ec0d84c41c4442d88ba6f5a2beb828bIADJRwbbO8J93uIDi4J305xNXA0A+pVDTPLPavzwsLW3uAZa8+ij4OObIgDqFTEDoOMyXwQAAQAwoDFfAgAwoDFfAwAwoDFfBAAwoDFf”
}

要测试这个token,我们可以使用 Agora 1:1 Web Demo.

测试Dual Token 端点

我们将使用Dual Token 端点完成测试:

http://localhost:8080/rte/testing/publisher/userAccount/1234/ http://localhost:8080/rte/testing/publisher/uid/1234/

端点将同时生成RTC和RTM token供UID为1234(字符串或unit皆可)的用户使用,对于视频频道,使用publisher的角色来测试,且同样适用于视频频道:以publisher角色进行测试。

{ “rtcToken”: “0062ec0d84c41c4442d88ba6f5a2beb828bIAD33wY6pO+xp6iBY8mbYz2YtOIiRoTTrzdIPF9DEFlSIwZa8+ij4OObIgAQ6e0EX+UyXwQAAQDvoTFfAgDvoTFfAwDvoTFfBADvoTFf”, “rtmToken”: “0062ec0d84c41c4442d88ba6f5a2beb828bIABbCwQgl2te3rk0MEDZ2xrPoalb37fFhTqmTIbGeWErWaPg45sAAAAAEAD1WwYBX+UyXwEA6APvoTFf” }

要测试token,我们可以将Agora 1对1通话 Web演示 用于RTCtoken,将Agora RTM教程演示 用于RTM token。

测试完端点后,你的终端窗口将显示所有请求。

完成任务!


感谢你抽出宝贵的时间阅读我的教程,如有任何疑问,可发表评论。如发现有任何改进的空间,可随时提出请求!

作者 Hermes Frangoudis
原文链接 https://www.agora.io/en/blog/how-to-build-a-token-server-using-golang/