Go中的JWT认证指南

839 阅读7分钟

JSON Web令牌(JWT)是处理在线认证的一种流行方法,你可以在任何服务器端编程语言中实现JWT认证。

对于阅读JWT的一般背景,我建议通过LogRocket博客上的这些文章了解更多关于JWT、最佳实践以及用JWT保护RESTful API的信息。

本文旨在帮助您在Go Web应用程序中使用golang-jwt 包开始实施JWT认证。

[golang-jwt](https://github.com/golang-jwt/jwt)包是在Go中实现JWT的最流行的包,因为它的功能和使用方便。golang-jwt 包提供了生成和验证JWT的功能。

前提条件

您需要满足这些基本要求,才能从本教程中获得最大收益:

  • 在您的机器上安装 Go 1.16 或更高版本(出于安全考虑)。
  • 有使用 Go 或其他语言构建 Web 应用程序的经验(可选)。

开始使用 Golang-JWT 包

在设置好 Go 工作区并初始化 Go 模块文件go.mod 之后,在工作区目录下的终端上运行此命令以安装golang-jwt 包:

go get github.com/golang-jwt/jwt

一旦你安装了golang-jwt ,创建一个Go文件并导入这些包和模块:

import (
   "log"
    "encoding/json"
   "github.com/golang-jwt/jwt"
   "net/http"
   "time"
)

在本教程中,你将使用这些包来记录错误,建立一个服务器,并设置令牌的过期时间。

在Go中设置一个网络服务器

让我们先创建一个简单的网络服务器,它的端点将用JWT来保护:

func main() {
   http.HandleFunc("/home", handlePage)
   err := http.ListenAndServe(":8080", nil)
   if err != nil {
      log.Println("There was an error listening on port :8080", err)
   }

}

主函数用你要设置的处理函数handlePage 来设置主页端点。handlePage 函数将使用JWTs保护页面。服务器被设置为监听端口:8080 ,但你可以使用你选择的任何端口。

handlePage 处理函数将返回Message 结构的编码 JSON 作为对客户端的响应,如果请求主体被编码后请求被授权:

type Message struct {
        Status string `json:"status"`
        Info   string `json:"info"`
}

func handlePage(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        var message Message
        err := json.NewDecoder(request.Body).Decode(&message)
        if err != nil {
                return
        }
        err = json.NewEncoder(writer).Encode(message)
        if err != nil {
                return
        }
}

handlePage 函数,在这一点上,并没有被认证,向页面发出请求将自由地工作。在本教程的后面,你将学习如何为你的处理函数添加认证。

API network page

使用Golang-JWT 包生成用于认证的JWTs

你将需要一个秘钥来使用golang-jwt 包生成JWT令牌。这里有一个本教程的私钥例子;然而,你应该使用一个加密安全的字符串作为你的秘钥,并从环境变量文件(.env)中加载它。

请查看篇文章,了解如何在你的Go应用程序中使用环境变量:

var sampleSecretKey = []byte("SecretYouShouldHide")

请注意,无论谁拥有你用于JWTs的秘钥,都可以验证你的应用程序的用户。在这种情况下,sampleSecretKey 变量持有私钥。

这里有一个生成JWT令牌的函数。该函数应该返回一个字符串和一个错误。如果在生成JWT时有错误,该函数会返回一个空字符串和错误。如果没有错误,该函数会返回JWT字符串和nil 类型:

func generateJWT() (string, error) {

}

你可以使用JWT包的New 方法创建一个新的令牌。New 方法接收一个签名方法(JWT的加密算法)并返回一个JWT令牌:

token := jwt.New(jwt.SigningMethodEdDSA)

如果你想修改JWT,你可以使用该令牌的Claims 方法:

claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["user"] = "username"

在这种情况下,你要为JWT设置一个过期时间,即十分钟,使用time 模块以及用户名和授权状态。在试图验证JWT时,你就可以检索到索赔。

生成JWT的最后一部分是使用你的秘钥来签署字符串。你可以使用令牌的SignedString 方法签署你的令牌字符串。SignedString 方法接受秘密密钥并返回一个签名的令牌字符串:

tokenString, err := token.SignedString(sampleSecretKey)
if err != nil {
    return "", err
 }

 return tokenString, nil

在签署令牌有错误的情况下,你可以返回一个空字符串和错误。
与cookies不同,你不需要存储JWT;你需要的只是你的签署密钥来验证令牌。

验证JWT令牌

验证JWT的传统方法是使用中间件(处理函数,接收其他处理函数进行操作)。下面是如何使用中间件来验证一个请求是否被授权:

func verifyJWT(endpointHandler func(writer http.ResponseWriter, request *http.Request)) http.HandlerFunc {

}

verifyJWT 函数是一个中间件,它接收了你要验证的请求的处理函数。处理函数使用来自请求头的令牌参数来验证请求,并根据状态做出响应:

 return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {

})

