前端双token校验,以及处理过程

1.背景

        由于业务需求,需要对用户信息进行安全性配置,就出现了短token--accessToken,那么短token通常几十分钟就会失效,降低了用户体验,此时后端又返回了一个长token--refreshToken,用来刷新accessToken,提高用户体感,减少用户登录的次数,达到无感刷新的境界。

2.编写逻辑

        登录过后,后端会返回来一个accessToken,refreshToken。我们将这两个token已合适的方式存储起来,accessToken通常设置为请求头header,作为接口鉴权所用,当用户发起请求的时候,后端发现此时的accessToken已经过期了就返回401状态码(状态码看后端程序设定,针对不同做处理),我们就拿refreshToken作为请求参数,调用刷新token令牌的接口,刷新完token后,会返回新的accessToken,这时候要替换掉header中旧的那个accessToken,然后再发起之前的原始请求。此时就达到了双token之间的替换以及无感刷新。

3.具体步骤

3.1 定义一个处理AccessToken失效的函数
 

async function handleAccessTokenError(originalRequest) {
  if(!isRefreshing){
    isRefreshing = true;
    try {
      const refreshToken = getRefreshToken();
      const resp = await axios.post('/coolfly/app/user/refresh-token', { refreshToken });
      if (resp.code === 0) {
        setAccessToken(resp.data?.accessToken);
        onRefreshed(resp.data?.accessToken);
        if (originalRequest) {
          originalRequest.headers['Hc-Authorization'] = resp.data?.accessToken;
          return axios(originalRequest);
        }
      } else if (resp.code === 5) { // RefreshToken 未授权
        removeToken();
        navigateToLogin();
      }
    } catch (refreshError) {
      console.error('Refresh token failed:', refreshError);
      removeToken();
      navigateToLogin();
    } finally {
      isRefreshing = false;
    }
  }

  //重新拿到accessToken后,重新发起请求
  const retryOriginalRequest  =  new Promise((resolve) => {
    addRefreshSubscriber((token) => {
      originalRequest.headers['Hc-Authorization'] = token;
      resolve(service(originalRequest));
    });
  })
  return retryOriginalRequest;
}
  • isRefreshing:用来避免在刷新令牌的过程中发起多个请求。
  • RefreshToken流程
    1. 检查RefreshToken是否存在。
    2. 如果存在,使用它来请求新的AccessToken
    3. 成功获取新令牌后,更新存储的AccessToken并重新发送原始请求。
    4. 如果RefreshToken也失效,移除令牌并重定向到登录页面。

3.2 辅助函数编写,提高代码可读性
 

function onRefreshed(token) {
  refreshSubscribers.map((callback) => callback(token));
  refreshSubscribers = [];
}

function addRefreshSubscriber(callback) {
  refreshSubscribers.push(callback);
}

function hintMsg(msg) {
  const translatedMessage = i18n.t(msg);
  message.info(translatedMessage);
}

function navigateToLogin() {
  window.location.replace('/#');
}
详细解释:
  • onRefreshed:用于通知所有等待中的请求刷新后的新令牌。
  • addRefreshSubscriber:将请求加入等待队列,以便在令牌刷新后继续处理。
  • hintMsg:用于显示国际化提示消息。
  • navigateToLogin:用于跳转到登录页面。

3.3 request源文件代码如下(去掉一些无关紧要的业务代码):

import axios from "axios";
import { message } from "antd";
import Config from "@/settings.js";
import {  getUserInfo, getAccessToken, setAccessToken, getRefreshToken, removeToken } from "@/utils/auth.js";
import store from "@/store";
import { SetUserInfo } from "@/store/modules/auth/action.js";
import { encrypt } from '@/utils/cryptojs.js'
import i18n from 'i18next';

let isRefreshing = false;
let refreshSubscribers = [];

function onRefreshed(token) {
  refreshSubscribers.map((callback) => callback(token));
  refreshSubscribers = [];
}

function addRefreshSubscriber(callback) {
  refreshSubscribers.push(callback);
}

