在Gin中使用JWT做认证以及JWT的续签方案

在Gin中使用JWT做认证以及JWT的续签方案,第1张

在Gin中使用JWT做认证以及JWT的续签方案 1 需求

之前做后台都是用的Session机制来做认证,最近在用Gin重构之前SpringBoot项目,网上看了几个开源Gin项目发现都是用的JWT来做认证,所以打算尝试一下在Gin中使用JWT做认证。Token机制相对于Session机制来说有许多优点,不需要服务端存储数据可以减小服务端的开销;但Token也有一些问题,在签发后服务端就不能改变其状态,不能主动让其失效、不能延长有效期。这里记录一下Gin中JWT的基本使用、以及一个简单的续签方案。

本文的完整demo结构如下(完整示例代码见Github),middleware中拦截请求验证Token有效性,utils中对jwt的生成、续签和验证做封装:

|——gin_jwt
|    |——middleware
|        |——jwt.go
|    |——model
|        |——user.go
|    |——utils
|        |——jwt.go
|    |——go.mod
|    |——main.go
2 Gin中使用JWT

看了几个开源Gin项目和一些博客都使用的是dgrijalva/jwt-go这个jwt库,但是这个库4年前就停止维护了,目前相关人员维护了一个新的jwt库golang-jwt/jwt,这里使用这个新的jwt库。首先使用如下命令下载jwt库:

go get -u github.com/golang-jwt/jwt/v4
假设我需要在Token中保存用户ID、用户名和Email,自定义如下Claims结构体:

type UserInfo struct {
	Id       int
	UserName string
	Email    string
}

type MyClaims struct {
	User model.UserInfo
	jwt.StandardClaims // 标准Claims结构体,可设置8个标准字段
}
2.1 生成Token

生成标准claim时过期时间一般不用太长,这里设置的两个小时后过期。生成Token时调用jwt库的NewWithClaims即可,传入签名算法和claims结构体,签名算法用得最多的是HS256。

const TokenExpireDuration = time.Hour * 2

var MySecret = []byte("yoursecret") // 生成签名的密钥
// 登录成功后调用,传入UserInfo结构体
func GenerateToken(userInfo model.UserInfo) (string, error) {
	expirationTime := time.Now().Add(TokenExpireDuration)  // 两个小时有效期
	claims := &MyClaims{
		User: userInfo,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(),
			Issuer:    "yourname",
		},
	}
	// 生成Token,指定签名算法和claims
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 签名
	if tokenString, err := token.SignedString(MySecret); err != nil {
		return "", err
	} else {
		return tokenString, nil
	}

}
2.2 校验Token

将前端传来的token字符串传入解析校验函数,校验函数调用ParseWithClaims进行解析,此时有两种解析方法,一种是将解析结果保存到claims变量中,另一种是从ParseWithClaims返回的Token结构体中取出Claims结构体。这里选择第一种,若token字符串合法但过期claims也会有数据,err会提示token过期。

func ParseToken(tokenString string) (*MyClaims, error) {
	claims := &MyClaims{}
	_, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
		return MySecret, nil
	})
	// 若token只是过期claims是有数据的,若token无法解析claims无数据
	return claims, err
}

// 第二种方法通过jwt.ParseWithClaims返回的Token结构体取出Claims结构体
func ParseToken2(tokenString string) (*MyClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(t *jwt.Token) (interface{}, error) {
		return MySecret, nil
	})
	if err != nil {
		return nil, err
	}
	if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("token无法解析")
}
2.3 中间件拦截请求并校验Token

这里的中间件逻辑没有续签功能,当Token校验失败,无论是过期还是不合法直接拒绝用户请求。

func JWTAuth() gin.HandlerFunc {
	return func(context *gin.Context) {
		auth := context.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			context.Abort()
			context.String(http.StatusOK, "未登录无权限")
			return
		}
		// 校验token,只要出错直接拒绝请求
		_, err := utils.ParseToken(auth)
		if err != nil {
			context.Abort()
			message := err.Error()
			context.JSON(http.StatusOK, message)
		} else {
			println("token 正确")
		}
		context.Next()
	}
}
2.4 主函数逻辑