如果请求被授权,verifyJWT 函数会返回作为参数传入的处理函数。

验证JWT的第一步是检查请求头中的令牌:

if request.Header["Token"] != nil {

}

如果有一个令牌,你就可以继续验证令牌和验证索赔。

你必须解析令牌,你可以使用jwt 包的Parse 方法解析令牌。parse 方法接收令牌和一个JWT装饰器函数,并返回一个接口和一个错误。

你需要使用你在生成令牌时用来签名的相同签名方法,使用令牌的Method 方法验证签名。在这个例子中,签名方法是ECDSA方法:

token, err := jwt.Parse(request.Header\["Token"\][0], func(token *jwt.Token) (interface{}, error) {
            _, ok := token.Method.(*jwt.SigningMethodECDSA)
            if !ok {
               writer.WriteHeader(http.StatusUnauthorized)
               _, err := writer.Write([]byte("You're Unauthorized!"))
               if err != nil {
                  return nil, err

               }
            }
            return "", nil

         })

如果签名验证失败(该函数返回!ok ),你可以向客户端返回一个StatusUnauthorized 头:

if err != nil {
               writer.WriteHeader(http.StatusUnauthorized)
               _, err2 := writer.Write([]byte("You're Unauthorized due to error parsing the JWT"))
              if err2 != nil {
                      return
                }
}

在上面的代码中,解析令牌时出现了错误。因此,用户是未授权的,你可以写一条消息并返回一个未授权的状态。

你可以使用令牌的Valid 方法来验证令牌:

if token.Valid {
                      endpointHandler(writer, request)
                        } else {
                                writer.WriteHeader(http.StatusUnauthorized)
                                _, err := writer.Write([]byte("You're Unauthorized due to invalid token"))
                                if err != nil {
                                        return
                                }
}

如果令牌有效,你可以用处理函数的writerrequest 参数传入端点处理程序,以便中间件函数返回端点。

下面是else 语句,用于客户端请求的头中没有令牌的情况:

else {
          writer.WriteHeader(http.StatusUnauthorized)
          _, err := writer.Write([]byte("You're Unauthorized due to No token in the header"))
           if err != nil {
               return
           }
}

由于你使用的是中间件,你的路由声明中的处理函数将是verifyJWT 中间件,并以路由的处理函数作为参数:

http.HandleFunc("/home", verifyJWT(handlePage))

一旦你把你的验证函数添加到路由中,端点就被验证了:

Endpoint authenticated

在客户端,客户端必须提供一个已发布的令牌。这里有一个函数,使用generateJWT 函数在请求中添加令牌:

func authPage(writer http.ResponseWriter, ) {
        token, err := generateJWT()
        if err != nil {
                        return
        } 
        client := &http.Client{}
        request, _ := http.NewRequest("POST", "<http://localhost:8080/>", nil)
        request.Header.Set("Token", token)
        _, _ = client.Do(request)

}

authPage 函数中,token 变量持有来自generateJWT 函数的令牌。使用对http 包的Client 类型的引用,你可以创建一个新的客户端并向端点发出请求。request 变量是请求实例,并且--使用请求实例的Set 方法的header 方法--你可以在请求头中设置令牌,如上所示。

你也可以选择将令牌设置为cookie,并在客户端向认证的端点发出请求时检索它进行验证。

从JWT令牌中提取索赔

当你在生成JWT时,你可以选择在令牌中嵌入信息。在generateJWT 函数中,你将username 变量添加到claims 地图中。

下面是你如何提取索赔,以username 的索赔为例。在验证令牌签名时,你可以使用中间件或将该功能添加到你的验证函数中:

func extractClaims(_ http.ResponseWriter, request *http.Request) (string, error) {
        if request.Header["Token"] != nil {
                tokenString := request.Header\["Token"\][0]
                token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {

          if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
                return nil, fmt.Errorf("there's an error with the signing method")
          }
                return sampleSecretKey, nil

            })

            if err != nil {
                        return "Error Parsing Token: ", err
                }
}

extractClaims 功能中,其过程与verifyJWT 功能相同;你从头中检索令牌,解析令牌,并验证签名:

claims, ok := token.Claims.(jwt.MapClaims)
          if ok && token.Valid {
                username := claims["username"].(string)
                return username, nil
          }

        }
        return "unable to extract claims", nil

在验证令牌时,你可以使用Claims 方法检索索赔,并使用索赔映射来检索JWT中的数据,如上图所示。

总结

本教程教你如何使用JWT认证,通过使用golang-jwt 包,用JSON Web Tokens在Go中认证你的API和网页端点。你可以在Github Gist上找到本教程的完整代码。

记住,要使用环境变量作为你的秘钥,不要在JWT中隐藏敏感数据。LogRocket博客上有许多JWT教程,你可以查看,以开始使用你感兴趣的语言或框架