前文我们实现了登录鉴权,接下来我们对其进行优化,实现双token无感刷新
前言
什么是无感刷新?
举个例子:
-
早上入园(登录成功)
- 前台给你:当天门票(
access_token)+ 年卡(refresh_token)
- 前台给你:当天门票(
-
玩过山车(请求数据)
-
下午门票过期(
access_token失效)- 检票员拦住你:"票过期了!"(接口返回401错误)
-
自动续票(无感刷新)
-
你默默掏出年卡(
refresh_token) -
续票亭给你:新的当天门票(新
access_token)
-
-
继续游玩(请求重发)
-
用新门票重新玩过山车(自动重试之前失败的请求)
-
你完全没中断游玩(用户无感知)
-
简单来说,就是将之前登录鉴权使用的一个token换成两个token,access_token(短token)和refresh_token(长token),access_token主要用于验证用户是否登录,而refresh_token主要作用是更新access_token和refresh_token,从而延长用户的使用时间,不需频繁登录。
如何实现双token无感刷新?
1.将后端返回给登录请求的数据中的单token改为access_token和refresh_token
router.prefix('/user')//路由前缀,表示所有路由都以/user开头
router.post('/login',async (ctx)=>{
const {username,password} = ctx.request.body
try{
const res = await userLogin(username,password)
if(res.length){
let data={
id:res[0].id,
username:res[0].username,
nikeName:res[0].nikeName,
create_time:res[0].create_time,
}
const access_token = sign(data,'1h')//生成短token令牌
const refresh_token=sign(data,'7d')//生成长token令牌
ctx.body={
code:'1',//逻辑成功
msg:'登录成功',
data,
access_token:access_token,//登录成功后,后端返回一个短token给前端
refresh_token:refresh_token//登录成功后,后端返回一个长token给前端
}
}else{
ctx.body={
code:'0',
msg:'账号或密码错误',
}
}
}catch(error){
ctx.body={
code:'-1',
msg:'服务器异常',
data:error
}
}
})
2.将access_token和refresh_token一起进行浏览器本地保存
用户登录成功后,才能接收到,并进行保存
const onFinish = values => {
axios.post('/user/login',values).then(res=>{
toast.success('登录成功')
navigate('/noteClass')
localStorage.setItem('access_token',res.access_token)//登录成功后,将令牌(token)存储到浏览器本地
localStorage.setItem('refresh_token',res.refresh_token)//登录成功后,将令牌(token)存储到浏览器本地
}).catch(err=>{
console.log(err)
})
}
3. axios请求拦截器添加到请求头上的数据改access_token
axios.interceptors.request.use(request => {
const access_token = localStorage.getItem('access_token')//从浏览器本地获取token
if (access_token) {//如果令牌(token)存在,将令牌(token)放到请求头中
request.headers.Authorization = access_token//将令牌(token)放到请求头中
}
return request//返回请求数据(放行)
})
4. 处理access_token过期逻辑
当首页发送text接口请求时,后端验证发现access_token过期了,则返回401状态码,axios响应拦截器拦截到401状态码,则将浏览器中的refresh_token放入请求体,并发送refresh接口请求,后端收到refresh_token后对其进行验证,如果refresh_token过期,直接返回416状态码,告诉前端跳转登录页面,如果refresh_token验证通过,则生成新的access_token和refresh_token,返回给前端,前端接收后,携带新的access_token重新发送text接口请求。
- 后端验证access_token过期,返回401状态码
function verify(){
//ctx最终为index.js中的use方法中的ctx参数
//next为内置的,表示执行了app.use中的回调函数之后要运行一个next,这样后面的app.use()才会生效
return async (ctx,next)=>{
//1.获取请求头中的token
const token = ctx.request.headers.authorization
//2.验证token是否存在
if(token){
//3.验证token是否正确
try{
const decoded = jwt.verify(token,'666')//解密token
//判断decoded是否存在,因为能解密成功,说明token没有过期,且有id属性
if(decoded.id){
await next()//放行
}
}catch(error){//如果短token过期
ctx.status=401
ctx.body={
code:'0',
msg:'登录过期,请重新登录',
}
}
}else{//无token
ctx.status=402
ctx.body={
code:'0',
msg:'请先登录'
}
}
}
}
- axios响应拦截器拦截401,并进行access_token和refresh_token刷新
- 将refresh_token作为请求体,发送refresh接口请求
(res) => {
//后端返回的短token过期的状态码为401
if (res.status === 401) {
const originalRequest = res.config//获取原始请求,将没有成功的原始请求记录下来
//重新请求新的短token和长token
const refresh_token = localStorage.getItem('refresh_token')//从浏览器本地获取token
if(refresh_token){
axios.post('/user/refresh',{refresh_token}).then(res=>{
if(res.code==='1'){
//将新的短token放到浏览器本地
localStorage.setItem('access_token',res.access_token)
//将新的长token放到浏览器本地
localStorage.setItem('refresh_token',res.refresh_token)
//将原始请求的headers中的Authorization属性设置为新的短token
originalRequest.headers.Authorization = res.access_token
//重新发送原始请求
return axios(originalRequest)
}
})
}
}
//如果长短token都过期了,后端返回的状态码为416
if(res.status===416){
toast.error(res.response.data.msg)
setTimeout(() => {
window.location.href = '/login'//使用原生js跳转方式,跳转到登录页
}, 2000)
}
if(res.status===402){
toast.error(res.response.data.msg)
setTimeout(() => {
window.location.href = '/login'//使用原生js跳转方式,跳转到登录页
}, 2000)
}
- 验证refresh_token是否过期
//验证refresh_token
function refreshVerify(token){
try{
const decoded = jwt.verify(token,'666')//解密token
if(decoded.id){
return decoded
}
}catch(error){
return false
}
}
- 验证成功,刷新access_token和refresh_token
- 失败则返回416状态码
router.post('/refresh',async (ctx)=>{
const {refresh_token} =ctx.request.body
//校验refresh_token是否有效
const vaild=refreshVerify(refresh_token)
if(vaild.id){//如果长token解密成功
//创建新的长短token
const data={
id:vaild.id,
username:vaild.username,
nikeName:vaild.nikeName,
create_time:vaild.create_time,
}
const access_token = sign(data,'1h')//生成短token令牌
const refresh_token=sign(data,'7d')//生成长token令牌
ctx.body={
code:'1',
msg:'刷新成功',
access_token:access_token,
refresh_token:refresh_token,
}
}else{//长token过期
ctx.status=416,
ctx.body={
code:'0',
msg:'登录过期,请重新登录',
}
}
})
- 最后重新发送text接口请求(代码在第一小点后面)