基於gin的golang web開發:認證利器jwt

JSON Web Token(JWT)是一種很流行的跨域認證解決方案,JWT基於JSON能夠在進行驗證的同時附帶身份信息,對於先後端分離項目頗有幫助。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT由三部分組成,每一個部分之間用點.隔開,分別稱爲HEADER、PAYLOAD和VERIFY SIGNATURE。HEADER和PAYLOAD通過base64解碼後爲JSON明文。git

  1. HEADER包含兩個字段,alg指明JWT的簽名算法,typ固定爲JWT
  2. PAYLOAD中包含JWT的聲明信息,標準中定義了isssubaud等聲明字段,若是標準聲明不夠用的話,咱們還能夠增長自定義聲明。要注意兩點,第一PAYLOAD只是通過base64編碼,幾乎就等因而明文,不要包含敏感信息。第二不要在PAYLOAD中放入過多的信息,由於驗證經過之後每個請求都要包含JWT,信息太多的話會形成一些沒有必要的資源浪費。
  3. VERIFY SIGNATURE爲使用HEADER中指定的算法生成的簽名。例如alg:HS256簽名算法HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),密鑰)

瞭解完JWT的基本原理以後,咱們來看一下在gin中是怎麼使用JWT的。github

引入gin-jwt中間件

在Gin中使用jwt有個開源項目gin-jwt,這項目幾乎包含了咱們要用到的一切。例如定義PAYLOAD中的聲明、受權驗證的方法、是否使用COOKIE等等。下面來看一下官網給出的例子。golang

package main

import (
	"log"
	"net/http"
	"os"
	"time"

	jwt "github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
)

type login struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

var identityKey = "id"

func helloHandler(c *gin.Context) {
	claims := jwt.ExtractClaims(c)
	user, _ := c.Get(identityKey)
	c.JSON(200, gin.H{
		"userID":   claims[identityKey],
		"userName": user.(*User).UserName,
		"text":     "Hello World.",
	})
}

type User struct {
	UserName  string
	FirstName string
	LastName  string
}

func main() {
	port := os.Getenv("PORT")
	r := gin.New()
	r.Use(gin.Logger())
	r.Use(gin.Recovery())

	if port == "" {
		port = "8000"
	}

	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
		Realm:       "test zone",
		Key:         []byte("secret key"),
		Timeout:     time.Hour,
		MaxRefresh:  time.Hour,
		IdentityKey: identityKey,
		PayloadFunc: func(data interface{}) jwt.MapClaims {
			if v, ok := data.(*User); ok {
				return jwt.MapClaims{
					identityKey: v.UserName,
				}
			}
			return jwt.MapClaims{}
		},
		IdentityHandler: func(c *gin.Context) interface{} {
			claims := jwt.ExtractClaims(c)
			return &User{
				UserName: claims[identityKey].(string),
			}
		},
		Authenticator: func(c *gin.Context) (interface{}, error) {
			var loginVals login
			if err := c.ShouldBind(&loginVals); err != nil {
				return "", jwt.ErrMissingLoginValues
			}
			userID := loginVals.Username
			password := loginVals.Password

			if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
				return &User{
					UserName:  userID,
					LastName:  "Bo-Yi",
					FirstName: "Wu",
				}, nil
			}

			return nil, jwt.ErrFailedAuthentication
		},
		Authorizator: func(data interface{}, c *gin.Context) bool {
			if v, ok := data.(*User); ok && v.UserName == "admin" {
				return true
			}

			return false
		},
		Unauthorized: func(c *gin.Context, code int, message string) {
			c.JSON(code, gin.H{
				"code":    code,
				"message": message,
			})
		},

		TokenLookup: "header: Authorization, query: token, cookie: jwt",
		TokenHeadName: "Bearer",
		TimeFunc: time.Now,
	})

	if err != nil {
		log.Fatal("JWT Error:" + err.Error())
	}

	errInit := authMiddleware.MiddlewareInit()

	if errInit != nil {
		log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
	}

	r.POST("/login", authMiddleware.LoginHandler)

	r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
		claims := jwt.ExtractClaims(c)
		log.Printf("NoRoute claims: %#v\n", claims)
		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
	})

	auth := r.Group("/auth")
	auth.GET("/refresh_token", authMiddleware.RefreshHandler)
	auth.Use(authMiddleware.MiddlewareFunc())
	{
		auth.GET("/hello", helloHandler)
	}

	if err := http.ListenAndServe(":"+port, r); err != nil {
		log.Fatal(err)
	}
}

