前后端分離必備, Golang Gin中如何使用JWT(JsonWebToken)中間件?
什么是JWT?
JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案,也是目前前后端分離項目中普遍使用的認證技術(shù). 本文介紹如何在Golang Gin Web框架中使用JWT認證中間件以及模擬測試, 以供參考, 關(guān)于JWT詳細原理可以參考:
- JWT RFC: https://tools.ietf.org/html/rfc7519
- JWT IETF: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
- JSON Web Token入門教程: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
主要流程
- 初始化Gin引擎
- 定義獲取Token的接口, 訪問該接口, 內(nèi)部自動生成JWT令牌, 并返回給前端
- 定義需要認證的路由接口, 使用JWT中間件進行認證, 中間件由
- 利用GoConvey(Golang的測試框架,集成go test, 支持終端和瀏覽器模式), 構(gòu)造客戶端, 填寫Token, 模擬前端訪問
- JWT中間件進行認證, 認證通過則返回消息體, 否則直接返回401或其他錯誤
流程圖

該流程圖描述了服務(wù)端代碼中的Token構(gòu)造, 以及認證流程.
服務(wù)端代碼
main.go中填充以下代碼, 運行g(shù)o run main.go, 開啟Web服務(wù).
- package main
- import (
- jwt_lib "github.com/dgrijalva/jwt-go"
- "github.com/dgrijalva/jwt-go/request"
- "github.com/gin-gonic/gin"
- "log"
- "time"
- )
- var (
- mysupersecretpassword = "unicornsAreAwesome"
- )
- func Auth(secret string) gin.HandlerFunc {
- return func(c *gin.Context) {
- //log.Printf("Request:\n%+v", c.Request)
- // ParseFromRequest方法提取路徑請求中的JWT令牌, 并進行驗證
- token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt_lib.Token) (interface{}, error) {
- b := ([]byte(secret))
- //log.Printf("b:%+v", b)
- return b, nil
- })
- log.Printf("token:%+v", token)
- if err != nil {
- c.AbortWithError(401, err)
- }
- }
- }
- func main() {
- r := gin.Default()
- public := r.Group("/api")
- // 定義根路由, 訪問http://locahost:8080/api/可以獲取到token
- public.GET("/", func(c *gin.Context) {
- // Create the token New方法接受一個簽名方法的接口類型(SigningMethod)參數(shù), 返回一個Token結(jié)構(gòu)指針
- // GetSigningMethod(簽名算法algorithm)
- token := jwt_lib.New(jwt_lib.GetSigningMethod("HS256")) //默認是簽名算法是HMAC SHA256(寫成 HS256)
- log.Printf("token:%+v", token)
- //2020/12/10 22:32:02 token:&{Raw: Method:0xc00000e2a0 Header:map[alg:HS256 typ:JWT] Claims:map[] Signature: Valid:false}
- // Set some claims 設(shè)置Id和過期時間字段, MapClaims實現(xiàn)了Clainms接口
- token.Claims = jwt_lib.MapClaims{
- "Id": "Christopher",
- "exp": time.Now().Add(time.Hour * 1).Unix(),
- }
- // Sign and get the complete encoded token as a string // 簽名并得到完整編碼后的Token字符串
- tokenString, err := token.SignedString([]byte(mysupersecretpassword))
- //{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJZCI6IkNocmlzdG9waGVyIiwiZXhwIjoxNjA3NjE0MzIyfQ.eQd7ztDn3706GrpitgnikKgOtzx-RHnq7cr2eqUlsZo"}
- if err != nil {
- c.JSON(500, gin.H{"message": "Could not generate token"})
- }
- c.JSON(200, gin.H{"token": tokenString})
- })
- // 定義需要Token驗證通過才能訪問的私有接口組http://localhost:8080/api/private
- private := r.Group("/api/private")
- private.Use(Auth(mysupersecretpassword)) // 使用JWT認證中間件(帶參數(shù))
- /*
- Set this header in your request to get here.
- Authorization: Bearer `token`
- */
- // 定義具體的私有根接口:http://localhost:8080/api/private/
- private.GET("/", func(c *gin.Context) {
- c.JSON(200, gin.H{"message": "Hello from private"})
- })
- r.Run("localhost:8080")
- }
客戶端代碼
新建jwt_test.go文件, 填充以下代碼, 運行g(shù)o test執(zhí)行單元測試.
- package test_test
- import (
- "encoding/json"
- . "github.com/smartystreets/goconvey/convey" //https://github.com/smartystreets/goconvey GoConvey是Golang的測試框架,集成go test, 支持終端和瀏覽器模式.
- "io/ioutil"
- "log"
- "net/http"
- "strings"
- "testing"
- )
- type User struct {
- Username string `json:"username"`
- Password string `json:"password"`
- }
- type Response struct {
- Token string `json:"token"`
- }
- func createNewsUser(username, password string) *User {
- return &User{username, password}
- }
- func TestLogin(t *testing.T) {
- Convey("Should be able to login", t, func() {
- user := createNewsUser("jonas", "1234")
- jsondata, _ := json.Marshal(user)
- userData := strings.NewReader(string(jsondata))
- log.Printf("userData:%+v", userData)
- // 這里模擬用戶登錄, 實際上后臺沒有使用用戶名和密碼, 該接口直接返回內(nèi)部生成的Token
- req, _ := http.NewRequest("GET", "http://localhost:8080/api/", userData)
- req.Header.Set("Content-Type", "application/json")
- client := &http.Client{}
- res, _ := client.Do(req)
- //log.Printf("res:%+v", res)
- So(res.StatusCode, ShouldEqual, 200) //對響應(yīng)碼進行斷言, 期望得到狀態(tài)碼為200
- Convey("Should be able to parse body", func() { //解析響應(yīng)體
- body, err := ioutil.ReadAll(res.Body)
- defer res.Body.Close()
- So(err, ShouldBeNil)
- Convey("Should be able to get json back", func() {
- responseData := new(Response)
- err := json.Unmarshal(body, responseData)
- So(err, ShouldBeNil)
- log.Printf("responseData:%s", responseData)
- Convey("Should be able to be authorized", func() {
- token := responseData.Token //提取Token
- log.Printf("token:%s", token)
- // 構(gòu)造帶Token的請求
- req, _ := http.NewRequest("GET", "http://localhost:8080/api/private", nil)
- req.Header.Set("Authorization", "Bearer "+token) //設(shè)置認證頭
- client = &http.Client{}
- res, _ := client.Do(req)
- body, err := ioutil.ReadAll(res.Body)
- if err != nil {
- log.Printf("Read body failed, %s", err.Error())
- }
- log.Printf("Body:%s", string(body))
- So(res.StatusCode, ShouldEqual, 200)
- })
- })
- })
- })
- }
參考文檔
gin-gonic/contrib/jwt中間件: https://github.com/gin-gonic/contrib/tree/master/jwt