iT邦幫忙

2021 iThome 鐵人賽

DAY 21
0

Gin with JWT

使用Gin框架整合JWT

在Golang語言中, jwt-go 庫提供了一些jwt編碼和驗證的工具,因此我們很容易使用該庫來實現token認證。

另外,我們也知道 gin 框架中支援使用者自定義middleware,我們可以很好的將jwt相關的邏輯封裝在middleware中,然後對具體的介面進行認證。

Installation

go get -u github.com/dgrijalva/jwt-go

Customer Claims

又可將其稱作Payload,以Json的形式將使用者相關訊息,甚至是過期時間、簽證發放時間都寫在這

app/middleware/jwt-token.go

// MyClaims Customer jwt.StandardClaims
type MyClaims struct {
	Account string `json:"account"`
	jwt.StandardClaims
}
  • 這邊我們自定義一個MyClaims,並除了jwt-go原本的jwt.StandardClaims外,我們還另外儲存了Account的資訊

Generate Token

再來我們會寫個產生Token的Function,用以產生用來認證的JWT Token。

app/middleware/jwt-token.go

// GenToken Create a new token
func GenToken(account string) (string, error) {
	c := MyClaims{
		account,
		jwt.StandardClaims{
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
			Issuer: "Flynn",
		},
	}
	// Choose specific algorithm
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	// Choose specific Signature
	return token.SignedString(SecretKey)
}
  • 透過Account來創造自定義的Claims,然後設定過期時間為兩小、發證人為Flynn
  • 再來則是將Claims選擇HS256 Algorithm並加密
  • 最後則將Token加上簽名

Login API

接下來則是寫個Login function來讓Login API的Router做使用

app/controller/user.go

// AuthHandler @Summary
// @Tags user
// @version 1.0
// @produce application/json
// @param register body Login true "login"
// @Success 200 string successful return token
// @Router /v1/users/login [post]
func (u UsersController) AuthHandler(c *gin.Context) {
	var form Login
	bindErr := c.BindJSON(&form)
	if bindErr != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": -1,
			"msg":  "Invalid params",
		})
		return
	}
	userOne, err := service.LoginOneUser(form.Account, form.Password)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": -1,
			"msg":    "Failed to parse params" + err.Error(),
			"data":   nil,
		})
	}

	if userOne == nil {
		c.JSON(http.StatusNotFound, gin.H{
			"status": -1,
			"msg":    "User not found",
			"data":   nil,
		})
	}

	if userOne != nil {
		tokenString, _ := middleware.GenToken(form.Account)
		c.JSON(http.StatusOK, gin.H{
			"code": 0,
			"msg":  "Success",
			"data": gin.H{"token": tokenString},
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"code": 0,
		"msg":  "Verified Failed.",
	})
	return
}
  • 透過body的內容去User table進行Query,若帳號密碼符合的話則generate一個新的token然後return給User
  • 再來就是補上該API的swagger annotation,這部分就不在贅述。

Auth Middleware of JWT Token

再來則是寫一個驗證JWT Token的Middleware來給router使用

app/midddleware/jwt-token.go

// ParseToken Parse token
func ParseToken(tokenString string) (*MyClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
		return SecretKey, nil
	})
	if err != nil {
		return nil, err
	}
	// Valid token
	if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

// JWTAuthMiddleware Middleware of JWT
func JWTAuthMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {
		// Get token from Header.Authorization field.
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": -1,
				"msg":  "Authorization is null in Header",
			})
			c.Abort()
			return
		}

		parts := strings.SplitN(authHeader, " ", 2)
		if !(len(parts) == 2 && parts[0] == "Bearer") {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": -1,
				"msg":  "Format of Authorization is wrong",
			})
			c.Abort()
			return
		}
		// parts[0] is Bearer, parts is token.
		mc, err := ParseToken(parts[1])
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": -1,
				"msg":  "Invalid Token.",
			})
			c.Abort()
			return
		}
		// Store Account info into Context
		c.Set("account", mc.Account)
		// After that, we can get Account info from c.Get("account")
		c.Next()
	}
}
  • 首先Based在自定義的Claims上寫個Parse Token Function出來
  • 接下來則開始寫我們的Middleware function,這邊就很簡單的從gin.Context的Header拿出Authorization token,接著就是對該Token進行驗證,並將驗證失敗的地方補上Response。

Existing API with JWT

app/config/route.go

posts.GET("/:id", middleware.JWTAuthMiddleware(), controller.QueryUsersController().GetUser)
  • 在需要JWT驗證的地方加上JWTAuthMiddleware()

main.go

// @title Gin swagger
// @version 1.0
// @description Gin swagger

// @contact.name Flynn Sun

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host localhost:8080
// schemes http
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
  • 然後主程式部份別忘了補上 BearerAuth,產生新的swag template,這樣Swagger頁面才有地方讓人輸入Token

app/controller/user.go

// GetUser @Summary
// @Tags user
// @version 1.0
// @produce application/json
// @Security BearerAuth
// @param id path int true "id" default(1)
// @Success 200 string successful return data
// @Router /v1/users/{id} [get]
func (u UsersController) GetUser(c *gin.Context) {
  • 最後則是該API所對應到的controller function,記得要加上@Security BearerAuth 這行annotation,否則他會吃不到swagger的BearerAuth token

最後則記得在專案根目錄輸入swag init來重新產生swagger template,否則上面更改的swagger shit不會生效喔。

Demo

首先我們Login並取得有效token

https://ithelp.ithome.com.tw/upload/images/20211006/20129737WayefloGbc.png

再來,假設沒有輸入token就執行需要JWT驗證的API,就會獲得以下回應

https://ithelp.ithome.com.tw/upload/images/20211006/201297370UoO9qHGVB.png

因此我們需要到swagger頁面上方找到 Authorization Button,並輸入我們的token:

Bearer <token>

https://ithelp.ithome.com.tw/upload/images/20211006/20129737Zc7EgyhnmS.png

接著就可以再次地去執行需要JWT驗證的API了

https://ithelp.ithome.com.tw/upload/images/20211006/201297372aRi4ldPWX.png

Summary

這章節我們透過實戰來將jwt-go與現有的後端程式碼進行結構,從middleware、router、controller再到swagger都實裝了一遍,如果有細節不懂者可以參考下方連結,這次的code我也會放在這。

https://github.com/Neskem/Ironman-2021/tree/Day-21


上一篇
Day20 JWT Token
下一篇
Day22 Gin with CORS
系列文
fmt.Println("從零開始的Golang生活")30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言