记录-使用双token实现无感刷新,前后端详细代码
Posted 林恒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录-使用双token实现无感刷新,前后端详细代码相关的知识,希望对你有一定的参考价值。
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前言
近期写的一个项目使用双token实现无感刷新。最后做了一些总结,本文详细介绍了实现流程,前后端详细代码。前端使用了Vue3+Vite,主要是axios封装,服务端使用了koa2做了一个简单的服务器模拟。
一、token 登录鉴权
jwt:JSON Web Token。是一种认证协议,一般用来校验请求的身份信息和身份权限。 由三部分组成:Header、Hayload、Signature
header:也就是头部信息,是描述这个 token 的基本信息,json 格式
"alg": "HS256", // 表示签名的算法,默认是 HMAC SHA256(写成 HS256) "type": "JWT" // 表示Token的类型,JWT 令牌统一写为JWT
"iss": "a.com", // 签发人 "exp": "1d", // expiration time 过期时间 "sub": "test", // 主题 "aud": "", // 受众 "nbf": "", // Not Before 生效时间 "iat": "", // Issued At 签发时间 "jti": "", // JWT ID 编号 // 可以定义私有字段 "name": "", "admin": ""
Signature 签名 是对前两部分的签名,防止数据被篡改。 需要指定一个密钥。这个密钥只有服务器才知道,不能泄露。使用 Header 里面指定的签名算法,按照公式产生签名。
算出签名后,把 Header、Payload、Signature 三个部分拼成的一个字符串,每个部分之间用 . 分隔。这样就生成了一个 token
二、何为双 token
accessToken
:用户获取数据权限refreshToken
:用来获取新的accessToken
双 token 验证机制,其中 accessToken 过期时间较短,refreshToken 过期时间较长。当 accessToken 过期后,使用 refreshToken 去请求新的 token。
双 token 验证流程
- 用户登录向服务端发送账号密码,登录失败返回客户端重新登录。登录成功服务端生成 accessToken 和 refreshToken,返回生成的 token 给客户端。
- 在请求拦截器中,请求头中携带 accessToken 请求数据,服务端验证 accessToken 是否过期。token 有效继续请求数据,token 失效返回失效信息到客户端。
- 客户端收到服务端发送的请求信息,在二次封装的 axios 的响应拦截器中判断是否有 accessToken 失效的信息,没有返回响应的数据。有失效的信息,就携带 refreshToken 请求新的 accessToken。
- 服务端验证 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客户端,无效,返回无效信息给客户端。
- 客户端响应拦截器判断响应信息是否有 refreshToken 有效无效。无效,退出当前登录。有效,重新存储新的 token,继续请求上一次请求的数据。
注意事项
- 短token失效,服务端拒绝请求,返回token失效信息,前端请求到新的短token如何再次请求数据,达到无感刷新的效果。
- 服务端白名单,成功登录前是还没有请求到token的,那么如果服务端拦截请求,就无法登录。定制白名单,让登录无需进行token验证。
三、服务端代码
1. 搭建koa2服务器
全局安装koa脚手架
npm install koa-generator -g
koa2 server
cd server 进入到项目安装jwt
npm i jsonwebtoken
npm i koa-cors
const cors=require(\'koa-cors\') ... app.use(cors())
2. 双token
新建utils/token.js
const jwt=require(\'jsonwebtoken\') const secret=\'2023F_Ycb/wp_sd\' // 密钥 /* expiresIn:5 过期时间,时间单位是秒 也可以这么写 expiresIn:1d 代表一天 1h 代表一小时 */ // 本次是为了测试,所以设置时间 短token5秒 长token15秒 const accessTokenTime=5 const refreshTokenTime=15 // 生成accessToken const setAccessToken=(payload=)=> // payload 携带用户信息 return jwt.sign(payload,secret,expireIn:accessTokenTime) //生成refreshToken const setRefreshToken=(payload=)=> return jwt.sign(payload,secret,expireIn:refreshTokenTime) module.exports= secret, setAccessToken, setRefreshToken
3. 路由
直接使用脚手架创建的项目已经在app.js使用了路由中间件 在router/index.js 创建接口
const router = require(\'koa-router\')() const jwt = require(\'jsonwebtoken\') const getAccesstoken, getRefreshtoken, secret =require(\'../utils/token\') /*登录接口*/ router.get(\'/login\',()=> let code,msg,data=null code=2000 msg=\'登录成功,获取到token\' data= accessToken:getAccessToken(), refreshToken:getReferToken() ctx.body= code, msg, data ) /*用于测试的获取数据接口*/ router.get(\'/getTestData\',(ctx)=> let code,msg,data=null code=2000 msg=\'获取数据成功\' ctx.body= code, msg, data ) /*验证长token是否有效,刷新短token 这里要注意,在刷新短token的时候回也返回新的长token,延续长token, 这样活跃用户在持续操作过程中不会被迫退出登录。长时间无操作的非活 跃用户长token过期重新登录 */ router.get(\'/refresh\',(ctx)=> let code,msg,data=null //获取请求头中携带的长token let r_tk=ctx.request.headers[\'pass\'] //解析token 参数 token 密钥 回调函数返回信息 jwt.verify(r_tk,secret,(error)=> if(error) code=4006, msg=\'长token无效,请重新登录\' else code=2000, msg=\'长token有效,返回新的token\', data= accessToken:getAccessToken(), refreshToken:getReferToken() ) )
4. 应用中间件
utils/auth.js
const secret = require(\'./token\') const jwt = require(\'jsonwebtoken\') /*白名单,登录、刷新短token不受限制,也就不用token验证*/ const whiteList=[\'/login\',\'/refresh\'] const isWhiteList=(url,whiteList)=> return whiteList.find(item => item === url) ? true : false /*中间件 验证短token是否有效 */ const cuth = async (ctx,next)=> let code, msg, data = null let url = ctx.path if(isWhiteList(url,whiteList)) // 执行下一步 return await next() else // 获取请求头携带的短token const a_tk=ctx.request.headers[\'authorization\'] if(!a_tk) code=4003 msg=\'accessToken无效,无权限\' ctx.body= code, msg, data else // 解析token await jwt.verify(a_tk,secret.(error)=> if(error)=> code=4003 msg=\'accessToken无效,无权限\' ctx.body= code, msg, datta else // token有效 return await next() ) module.exports=auth
const auth=requier(./utils/auth) ··· app.use(auth)
其实如果只是做一个简单的双token验证,很多中间件是没必要的,比如解析静态资源。不过为了节省时间,方便就直接使用了koa2脚手架。
最终目录结构:
四、前端代码
1. Vue3+Vite框架
前端使用了Vue3+Vite的框架,看个人使用习惯。
npm init vite@latest client_side
npm i axios
2. 定义使用到的常量
config/constants.js
export const ACCESS_TOKEN = \'a_tk\' // 短token字段 export const REFRESH_TOKEN = \'r_tk\' // 短token字段 export const AUTH = \'Authorization\' // header头部 携带短token export const PASS = \'pass\' // header头部 携带长token
3. 存储、调用过期请求
关键点:把携带过期token的请求,利用Promise存在数组中,保持pending状态,也就是不调用resolve()。当获取到新的token,再重新请求。 utils/refresh.js
export REFRESH_TOKEN,PASS from \'../config/constants.js\' import getRefreshToken, removeRefreshToken, setAccessToken, setRefreshToken from \'../config/storage\' let subsequent=[] let flag=false // 设置开关,保证一次只能请求一次短token,防止客户多此操作,多次请求 /*把过期请求添加在数组中*/ export const addRequest = (request) => subscribes.push(request) /*调用过期请求*/ export const retryRequest = () => console.log(\'重新请求上次中断的数据\'); subscribes.forEach(request => request()) subscribes = [] /*短token过期,携带token去重新请求token*/ export const refreshToken=()=> if(!flag) flag = true; let r_tk = getRefershToken() // 获取长token if(r_tk) server.get(\'/refresh\',Object.assign(, headers:[PASS]=r_tk )).then((res)=> //长token失效,退出登录 if(res.code===4006) flag = false removeRefershToken(REFRESH_TOKEN) else if(res.code===2000) // 存储新的token setAccessToken(res.data.accessToken) setRefreshToken(res.data.refreshToken) flag = false // 重新请求数据 retryRequest() )
4. 封装axios
utlis/server.js
import axios from "axios"; import * as storage from "../config/storage" import * as constants from \'../config/constants\' import addRequest, refreshToken from "./refresh"; const server = axios.create( baseURL: \'http://localhost:3004\', // 你的服务器 timeout: 1000 * 10, headers: "Content-type": "application/json" ) /*请求拦截器*/ server.interceptors.request.use(config => // 获取短token,携带到请求头,服务端校验 let aToken = storage.getAccessToken(constants.ACCESS_TOKEN) config.headers[constants.AUTH] = aToken return config ) /*响应拦截器*/ server.interceptors.response.use( async response => // 获取到配置和后端响应的数据 let config, data = response console.log(\'响应提示信息:\', data.msg); return new Promise((resolve, reject) => // 短token失效 if (data.code === 4003) // 移除失效的短token storage.removeAccessToken(constants.ACCESS_TOKEN) // 把过期请求存储起来,用于请求到新的短token,再次请求,达到无感刷新 addRequest(() => resolve(server(config))) // 携带长token去请求新的token refreshToken() else // 有效返回相应的数据 resolve(data) ) , error => return Promise.reject(error) )
5. 复用封装
import * as constants from "./constants" // 存储短token export const setAccessToken = (token) => localStorage.setItem(constanst.ACCESS_TOKEN, token) // 存储长token export const setRefershToken = (token) => localStorage.setItem(constants.REFRESH_TOKEN, token) // 获取短token export const getAccessToken = () => localStorage.getItem(constants.ACCESS_TOKEN) // 获取长token export const getRefershToken = () => localStorage.getItem(constants.REFRESH_TOKEN) // 删除短token export const removeAccessToken = () => localStorage.removeItem(constants.ACCESS_TOKEN) // 删除长token export const removeRefershToken = () => localStorage.removeItem(constants.REFRESH_TOKEN)
6. 接口封装
apis/index.js
import server from "../utils/server"; /*登录*/ export const login = () => return server( url: \'/login\', method: \'get\' ) /*请求数据*/ export const getData = () => return server( url: \'/getList\', method: \'get\' )
前端如何实现token的无感刷新
通常,对于一些需要记录用户行为的系统,在进行网络请求的时候都会要求传递一下登录的token。不过,为了接口数据的安全,服务器的token一般不会设置太长,根据需要一般是1-7天的样子,token过期后就需要重新登录。不过,频繁的登录会造成体验不好的问题,因此,需要体验好的话,就需要定时去刷新token,并替换之前的token。
要做到token的无感刷新,主要有3种方案:
方案一:
后端返回过期时间,前端每次请求就判断token的过期时间,如果快到过期时间,就去调用刷新token接口。
缺点:需要后端额外提供一个token过期时间的字段;使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。
方法二
写个定时器,然后定时刷新token接口。
缺点:浪费资源,消耗性能,不建议采用。
方法三
在请求响应拦截器中拦截,判断token 返回过期后,调用刷新token接口。
综合上面的三个方法,最好的是第三个,因为它不需要占用额外的资源。接下来,我们看一下使用axios进行网络请求,然后响应service.interceptors.response的拦截。
import axios from 'axios'
service.interceptors.response.use(
response => {
if (response.data.code === 409) {
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
setToken(token)
response.headers.Authorization = `${token}`
}).catch(err => {
removeToken()
router.push('/login')
return Promise.reject(err)
})
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
}
)
问题一:如何防止多次刷新token
为了防止多次刷新token,可以通过一个变量isRefreshing 去控制是否在刷新token的状态。
import axios from 'axios'
service.interceptors.response.use(
response => {
if (response.data.code === 409) {
if (!isRefreshing) {
isRefreshing = true
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
setToken(token)
response.headers.Authorization = `${token}`
}).catch(err => {
removeToken()
router.push('/login')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
}
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
}
)
二、同时发起两个或者两个以上的请求时,怎么刷新token
当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。
那么如何做到让这个请求处于等待中呢?为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。
import axios from 'axios'
// 是否正在刷新的标记
let isRefreshing = false
//重试队列
let requests = []
service.interceptors.response.use(
response => {
//约定code 409 token 过期
if (response.data.code === 409) {
if (!isRefreshing) {
isRefreshing = true
//调用刷新token的接口
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
// 替换token
setToken(token)
response.headers.Authorization = `${token}`
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(token))
requests = [] // 重新请求完清空
return service(response.config)
}).catch(err => {
//跳到登录页
removeToken()
router.push('/login')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// 返回未执行 resolve 的 Promise
return new Promise(resolve => {
// 用函数形式将 resolve 存入,等待刷新后再执行
requests.push(token => {
response.headers.Authorization = `${token}`
resolve(service(response.config))
})
})
}
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
}
)
以上是关于记录-使用双token实现无感刷新,前后端详细代码的主要内容,如果未能解决你的问题,请参考以下文章