Jansiel Notes

双token和无感刷新token(简单写法,一文说明白,不墨迹)

为什么有这篇小作文?

最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子

项目构成

  • 后端部分:使用golang的gin框架起的服务
  • 前端部分:vue+elementui

先说后端部分,后端逻辑相对前端简单点,关键两步

  1. 登陆接口生成双token
1"github.com/dgrijalva/jwt-go"
2
 1func (this UserController) DoLogin(ctx *gin.Context) {
 2	username := ctx.Request.FormValue("username")
 3	passWord := ctx.Request.FormValue("password")
 4	passMd5 := middlewares.CreateMD5(passWord)
 5	expireTime := time.Now().Add(10 * time.Second).Unix() //token过期时间10秒,主要是测试方便
 6	refreshTime := time.Now().Add(20 * time.Second).Unix() //刷新的时间限制,超过20秒重新登录
 7	user := modules.User{}
 8	err := modules.DB.Model(&modules.User{}).Where("username = ? AND password = ?", username, passMd5).Find(&user).Error
 9	if err != nil {
10		ctx.JSON(400, gin.H{
11			"success": false,
12			"message": "用户名或密码错误",
13		})
14	} else {
15		println("expireTime", string(rune(expireTime)))
16		myClaims := MyClaims{
17			user.Id,
18			jwt.StandardClaims{
19				ExpiresAt: expireTime,
20			},
21		}
22		myClaimsRefrrsh := MyClaims{
23			user.Id,
24			jwt.StandardClaims{
25				ExpiresAt: refreshTime,
26			},
27		}
28		jwtKey := []byte("lyf123456")
29		tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
30		tokenStr, err := tokenObj.SignedString(jwtKey)
31		tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
32		tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
33		if err != nil && err2 != nil {
34			ctx.JSON(200, gin.H{
35				"message": "生成token失败",
36				"success": false,
37			})
38		} else {
39			ctx.JSON(200, gin.H{
40				"message":      "登录成功",
41				"success":      true,
42				"token":        tokenStr,//数据请求的token
43				"refreshToken": tokenStrRefresh,//刷新token用的
44			})
45		}
46	}
47}
48
  1. 刷新token的方法
 1func (this UserController) RefrshToken(ctx *gin.Context) {
 2	tokenData := ctx.Request.Header.Get("Authorization") //这里是个关键点,刷新token时也要带上token,不过这里是前端传的refreshToken
 3	if tokenData == "" {
 4		ctx.JSON(401, gin.H{
 5			"message": "token为空",
 6			"success": false,
 7		})
 8		ctx.Abort()
 9		return
10	}
11	tokenStr := strings.Split(tokenData, " ")[1]
12	_, claims, err := middlewares.ParseToken(tokenStr)
13	expireTime := time.Now().Add(10 * time.Second).Unix()
14	refreshTime := time.Now().Add(20 * time.Second).Unix()
15	if err != nil {
16		ctx.JSON(400, gin.H{
17			"success": false,
18			"message": "token传入错误",
19		})
20	} else {
21		myClaims := MyClaims{
22			claims.Uid,
23			jwt.StandardClaims{
24				ExpiresAt: expireTime,
25			},
26		}
27		myClaimsRefrrsh := MyClaims{
28			claims.Uid,
29			jwt.StandardClaims{
30				ExpiresAt: refreshTime,
31			},
32		}
33		jwtKey := []byte("lyf123456")
34		tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
35		tokenStr, err := tokenObj.SignedString(jwtKey)
36		tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
37		tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
38		if err != nil && err2 != nil {
39			ctx.JSON(400, gin.H{
40				"message": "生成token失败",
41				"success": false,
42			})
43		} else {
44			ctx.JSON(200, gin.H{
45				"message":      "刷新token成功",
46				"success":      true,
47				"token":        tokenStr,
48				"refreshToken": tokenStrRefresh,
49			})
50		}
51	}
52}
53
  1. 路由中间件里验证token
 1package middlewares
 2
 3import (
 4	"strings"
 5
 6	"github.com/dgrijalva/jwt-go"
 7	"github.com/gin-gonic/gin"
 8)
 9
