双token登录无感刷新

定义

  • accessToken:用户获取数据权限

  • refreshToken:用来获取新的accessToken

双 token 验证机制,其中 accessToken 过期时间较短,refreshToken 过期时间较长。当 accessToken 过期后,使用 refreshToken 去请求新的 token。

双 token 验证流程
  1. 用户登录向服务端发送账号密码,登录失败返回客户端重新登录。登录成功服务端生成 accessToken 和 refreshToken,返回生成的 token 给客户端。

  2. 在请求拦截器中,请求头中携带 accessToken 请求数据,服务端验证 accessToken 是否过期。token 有效继续请求数据,token 失效返回失效信息到客户端。

  3. 客户端收到服务端发送的请求信息,在二次封装的 axios 的响应拦截器中判断是否有 accessToken 失效的信息,没有返回响应的数据。有失效的信息,就携带 refreshToken 请求新的 accessToken。

  4. 服务端验证 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客户端,无效,返回无效信息给客户端。

  5. 客户端响应拦截器判断响应信息是否有 refreshToken 有效无效。无效,退出当前登录。有效,重新存储新的 token,继续请求上一次请求的数据。

注意事项
  1. 短token失效,服务端拒绝请求,返回token失效信息,前端请求到新的短token如何再次请求数据,达到无感刷新的效果。

  2. 服务端白名单,成功登录前是还没有请求到token的,那么如果服务端拦截请求,就无法登录。定制白名单,让登录无需进行token验证。

后端代码

app.js

var expressJWT = require('express-jwt')

设置白名单
app.use(expressJWT({
  secret:"123456",
  algorithms:["HS256"],
  //getToken是修改express-jwt获取请求头并判断的标准。默认是只有一个authorization,也就是我们请求头的Authorization字段,但是,如果我们想要他有多个判断时,比如,我们再要他判断一个pass,就可以像如下这么写
  getToken: function fromHeaderOrQuerystring (req) {
    console.log(req.headers);
    if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
      return req.headers.authorization.split(' ')[1];
    }
    if (req.headers.pass && req.headers.pass.split(' ')[0] === 'Bearer') {
      return req.headers.pass.split(' ')[1];
    }
    return null;
  }
}).unless({path:["/login"]}))
//错位处理中间件,这里如果错误是UnauthorizedError,那么就证明accesstoken过期。
app.use(function (err, req, res, next) {
  console.log(err);
  if (err.name === 'UnauthorizedError') {
      res.status(200).send({ code: 4003, msg: "未验证身份" });//这里status中一定要写200,否则页面会报错
  } else {
      next();
  }
});

 

index.js

登录
router.post('/login', async (req, res) => {
  let data = req.body
  let routeinfo = await routerMoudel.find()
  let user = await loginModel.find({ username: data.username })
  if (user.length > 0) {
    let accessToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: 5 })
    let refreshToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: "1d" })
    // let token = jwt.sign({ ...user[0] }, '123456', { expiresIn: '1d' })
    if (user[0].password == data.password) {
      res.send({
        code: 200,
        msg: "登陆成功",
        routeinfo,
        accessToken,
        refreshToken
      })
    } else {
      res.send({
        code: 201,
        msg: "密码错误"
      })
    }
  } else {
    res.send({
      code: 203,
      msg: "用户名错误"
    })
  }
})

//刷新短token
router.get('/refresh', (req,res) => {
  let accessToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: 5 })
  let refreshToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: "1d" })
  res.send({
    code: 200,
    accessToken,
    refreshToken
  })
})

 

前端代码

http/index.js

import axios from 'axios'
//全局变量,这些通常会在全局
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

//全局方法,这些通常也会在全局
// 存储短token
export const setAccessToken = (token) => localStorage.setItem(ACCESS_TOKEN, token)
// 存储长token
export const setRefershToken = (token) => localStorage.setItem(REFRESH_TOKEN, token)
// 获取短token
export const getAccessToken = () => localStorage.getItem(ACCESS_TOKEN)
// 获取长token
export const getRefershToken = () => localStorage.getItem(REFRESH_TOKEN)
// 删除短token
export const removeAccessToken = () => localStorage.removeItem(ACCESS_TOKEN)
// 删除长token
export const removeRefershToken = () => localStorage.removeItem(REFRESH_TOKEN)


let subscribes=[]
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){
                    console.log('长token没了');
                    flag = false
                    removeRefershToken(REFRESH_TOKEN)
                } else if(res.code===200){
                    // 存储新的token
                    setAccessToken(res.accessToken)
                    setRefershToken(res.refreshToken)
                    flag = false
                    // 重新请求数据
                    retryRequest()
                }
            })
        }
    }
}


//封装axios
const server = axios.create({
  baseURL: 'http://localhost:3000', // 你的服务器
  timeout: 1000 * 10,
  headers: {
      "Content-type": "application/json"
  }
})

/*请求拦截器*/
server.interceptors.request.use(config => {
  // 获取短token,携带到请求头,服务端校验
  let aToken = getAccessToken(ACCESS_TOKEN)
  console.log(aToken);
  config.headers[AUTH] = aToken
  console.log(config.headers);
  return config
})

/*响应拦截器*/
server.interceptors.response.use(
  async response => {
      // 获取到配置和后端响应的数据
      let { config, data } = response
      console.log('响应提示信息:', data);
      return new Promise((resolve, reject) => {
          // 短token失效
          if (data.code === 4003) {
              console.log(config);
              // 移除失效的短token
              removeAccessToken(ACCESS_TOKEN)
              // 把过期请求存储起来,用于请求到新的短token,再次请求,达到无感刷新
              addRequest(() => resolve(server(config)))
              // 携带长token去请求新的token
              refreshToken()
          } else {
              // 有效返回相应的数据
              resolve(data)
          }

      }).catch((err)=>{
        console.log(err);
      })

  },
  error => {
      return Promise.reject(error)
  }
)

export default server

 

登录页面

<script setup>
import server from '../http/index'
import { ref } from "vue";
import { useRouter } from "vue-router";
import {setAccessToken,setRefershToken} from '../http/index'
const router = useRouter();
const form = ref({
  username: "",
  password: "",
});

const onSubmit = async () => {
  let res = await server.post("login", form.value);
  console.log(res.routeinfo,'+++');
  setAccessToken(res.accessToken);
  setRefershToken(res.refreshToken);
  form.value=""
  sessionStorage.setItem("routerDate", JSON.stringify(res.routeinfo));
  router.push("/container");
};

</script>
<template >
  <div>
    <el-form :model="form" label-width="auto" style="max-width: 600px">
      <el-form-item label="用户名">
        <el-input v-model="form.username" />
      </el-form-item>

      <el-form-item label="密码">
        <el-input v-model="form.password" />
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="onSubmit">Create</el-button>
        <el-button>Cancel</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<style scoped>
</style>

 

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值