This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

1 the demand

Previously, Session mechanism was used for authentication in the background. Recently, before using Gin for reconstruction of SpringBoot project, I found that several open source Gin projects used JWT for authentication, so I plan to try to use JWT for authentication in Gin. The Token mechanism has many advantages over the Session mechanism. It does not require the server to store data, which can reduce the overhead of the server. However, there are some problems with the Token. After the Token is issued, the server cannot change its state, make it invalid or extend its validity. The basic use of JWT in Gin is documented here, along with a simple renewal scheme. The full demo structure of this article is as follows (see Github for the full sample code), with middleware intercepting requests to verify Token validity and utils encapsulating JWT generation, renewal, and validation:

| - gin_jwt | | - middleware | | -- JWT. Go | | - model | | -- user. Go | | - utils | | -- JWT. Go | | -- go. Mod | | -- main. GoCopy the code

2 Use JWT in Gin

Several open source Gin projects and some blogs use the Dgrijalva/JwT-go JWT library. However, this library was discontinued 4 years ago. Currently, a new JWT library golang-jwt/ JWT is being maintained. To download the JWT library, run the following command: go get -u github.com/golang-jwt/jwt/v4 Suppose I need to save a user ID, user name, and Email in a Token, customize the following Claims structure:

type UserInfo struct {
	Id       int
	UserName string
	Email    string
}

type MyClaims struct {
	User model.UserInfo
	jwt.StandardClaims // Standard Claims structure, which can set 8 standard fields
}
Copy the code

2.1 generate Token

When standard claims are generated, the expiration time is usually not long. The expiration time set here is two hours. To generate tokens, NewWithClaims of the JWT library is called, and the signature algorithm and claims structure are passed in. HS256 is the most commonly used signature algorithm.

const TokenExpireDuration = time.Hour * 2

var MySecret = []byte("yoursecret") // Generate the signature key
// called after successful login, passing in the UserInfo structure
func GenerateToken(userInfo model.UserInfo) (string, error) {
	expirationTime := time.Now().Add(TokenExpireDuration)  // Valid for two hours
	claims := &MyClaims{
		User: userInfo,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(),
			Issuer:    "yourname",}}// Generate tokens, specify signature algorithms and claims
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	/ / signature
	iftokenString, err := token.SignedString(MySecret); err ! =nil {
		return "", err
	} else {
		return tokenString, nil}}Copy the code

2.2 check Token

The token string from the front end is passed into the parse verification function, which calls ParseWithClaims for parsing. There are two parsing methods: one is to save the parsing result into the claims variable. The other is to fetch the Claims structure from the Token structure returned by ParseWithClaims. If the token string is valid but claims has expired, err indicates that the token has expired.

func ParseToken(tokenString string) (*MyClaims, error) {
	claims := &MyClaims{}
	_, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
		return MySecret, nil
	})
	// If the token has expired claims there is data; if the token cannot resolve claims there is no data
	return claims, err
}

// The second method retrieves Claims from the Token structure returned by jwt.ParseWithClaims
func ParseToken2(tokenString string) (*MyClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(t *jwt.Token) (interface{}, error) {
		return MySecret, nil
	})
	iferr ! =nil {
		return nil, err
	}
	if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("Token cannot be resolved")}Copy the code

2.3 Middleware intercepts requests and verifies tokens

The middleware logic here does not have the renewal function. When the Token fails to check, the request is rejected directly, no matter it is expired or illegal.

func JWTAuth(a) gin.HandlerFunc {
	return func(context *gin.Context) {
		auth := context.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			context.Abort()
			context.String(http.StatusOK, "No login permission")
			return
		}
		// Verify the token and reject the request if any error occurs
		_, err := utils.ParseToken(auth)
		iferr ! =nil {
			context.Abort()
			message := err.Error()
			context.JSON(http.StatusOK, message)
                        return
		} else {
			println("Token right")
		}
		context.Next()
	}
}
Copy the code

2.4 Main function logic

There are only two interfaces: one is the login interface, and the other is the sayHello interface that needs to verify the Token. If the user logs in to this interface, it will return Hello + user name.

package main

import (
	"gin_jwt/middleware"
	"gin_jwt/model"
	"gin_jwt/utils"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
)

// Write user data to death without accessing data
var db = &model.User{Id: 10001, Email: "[email protected]", UserName: "Alice", Password: "123456"}