这里简单编写了一下主函数,只有两个接口:一个是登录接口,另一个是需要验证Token的sayHello接口,若用户登录了此接口会返回Hello + 用户名。

package main

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

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

// 不访问数据,写死用户数据
var db = &model.User{Id: 10001, Email: "[email protected]", UserName: "Alice", Password: "123456"}

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

	r.POST("login", func(c *gin.Context) {
		var userVo model.User
		if c.ShouldBindJSON(&userVo) != nil {
			c.String(http.StatusOK, "参数错误")
			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":   "登录成功",
			})
			return
		}
		c.String(http.StatusOK, "登录失败")
		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() {
	r := setupRouter()
	r.Run(":8080")
}
3 续签Token

按照JWT标准,Token就应该是无状态的,过期后重新給客户端发布新Token即可。但Token过期时间一般比较短,若没有自动续签机制的话,让用户频繁重新登录会造成比较糟糕的体验。目前网上提的比较多的方案是结合redis维持Token黑名单或白名单,原理和session有点类似了,服务端保存了Token的状态有点违背了Token的无状态原则;还有的方案是使用两个Token,一个access_token用于访问资源,一个过期时间较长的refresh_token用于获取新的access_token。这里想了一个比较简单的方案,当Token过期但不超过10分钟时放行请求并为其生成一个新的Token,将新token放在一个名为newtoken的自定义header中;前端拦截器中判断response的header是否有newtoken,若有的话则刷新本地保存的token字符串。

3.1 封装续签函数

判断claims的过期时间是否超过指定时间,若还未超过指定时间则生成一个新的token字符串,否则返回空字符串。

func RenewToken(claims *MyClaims) (string, error) {
	// 若token过期不超过10分钟则给它续签
	if withinLimit(claims.ExpiresAt, 600) {
		return GenerateToken(claims.User)
	}
	return "", errors.New("登录已过期")
}
// 计算过期时间是否超过l
func withinLimit(s int64, l int64) bool {
	e := time.Now().Unix()
	return e-s < l
}
3.2 修改jwt的中间件逻辑

当Token校验失败时判断一下是否是过期错误,若是过期错误则将Claims结构体传给续签函数,若续签成功则将新的token字符串放到response的指定header中,并修改request的header值。

func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		auth := c.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			// 无token直接拒绝
			c.Abort()
			c.String(http.StatusOK, "未登录无权限")
			return
		}
		// 校验token
		claims, err := utils.ParseToken(auth)
		if err != nil {
			if strings.Contains(err.Error(), "expired") {
				// 若过期,调用续签函数
				newToken, _ := utils.RenewToken(claims)
				if newToken != "" {
					// 续签成功給返回头设置一个newtoken字段
					c.Header("newtoken", newToken)
					c.Request.Header.Set("Authorization", newToken)
					c.Next()
					return
				}
			}
			// Token验证失败或续签失败直接拒绝请求
			c.Abort()
			c.String(http.StatusOK, err.Error())
			return
		}
		// token未过期继续执行1其他中间件
		c.Next()
	}
}
3.3 前端添加拦截器

这里以Vue常用的axios为例,在response的拦截器中判断是否有自定义的header,若有则从中取出新的token字符串用以刷新本地旧的token字符串。

axios.interceptors.response.use(
    res => {
        let newToken = res.headers["newtoken"]
        if (res.headers["newtoken"]) {
            // 若有newtoken则刷新token
            sessionStorage.setItem('tokenString', newToken)
            console.log('token refreshed~~~')
        }
        return res
    },
    err =>{
        return Promise.reject(err)
    })
4 测试运行

这里使用Postman进行测试,首先发起登录请求,登录成功后得到服务端返回的token字符串。

在header的Authorization中加入token字符串访问sayHello接口可以得到响应

Token过期10分钟内访问sayHello接口,可以看到依然有响应,并且response的header中多了newtoken字段,编写前端时保存这个新的token即可。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5715941.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存