双token和无感刷新token(简单写法,一文说明白,不墨迹)
为什么有这篇小作文?
最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子
项目构成
- 后端部分:使用golang的gin框架起的服务
- 前端部分:vue+elementui
先说后端部分,后端逻辑相对前端简单点,关键两步
- 登陆接口生成双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
- 刷新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
- 路由中间件里验证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。(完结)
相关笔记