func setupRouter(a) *gin.Engine {
	r := gin.Default()

	r.POST("login".func(c *gin.Context) {
		var userVo model.User
		ifc.ShouldBindJSON(&userVo) ! =nil {
			c.String(http.StatusOK, "Parameter error")
			return
		}
		if userVo.Email == db.Email && userVo.Password == db.Password {
			info := model.NewInfo(*db)
			tokenString, _ := utils.GenerateToken(*info)
			c.JSON(http.StatusOK, gin.H{
				"code":  201."token": tokenString,
				"msg":   "Login successful",})return
		}
		c.String(http.StatusOK, "Login failed")
		return
	})

	authorized := r.Group("/", middleware.JWTAuth())

	authorized.GET("/sayHello".func(c *gin.Context) {
		auth := c.Request.Header.Get("Authorization")
		claims, _ := utils.ParseToken(auth)
		log.Println(claims)
		c.String(http.StatusOK, "hello "+claims.User.UserName)
	})

	return r
}

func main(a) {
	r := setupRouter()
	r.Run(": 8080")}Copy the code

3 to renew Token

According to JWT standards, tokens should be stateless and can be issued to clients after expiration. However, the Token expiration time is usually short. If there is no automatic renewal mechanism, frequent re-login will cause a poor experience. At present, most schemes proposed online are to maintain the blacklist or whitelist of tokens combined with Redis. The principle is similar to session, and the server saves the status of tokens, which violates the stateless principle of tokens. Another option is to use two tokens, one access_token for accessing resources and one refresh_token with a long expiration time for obtaining a new Access_token. A relatively simple scheme is proposed here. When the Token expires but not more than 10 minutes, the request is released and a newtoken is generated for it. The newtoken is placed in a custom header named newtoken. The front-end interceptor determines whether the response header has a newToken, and if so, refreshes the locally saved token string.

3.1 Encapsulate the renewal function

Determines whether claims has expired after the specified time and generates a new token string if not, otherwise returns an empty string.

func RenewToken(claims *MyClaims) (string, error) {
	// Renew the token if it expires within 10 minutes
	if withinLimit(claims.ExpiresAt, 600) {
		return GenerateToken(claims.User)
	}
	return "", errors.New("Login has expired")}// Calculate whether the expiration time exceeds L
func withinLimit(s int64, l int64) bool {
	e := time.Now().Unix()
	return e-s < l
}
Copy the code

3.2 Modify the middleware logic of JWT

If the Token fails to be verified, the claim structure is passed to the renewal function. If the renewal is successful, the new Token string is placed in the response header and the request header value is modified.

func JWTAuth(a) gin.HandlerFunc {
	return func(c *gin.Context) {
		auth := c.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			// Reject without token
			c.Abort()
			c.String(http.StatusOK, "No login permission")
			return
		}
		/ / validation token
		claims, err := utils.ParseToken(auth)
		iferr ! =nil {
			if strings.Contains(err.Error(), "expired") {
				// If it expires, call the renew function
				newToken, _ := utils.RenewToken(claims)
				ifnewToken ! ="" {
					// Set a newToken field for the return header after successful renewal
					c.Header("newtoken", newToken)
					c.Request.Header.Set("Authorization", newToken)
					c.Next()
					return}}// The Token authentication fails or the Token renewal fails
			c.Abort()
			c.String(http.StatusOK, err.Error())
			return
		}
		// Token does not expire continue to execute 1 other middleware
		c.Next()
	}
}
Copy the code

3.3 Adding front-end interceptors

Here, axios, which is commonly used by Vue, is used as an example to determine whether there is a custom header in the interceptor of Response. If there is, a new token string is extracted from it to refresh the local old token string.

axios.interceptors.response.use(
    res= > {
        let newToken = res.headers["newtoken"]
        if (res.headers["newtoken"]) {
            // Refresh the token if there is newToken
            sessionStorage.setItem('tokenString', newToken)
            console.log('token refreshed~~~')}return res
    },
    err= >{
        return Promise.reject(err)
    })
Copy the code

4 Test Run

In this test, Postman is used to initiate a login request. After successful login, the token string returned by the server is obtained.

Add the token string to header Authorization to access the sayHello interface and get a response

If you access the sayHello interface within 10 minutes after the Token expires, you can still see a response and the response header contains the newtoken field. Save the newtoken when you write the front-end.