无痛刷新token续接401请求
发布于 4 年前 作者 gfan 4919 次浏览 来自 分享

在小程序开发中,我们都知道小程序是没有cookie的,那么用户身份是如何确定的,后段颁发token,前端每次请求头部附带token。

既然是token,那么肯定有它的过期时间,没有一个token是永久的,永久的token就相当于一串永久的密码,是不安全的,

那么既然有刷新时间,问题就来了

1.前后端交互的过程中token如何存储?

2.token过期时,前端该怎么处理?

3.当用户正在操作时,遇到token过期该怎么办?直接跳回登陆页面?

token如何存储?

cookie的大小约4k,兼容性在ie6及以上 都兼容,在浏览器和服务器间来回传递,因此它得在服务器的环境下运行,而且可以设定过期时间,默认的过期时间是session会话结束。

localStorage的大小约5M,兼容性在ie7及以上都兼容,有浏览器就可以,不需要在服务器的环境下运行, 会一直存在,除非手动清除 。

答案大致分为2种

存在 cookie 中

存在 localStorage 中

token过期时,前端该怎么处理?

__1.__第一种:跳回登陆页面重新登陆

2.第二种:拦截401重新获取token

class HttpClient {
  /**
   * Create a new instance of HttpClient.
   */
  constructor() {
    this.interceptors = {
      request: [],
      response: []
    };
  }

  /**
   * Sends a single request to server.
   *
   * [@param](/user/param) {Object} options - Coming soon.
   */
  sendRequest(options) {
    let requestOptions = options;

    if (!requestOptions.header) {
      requestOptions.header = {};
    }

    // 重新设置 Accept 和 Content-Type
    requestOptions.header = Object.assign(
      {
        Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=utf-8'
      },
      requestOptions.header
    );
    this.interceptors.request.forEach((interceptor) => {
      const request = interceptor(requestOptions);
      requestOptions = request.options;
    });

    // 将以 Promise 返回数据, 无 success、fail、complete 参数
    // let response = uni.request(requestOptions);

    // 使用Promise包装一下, 以 complete方式来接收接口调用结果
    let response = new Promise((resolve, reject) => {
      requestOptions.complete = (res) => {
        const { statusCode } = res;
        const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
        if(statusCode==401){ //拦截401请求
          uni.reLaunch({ //关闭所有页面直接跳到登陆页面
            url: '/pages/login/login'
          });
        }
        if (isSuccess) {
          if(res.data.code==1){
            resolve(res.data);
          }else{
            reject(res);
          }
        } else {
          reject(res);
        }
      };
      requestOptions.requestId = new Date().getTime();
      uni.request(requestOptions);
    });

    this.interceptors.response.forEach((interceptor) => {
      response = interceptor(response);
    });

    return response;
  }
}

export default HttpClient;

这种方法适用余有登陆页面的小程序,但同样存在问题,假如用户在填写表单,填写完毕你却告诉我重新登陆,确定用户不会删掉你的小程序???

有人说了  异常退出 我会本地缓存填写的表单内容,当然你要是能接受这种我也无话可说!!!

我们要做的是无痛刷新toekn,那么首先要在401拦截的时候去重新登陆获取新的token

继续优化改造

import store from "../store/index.js";
class HttpClient {
  /**
   * Create a new instance of HttpClient.
   */
  constructor() {
    this.interceptors = {
      request: [],
      response: []
    };
  }

  /**
   * Sends a single request to server.
   *
   * [@param](/user/param) {Object} options - Coming soon.
   */
  sendRequest(options) {
    let requestOptions = options;

    if (!requestOptions.header) {
      requestOptions.header = {};
    }

    // 重新设置 Accept 和 Content-Type
    requestOptions.header = Object.assign(
      {
        Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=utf-8'
      },
      requestOptions.header
    );
    this.interceptors.request.forEach((interceptor) => {
      const request = interceptor(requestOptions);
      requestOptions = request.options;
    });

    // 将以 Promise 返回数据, 无 success、fail、complete 参数
    // let response = uni.request(requestOptions);

    // 使用Promise包装一下, 以 complete方式来接收接口调用结果
    let response = new Promise((resolve, reject) => {
      requestOptions.complete = (res) => {
        const { statusCode } = res;
        const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
        if(statusCode==401){ //拦截401请求
          store
            .dispatch("auth/login")    //调用vueX中的登陆将登陆信息保存到VueX
            .then(()=>{ //提示用户刚才操作无效重新操作一次
              uni.showToast({
                title: '请重新操作',
                duration: 2000,
                icon: "none",
              });
            })
            .catch(()=>{
              uni.showToast({
                title: '账户异常请重启程序',
                duration: 2000,
                icon: "none",
              }); 
            })
        }
        if (isSuccess) {
          if(res.data.code==1){
            resolve(res.data);
          }else{
            reject(res);
          }
        } else {
          reject(res);
        }
      };
      requestOptions.requestId = new Date().getTime();
      uni.request(requestOptions);
    });

    this.interceptors.response.forEach((interceptor) => {
      response = interceptor(response);
    });

    return response;
  }
}

export default HttpClient;

到此我们实现的在401错误时候去重新登陆获取新的token,且告知用户重新操作一次

到此你会发现一个问题,当页面存在一个请求,目前方案毫无问题,但是当存在两个、三个、四个请求,你会骂娘

失败3个请求会重新调用3次登陆会刷新3次token

那么此时要做的就是保证不多次登陆

思路加一个开关,当在登陆过程中后续错误不再走登陆接口

import store from "../store/index.js";

// 是否正在重新登陆刷新的标记
var loginRefreshing = false

