• Golang JWT 认证 (三)-添加token自动刷新机制


    上一个Demo中,token一旦过期无法刷新需要重新登录,因此需要某种机制来自动更新token

    一: 实现原理

    1. 后端中间件改进

    读取token中过期时间
    token过期时间-最大截止刷新时间>当前时间?
    返回结果添加一个http头new-token=新生成的token
    End
    //gin jwt 认证中间件
    func AuthRequired() gin.HandlerFunc {
    	return func(ctx *gin.Context) {
    		tokenString := strings.TrimPrefix(ctx.GetHeader("Authorization"), "Bearer ")
    		token, err := jwt.ParseWithClaims(tokenString, &customClaims{}, func(t *jwt.Token) (interface{}, error) { return jwtKey, nil })
    		if err != nil {
    			ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("access token parse error: %v", err)})
    			return
    		}
    		if claims, ok := token.Claims.(*customClaims); ok && token.Valid {
    			if !claims.VerifyExpiresAt(time.Now(), false) {
    				ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": "access token expired"})
    				return
    			}
    			// *******************************新增部分***********************************
    			// 即将超过过期时间,则添加一个http header `new-token` 给前端更新
    			if t := claims.ExpiresAt.Time.Add(-time.Minute * TOKEN_MAX_REMAINING_MINUTE); t.Before(time.Now()) {
    				claims := customClaims{
    					Username: claims.Username,
    					IsAdmin:  claims.Username == "admin",
    					RegisteredClaims: jwt.RegisteredClaims{
    						ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(TOKEN_MAX_EXPIRE_HOUR * time.Hour)},
    					},
    				}
    			// *******************************新增部分结束*******************************
    				token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    				tokenString, _ := token.SignedString(jwtKey)
    				ctx.Header("new-token", tokenString)
    			}
    			ctx.Set("claims", claims)
    		} else {
    			ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("Claims parse error: %v", err)})
    			return
    		}
    		ctx.Next()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2. 前端改进

    请求API
    返回http头是否包含new-token?
    更新localStorage中token
    End

    3. 过期后点击请求测试

    在这里插入图片描述

    二: 完整代码

    后端

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"strings"
    	"time"
    
    	"github.com/gin-gonic/gin"
    	"github.com/golang-jwt/jwt/v4"
    )
    
    var jwtKey []byte = []byte("secret")
    
    const (
    	TOKEN_MAX_EXPIRE_HOUR      = 1  // token最长有效期
    	TOKEN_MAX_REMAINING_MINUTE = 15 // token还有多久过期就返回新token
    )
    
    type customClaims struct {
    	Username string `json:"username"`
    	IsAdmin  bool   `json:"IsAdmin"`
    	jwt.RegisteredClaims
    }
    
    //gin jwt 认证中间件
    func AuthRequired() gin.HandlerFunc {
    	return func(ctx *gin.Context) {
    		tokenString := strings.TrimPrefix(ctx.GetHeader("Authorization"), "Bearer ")
    		token, err := jwt.ParseWithClaims(tokenString, &customClaims{}, func(t *jwt.Token) (interface{}, error) { return jwtKey, nil })
    		if err != nil {
    			ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("access token parse error: %v", err)})
    			return
    		}
    		if claims, ok := token.Claims.(*customClaims); ok && token.Valid {
    			if !claims.VerifyExpiresAt(time.Now(), false) {
    				ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": "access token expired"})
    				return
    			}
    			// 即将超过过期时间,则添加一个http header `new-token` 给前端更新
    			if t := claims.ExpiresAt.Time.Add(-time.Minute * TOKEN_MAX_REMAINING_MINUTE); t.Before(time.Now()) {
    				claims := customClaims{
    					Username: claims.Username,
    					IsAdmin:  claims.Username == "admin",
    					RegisteredClaims: jwt.RegisteredClaims{
    						ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(TOKEN_MAX_EXPIRE_HOUR * time.Hour)},
    					},
    				}
    				token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    				tokenString, _ := token.SignedString(jwtKey)
    				ctx.Header("new-token", tokenString)
    			}
    			ctx.Set("claims", claims)
    		} else {
    			ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("Claims parse error: %v", err)})
    			return
    		}
    		ctx.Next()
    	}
    }
    
    type loginRequest struct {
    	Username string `json:"username"`
    	Password string `json:"password"`
    }
    
    func main() {
    	r := gin.Default()
    	r.POST("/auth/login", func(ctx *gin.Context) {
    		var req loginRequest
    		ctx.BindJSON(&req)
    
    		if req.Username != req.Password {
    			ctx.JSON(http.StatusOK, gin.H{"code": -1, "msg": "incorrect username or password"})
    			return
    		}
    
    		log.Printf("login user " + req.Username)
    
    		claims := customClaims{
    			Username: req.Username,
    			IsAdmin:  req.Username == "admin",
    			RegisteredClaims: jwt.RegisteredClaims{
    				ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(TOKEN_MAX_EXPIRE_HOUR * time.Hour)},
    			},
    		}
    
    		token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    
    		if tokenString, err := token.SignedString(jwtKey); err != nil {
    			ctx.JSON(http.StatusOK, gin.H{"code": -1, "msg": "generate access token failed: " + err.Error()})
    		} else {
    			ctx.JSON(http.StatusOK, gin.H{"code": 0, "msg": "", "data": tokenString})
    		}
    	})
    
    	api := r.Group("/api")
    	api.Use(AuthRequired())
    	api.GET("/test", func(ctx *gin.Context) {
    		claims := ctx.MustGet("claims").(*customClaims)
    		ctx.JSON(http.StatusOK, gin.H{"code": 0, "data": fmt.Sprintf("current user: %v , is admin: %v", claims.Username, claims.IsAdmin)})
    	})
    
    	r.Run(":8080")
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    前端

    <template>
      <el-config-provider namespace="ep">
        <el-menu class="el-menu-demo" mode="horizontal">
          <el-menu-item index="1">Vue JWT Demoel-menu-item>
          <el-menu-item index="2">当前用户: {{ username }}el-menu-item>
        el-menu>
        
        <el-row style="margin-top: 2rem">
          <el-col :span="8">el-col>
          <el-col :span="8">
            <template v-if="username === ''">
              <el-form :model="form">
                <el-form-item label="用户">
                  <el-input v-model="form.username" placeholder="username" />
                el-form-item>
                <el-form-item label="密码">
                  <el-input v-model="form.password" placeholder="password" />
                el-form-item>
                <el-form-item>
                  <el-button type="primary" style="width: 100%" @click="onLogin">获取Tokenel-button>
                el-form-item>
              el-form>
            template>
            <template v-else>
              <template v-if="result !== ''">
                <h1>请求成功h1>
                <h2>{{ result }}h2>
              template>
              <el-button type="primary" @click="onAPI">请求APIel-button>
              <el-button @click="onLogout">退出登陆el-button>
            template>
          el-col>
          <el-col :span="8">el-col>
        el-row>
        
      el-config-provider>
    template>
    
    <script setup lang="ts">
    import { reactive, ref } from 'vue'
    import axios from 'axios'
    import { ElMessage } from 'element-plus';
    
    const form = reactive({
      username: '',
      password: '',
    })
    
    const username = ref(localStorage.getItem('username') || '')
    const result = ref('')
    
    const onLogin = () => {
      axios.post('/auth/login', { username: form.username, password: form.password })
        .then(response => {
          if (response.data.code !== 0) {
            ElMessage.warning(`登陆失败: ${response.data.msg}`)
          } else {
            localStorage.setItem('token', response.data.data);
            localStorage.setItem('username', form.username);
            username.value = form.username;
            ElMessage.info(`${form.username}登陆成功!`)
          }
        })
        .catch(err => {
          console.log(err)
          ElMessage.error(err)
        })
    }
    
    const onAPI = () => {
      axios.get('/api/test', { headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } })
        .then(response => {
          if (response.headers['new-token']) {
            localStorage.setItem('token', response.headers['new-token']);
          }
          if (response.data.code !== 0) {
            ElMessage.warning(`获取: ${response.data.msg}`)
          } else {
            result.value = response.data.data
            ElMessage.info(`请求成功!`)
          }
        })
        .catch(err => {
          console.log(err)
          ElMessage.error(err)
        })
    }
    
    const onLogout = () => {
      localStorage.removeItem('username')
      localStorage.removeItem('token')
      username.value = ''
    }
    script>
    
    <style>
    #app {
      text-align: center;
      color: var(--ep-text-color-primary);
    }
    
    /* 
    .element-plus-logo {
      width: 50%;
    } */
    style>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    三: 其他思路

    • 后端添加一个refresh_token路由,由客户端判断过期时间,请求该路由
    	api.GET("/refresh-token", func(ctx *gin.Context) {
    		claims := ctx.MustGet("claims").(*customClaims)
    		newClaims := customClaims{
    			Username: claims.Username,
    			IsAdmin:  claims.Username == "admin",
    			RegisteredClaims: jwt.RegisteredClaims{
    				ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(TOKEN_MAX_EXPIRE_HOUR * time.Hour)},
    			},
    		}
    		token := jwt.NewWithClaims(jwt.SigningMethodHS256, newClaims)
    		tokenString, _ := token.SignedString(jwtKey)
    		ctx.JSON(http.StatusOK, gin.H{"code": 0, "msg": "", "data": tokenString})
    	})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 前段存储token时,在localStorage中再添加一个expire时间字段,每次请求API时判断是否快过期,并决定是否更新为新的token和expire时间
  • 相关阅读:
    MySQL学习笔记14
    python 的selenium库自动化操控浏览器最新教程
    【Kafka系列】(二)Kafka的基本使用
    Python【list合并】
    EMAS Serverless 到底有多便利?
    Lostash同步Mysql数据到ElasticSearch(二)logstash脚本配置和常见坑点
    引用 Python 中 import 模块
    项目验收测试
    利用Python处理DAX多条件替换
    PHP自己的框架留言板功能实现
  • 原文地址:https://blog.csdn.net/LeoForBest/article/details/126669714