function hintMsg(msg) {
  const translatedMessage = i18n.t(msg);
  message.info(translatedMessage);
}

// 创建axios实例
const service = axios.create({
  baseURL: '/api', // api 的 base_url
  // baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: Config.timeout, // 请求超时时间
});

// request拦截器
service.interceptors.request.use(
  (config) => {
    // 设置通用请求头
   
    config.headers['Content-Type'] = 'application/json';
    // 根据token设置Authorization头
    // const { accessToken } = getUserInfo()
    if (getAccessToken()) {
      const userInfo = getUserInfo();
      config.headers['Authorization'] = getAccessToken() || userInfo.accessToken;
      config.headers['Uid'] = userInfo.uid;
    }
    //加密请求  需要加密的接口路径
    const encryptedApis = [
      '/api/app/user/login',
      '/api/app/user/refresh-token',
      '/api/app/user/reset-pwd',
      '/api/app/user/register',
      '/api/app/auth/aws-s3/upload',  //文件上传
      '/api/app/auth/aws-s3/get/ps',  //文件预签名
    ];
    if (encryptedApis.includes(config.url)) {

      config.headers['Content-Type'] = 'text/plain;charset=UTF-8.';
      if (config.data) {
        config.data = encrypt(config.data);
      }
    }
    return config;
  }
);

// response 拦截器
service.interceptors.response.use(
  (response) => {
    const {data, status } = response;
    const { code, message}  = data;
    if (code === 0 && status === 200) {
      return data;
    }else {
      hintMsg(message);
      handleErrorCode(code, message, response.config)
      return Promise.reject(data);
    }
  },
  async (error) => {
      const responseCode = error.response?.status || error.response?.data;
      if (responseCode !== undefined) {
        await handleErrorCode(responseCode, error.response?.message, error.config);
      } else {
        hintMsg("serverError");
      }
      return Promise.reject(error);
  }
);

async function handleErrorCode(code, message, originalRequest = null) {
  switch (code) {
    case 1: // AccessToken 未授权
      // hintMsg("UserError");
      return await handleAccessTokenError(originalRequest);
    case 5: // RefreshToken 未授权
      message.error(message)
      removeToken();
      navigateToLogin();
      break;
    case 98: // 服务器繁忙
      hintMsg("serverError");
      break;
    case 99: // 服务器内部错误
      store.dispatch(SetUserInfo(null));
      removeToken();
      message.error(message)
      hintMsg("serverError");
      break;
    default:
      hintMsg(message || "serverError");
      break;
  }
}

async function handleAccessTokenError(originalRequest) {
  if(!isRefreshing){
    isRefreshing = true;
    try {
      const refreshToken = getRefreshToken();
      const resp = await axios.post('/api/app/user/refresh-token', { refreshToken });
      if (resp.code === 0) {
        setAccessToken(resp.data?.accessToken);
        onRefreshed(resp.data?.accessToken);
        if (originalRequest) {
          originalRequest.headers['Authorization'] = resp.data?.accessToken;
          return axios(originalRequest);
        }
      } else if (resp.code === 5) { // RefreshToken 未授权
        removeToken();
        navigateToLogin();
      }
    } catch (refreshError) {
      console.error('Refresh token failed:', refreshError);
      removeToken();
      navigateToLogin();
    } finally {
      isRefreshing = false;
    }
  }

  //重新拿到accessToken后,重新发起请求
  const retryOriginalRequest  =  new Promise((resolve) => {
    addRefreshSubscriber((token) => {
      originalRequest.headers['Authorization'] = token;
      resolve(service(originalRequest));
    });
  })
  return retryOriginalRequest;
  
}

function navigateToLogin() {
  window.location.replace('/#')
}

export default service;

总结

通过以上步骤,我们实现了一个双Token验证的流程。该流程包括AccessTokenRefreshToken的管理、请求拦截和响应处理,以及错误处理和提示。这样的配置能够有效提升应用的安全性和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值