10type MyClaims struct {
11	Uid int
12	jwt.StandardClaims
13}
14
15func AuthMiddleWare(c *gin.Context) {
16	tokenData := c.Request.Header.Get("Authorization")
17	if tokenData == "" {
18		c.JSON(401, gin.H{
19			"message": "token为空",
20			"success": false,
21		})
22		c.Abort()
23		return
24	}
25	tokenStr := strings.Split(tokenData, " ")[1]
26	token, _, err := ParseToken(tokenStr)
27	if err != nil || !token.Valid {
28         // 这里我感觉觉是个关键点,我看别人写的,过期了返回401,但是前端的axios的响应拦截器里捕获不到,所以我用201状态码,
29		c.JSON(201, gin.H{
30			"message": "token已过期",
31			"success": false,
32		})
33		c.Abort()
34		return
35	} else {
36		c.Next()
37	}
38}
39
40func ParseToken(tokenStr string) (*jwt.Token, *MyClaims, error) {
41	jwtKey := []byte("lyf123456")
42	// 解析token
43	myClaims := &MyClaims{}
44	token, err := jwt.ParseWithClaims(tokenStr, myClaims, func(token *jwt.Token) (interface{}, error) {
45		return jwtKey, nil
46	})
47	return token, myClaims, err
48}
49

总结一下:后端部分三步,1.登陆时生成双token,2,路由中间件里验证token,过期时返回201状态码(201是我私人定的,并不是行业标准)。3,刷新token的方法里也和登陆接口一样返回双token

前端部分

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

 1import axios from 'axios'
 2import { ElMessage } from 'element-plus'
 3import router from '../router'
 4//数据请求用
 5const server=axios.create({
 6  baseURL:'/shopApi',
 7  timeout:5000
 8})
 9// 刷新token专用
10const serverRefreshToken=axios.create({
11  baseURL:'/shopApi',
12  timeout:5000
13})
14//获取新token的方法
15async function getNewToken(){
16  let res=await serverRefreshToken.request({
17    url:`/admin/refresh`,
18    method:"post",
19  })
20  if(res.status==200){
21    sessionStorage.setItem("token",res.data.token)
22    sessionStorage.setItem("refreshToken",res.data.refreshToken)
23    return true
24  }else{
25    ElMessage.error(res.data.message)
26    router.push('/login')
27    return false
28  }
29}
30//这里是正常获取数据用的请求拦截器,主要作用是给所有请求的请求头里加上token
31server.interceptors.request.use(config=>{
32  let token=""
33  token=sessionStorage.getItem("token")
34  if(token){
35    config.headers.Authorization="Bearer "+token
36  }
37  return config
38},error=>{
39  Promise.reject(error)
40})
41//这里是正常获取数据用的响应拦截器,正常数据请求都是200状态码,当拦截到201状态码时,代表token过期了,
42// 应热心小伙伴的提醒,加上防止token过期后正好短时间内多个请求重复刷新token,刷新token成功再请求
43let isRefreshing=false
44let refreshFnArr=[]
45server.interceptors.response.use(async(res)=>{
46  if(res.status==201){
47    if(!isRefreshing){
48    // 如果正好段时间内触发了多个请求
49      isRefreshing=true
50      let bl=await getNewToken()
51      if(bl){
52        refreshFnArr.forEach(fn=>{
53          fn()
54        })
55        refreshFnArr=[]
56        res= await server.request(res.config)
57        isRefreshing=false
58      }
59    }else{
60      return new Promise(resolve=>{
61        refreshFnArr.push(
62          ()=>{
63            resolve(res.config)
64          }
65        )
66      })
67    }
68  }
69  return res
70},error=>{
71  if(error.response.status==500||error.response.status==401||error.response.status==400){
72    router.push('/login')
73    ElMessage.error(error.response.data.message)
74    Promise.reject(error)
75  }
76
77})
78//这里是刷新token专用的axios对象,他的作用是给请求加上刷新token专用的refreshToken
79serverRefreshToken.interceptors.request.use(config=>{
80  let token=""
81  token=sessionStorage.getItem("refreshToken")
82  if(token){
83    config.headers.Authorization="Bearer "+token
84  }
85  return config
86},error=>{
87  Promise.reject(error)
88})
89export default server
90

总结一下,前端部分:1,正常数据请求和刷新token用的请求分开了,各司其职。省去复杂的判断。2,获取新的token和refreshToken后更新原来旧的token和refreshToken。(完结)