文章目录
- 一: 实现原理
- 1. 后端中间件改进
- 2. 前端改进
- 3. 过期后点击请求测试
- 二: 完整代码
- 后端
- 前端
- 三: 其他思路
上一个Demo中,token一旦过期无法刷新需要重新登录,因此需要某种机制来自动更新token
一: 实现原理
1. 后端中间件改进
//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()}
}
2. 前端改进
3. 过期后点击请求测试
二: 完整代码
后端
package mainimport ("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 loginRequestctx.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")
}
前端
<template><el-config-provider namespace="ep"><el-menu class="el-menu-demo" mode="horizontal"><el-menu-item index="1">Vue JWT Demo</el-menu-item><el-menu-item index="2">当前用户: {{ username }}</el-menu-item></el-menu><!-- <img alt="Vue logo" class="element-plus-logo" src="./assets/logo.png" /> --><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">获取Token</el-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">请求API</el-button><el-button @click="onLogout">退出登陆</el-button></template></el-col><el-col :span="8"></el-col></el-row><!-- <HelloWorld msg="Hello Vue 3.0 + Element Plus + Vite" /> --></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.dataElMessage.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>
三: 其他思路
- 后端添加一个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})})
- 前段存储token时,在localStorage中再添加一个expire时间字段,每次请求API时判断是否快过期,并决定是否更新为新的token和expire时间