class HttpClient {
  /**
   * Create a new instance of HttpClient.
   */
  constructor() {
    this.interceptors = {
      request: [],
      response: []
    };
  }

  /**
   * Sends a single request to server.
   *
   * [@param](/user/param) {Object} options - Coming soon.
   */
  sendRequest(options) {
    let requestOptions = options;

    if (!requestOptions.header) {
      requestOptions.header = {};
    }

    // 重新设置 Accept 和 Content-Type
    requestOptions.header = Object.assign(
      {
        Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=utf-8'
      },
      requestOptions.header
    );
    this.interceptors.request.forEach((interceptor) => {
      const request = interceptor(requestOptions);
      requestOptions = request.options;
    });

    // 将以 Promise 返回数据, 无 success、fail、complete 参数
    // let response = uni.request(requestOptions);

    // 使用Promise包装一下, 以 complete方式来接收接口调用结果
    let response = new Promise((resolve, reject) => {
      requestOptions.complete = (res) => {
        const { statusCode } = res;
        const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
        if(statusCode==401){ //拦截401请求
          if(!loginRefreshing){//防止重复登陆
            loginRefreshing = true
            store
              .dispatch("auth/login")    //调用vueX中的登陆将登陆信息保存到VueX
              .then(()=>{ //提示用户刚才操作无效重新操作一次
                uni.showToast({
                  title: '请重新操作',
                  duration: 2000,
                  icon: "none",
                });
              })
              .catch(()=>{
                uni.showToast({
                  title: '账户异常请重启程序',
                  duration: 2000,
                  icon: "none",
                }); 
              })
              .finally(()=>{
                //销毁 是否正在重新登陆刷新的标记
                loginRefreshing = false
              });
            }
        }
        if (isSuccess) {
          if(res.data.code==1){
            resolve(res.data);
          }else{
            reject(res);
          }
        } else {
          reject(res);
        }
      };
      requestOptions.requestId = new Date().getTime();
      uni.request(requestOptions);
    });

    this.interceptors.response.forEach((interceptor) => {
      response = interceptor(response);
    });

    return response;
  }
}

export default HttpClient;

我们可以看到在遇到两个401错误时候并没有请求两次login,只请求一次,到此刷新token算是完成了,但是需要用户配合去重新操作一次,还不是真正的无痛刷线token,做到用户无感知

思路:将请求401的请求缓存起来,在重新登陆完成之后再将缓存中的请求重新发出,

废话不多说直接上代码

import AuthService from "@/services/auth.service";
import store from "../store/index.js";
// 是否正在重新登陆刷新的标记
var loginRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []

class HttpClient {
  /**
   * Create a new instance of HttpClient.
   */
  constructor() {
    this.interceptors = {
      request: [],
      response: []
    };
  }

  /**
   * Sends a single request to server.
   *
   * [@param](/user/param) {Object} options - Coming soon.
   */
  sendRequest(options) {
    let requestOptions = options;

    if (!requestOptions.header) {
      requestOptions.header = {};
    }

    // 重新设置 Accept 和 Content-Type
    requestOptions.header = Object.assign(
      {
        Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=utf-8'
      },
      requestOptions.header
    );
    this.interceptors.request.forEach((interceptor) => {
      const request = interceptor(requestOptions);
      requestOptions = request.options;
    });

    // 将以 Promise 返回数据, 无 success、fail、complete 参数
    // let response = uni.request(requestOptions);

    // 使用Promise包装一下, 以 complete方式来接收接口调用结果
    let response = new Promise((resolve, reject) => {
			let timeId = setTimeout(()=>{
			  reject({statusCode:504});
			},10000)
      requestOptions.complete = (res) => {
        clearTimeout(timeId)
        const { statusCode } = res;
        const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
        if(statusCode==401){ //无痛刷新token
          if(!loginRefreshing){//防止重复登陆
            loginRefreshing = true
            store.dispatch("auth/logout");
            store
              .dispatch("auth/login")
              .then(()=>{
                //所有存储到对列组中的请求重新执行。
                requests.forEach(callback=>{
                  callback(AuthService.getToken() ? AuthService.getToken() : "")
                })
                //重试队列清空
                requests = []
              })
              .catch(()=>{
                uni.showToast({
                  title: '账户异常请重启程序',
                  duration: 2000,
                  icon: "none",
                }); 
              })
              .finally(()=>{
                //销毁 是否正在重新登陆刷新的标记
                loginRefreshing = false
              });
          }
          return new Promise((resolve) => {
            // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
            requests.push((token) => {
              requestOptions.header.token = token //带着登陆后的新token
              resolve(uni.request(requestOptions))
            })
          })
        }
        if (isSuccess) {
          if(res.data.code==1){
            resolve(res.data);
          }else{
            reject(res);
          }
        } else {
          reject(res);
        }
      };
      requestOptions.requestId = new Date().getTime();
      uni.request(requestOptions);
    });

    this.interceptors.response.forEach((interceptor) => {
      response = interceptor(response);
    });

    return response;
  }
}

export default HttpClient;

知识点,在重新获取新的token后要将缓存起来的请求中的token替换为重新登陆后新的token

到此无痛刷新token续接401请求的方法已经处理完毕,在用户提交表单时候遇到token失效重新获取新的token再续接表单请求,此时用户毫无感知,可能在请求时间上多了延迟,体验好感度99+,哈哈哈哈哈     

到此__无痛刷新token续接401__已经完成请求快去试试吧

提示:有些后端在接口请求做了签名,记得像更换token一样在重新登陆完成之后更换新的时间戳新的签名等字段

附上本请求封装的uniapp基础性脚手架 https://github.com/wkiwi/uni-app-base

回到顶部