咱們能夠看到jwt.GinJWTMiddleware用於聲明一箇中間件。PayloadFunc方法中給默認的PAYLOAD增長了id字段,取值爲UserName。Authenticator認證器,咱們能夠在這裏驗證用戶身份,參數爲*gin.Context,因此在這裏咱們能夠像寫Gin Handler那樣獲取到Http請求中的各類內容。Authorizator受權器能夠判斷判斷當前JWT是否有權限繼續訪問。固然還能夠設置像過時時間,密鑰,是否設置COOKIE等其餘選項。web

登陸Handler

以上例子中配置了路由r.POST("/login", authMiddleware.LoginHandler)下面咱們來看一下登陸過程是怎樣的。算法

func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
	if mw.Authenticator == nil {
		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c))
		return
	}

	data, err := mw.Authenticator(c)

	if err != nil {
		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
		return
	}

	// Create the token
	token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
	claims := token.Claims.(jwt.MapClaims)

	if mw.PayloadFunc != nil {
		for key, value := range mw.PayloadFunc(data) {
			claims[key] = value
		}
	}

	expire := mw.TimeFunc().Add(mw.Timeout)
	claims["exp"] = expire.Unix()
	claims["orig_iat"] = mw.TimeFunc().Unix()
	tokenString, err := mw.signedString(token)

	if err != nil {
		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
		return
	}

	// set cookie
	if mw.SendCookie {
		expireCookie := mw.TimeFunc().Add(mw.CookieMaxAge)
		maxage := int(expireCookie.Unix() - mw.TimeFunc().Unix())

		if mw.CookieSameSite != 0 {
			c.SetSameSite(mw.CookieSameSite)
		}

		c.SetCookie(
			mw.CookieName,
			tokenString,
			maxage,
			"/",
			mw.CookieDomain,
			mw.SecureCookie,
			mw.CookieHTTPOnly,
		)
	}

	mw.LoginResponse(c, http.StatusOK, tokenString, expire)
}

LoginHandler總體邏輯仍是比較簡單的,檢查並調用前面設置的Authenticator方法,驗證成功的話生成一個新的JWT,調用PayloadFunc方法設置PAYLOAD的自定義字段,根據SendCookie判斷是否須要在HTTP中設置COOKIE,最後調用LoginResponse方法設置返回值。json

使用中間件

jwt-gin包提供了一個標準的Gin中間件,咱們能夠在須要驗證JWT的路由上設置中間件。前面例子中對路由組/auth增長了JWT驗證auth.Use(authMiddleware.MiddlewareFunc())後端

func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc {
	return func(c *gin.Context) {
		mw.middlewareImpl(c)
	}
}

func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
	claims, err := mw.GetClaimsFromJWT(c)
	if err != nil {
		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
		return
	}

	if claims["exp"] == nil {
		mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
		return
	}

	if _, ok := claims["exp"].(float64); !ok {
		mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
		return
	}

	if int64(claims["exp"].(float64)) < mw.TimeFunc().Unix() {
		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
		return
	}

	c.Set("JWT_PAYLOAD", claims)
	identity := mw.IdentityHandler(c)

	if identity != nil {
		c.Set(mw.IdentityKey, identity)
	}

	if !mw.Authorizator(identity, c) {
		mw.unauthorized(c, http.StatusForbidden, mw.HTTPStatusMessageFunc(ErrForbidden, c))
		return
	}

	c.Next()
}

GetClaimsFromJWT方法在當前上下文中獲取JWT,失敗的話返回未受權。接着會判斷JWT是否過時,最後前面設置的Authorizator方法驗證是否有權限繼續訪問。跨域

文章出處:基於gin的golang web開發:認證利器jwtcookie

相關文章
相關標籤/搜索