双token无感刷新机制的实现

4,947 阅读5分钟

前文我们实现了登录鉴权,接下来我们对其进行优化,实现双token无感刷新

前言

什么是无感刷新?

举个例子:

  1. 早上入园(登录成功)

    • 前台给你:当天门票(access_token)+ 年卡(refresh_token
  2. 玩过山车(请求数据)

  3. 下午门票过期access_token失效)

    • 检票员拦住你:"票过期了!"(接口返回401错误)
  4. 自动续票(无感刷新)

    • 你默默掏出年卡(refresh_token

    • 续票亭给你:新的当天门票(新access_token

  5. 继续游玩(请求重发)

    • 用新门票重新玩过山车(自动重试之前失败的请求)

    • 你完全没中断游玩(用户无感知)

简单来说,就是将之前登录鉴权使用的一个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接口请求。

  1. 后端验证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:'请先登录'
            }
        }
    }
}
  1. 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接口请求(代码在第一小点后面)