为什么要有refreshToken

一口鸭梨一口鸭梨 -
为什么要有refreshToken

当你第一次接触的时候,你有没有一个这样子的疑惑,为什么需要refreshToken这个东西,而不是服务器端给一个期限较长甚至永久性的accessToken呢?
抱着这个疑惑我在网上搜寻了一番,
其实这个accessToken的使用期限有点像我们生活中的入住酒店,当我们在入住酒店时,会出示我们的身份证明来登记获取房卡,此时房卡相当于accessToken,可以访问对应的房间,当你的房卡过期之后就无法再开启房门了,此时就需要再到前台更新一下房卡,才能正常进入,这个过程也就相当于refreshToken。
accessToken使用率相比refreshToken频繁很多,如果按上面所说如果accessToken给定一个较长的有效时间,就会出现不可控的权限泄露风险。
使用refreshToken可以提高安全性

用户在访问网站时,accessToken被盗取了,此时攻击者就可以拿这个accessToke访问权限以内的功能了。如果accessToken设置一个短暂的有效期2小时,攻击者能使用被盗取的accessToken的时间最多也就2个小时,除非再通过refreshToken刷新accessToken才能正常访问。

设置accessToken有效期是永久的,用户在更改密码之后,之前的accessToken也是有效的

总体来说有了refreshToken可以降低accessToken被盗的风险
关于JWT无感刷新TOKEN方案(结合axios)
业务需求
在用户登录应用后,服务器会返回一组数据,其中就包含了accessToken和refreshToken,每个accessToken都有一个固定的有效期,如果携带一个过期的token向服务器请求时,服务器会返回401的状态码来告诉用户此token过期了,此时就需要用到登录时返回的refreshToken调用刷新Token的接口(Refresh)来更新下新的token再发送请求即可。
话不多说,先上代码
工具
axios作为最热门的http请求库之一,我们本篇文章就借助它的错误响应拦截器来实现token无感刷新功能。
具体实现

本次基于axios-bz代码片段封装响应拦截器
可直接配置到你的项目中使用 ✈️ ✈️

利用interceptors.response,在业务代码获取到接口数据之前进行状态码401判断当前携带的accessToken是否失效。
下面是关于interceptors.response中异常阶段处理内容。当响应码为401时,响应拦截器会走中第二个回调函数onRejected

下面代码分段可能会让大家阅读起来不是很顺畅,我直接把整份代码贴在下面,且每一段代码之间都添加了对应的注释

// 最大重发次数
const MAX_ERROR_COUNT = 5;
// 当前重发次数
let currentCount = 0;
// 缓存请求队列
const queue: ((t: string) => any)[] = [];
// 当前是否刷新状态
let isRefresh = false;

export default async (error: AxiosError<ResponseDataType>) => {
const statusCode = error.response?.status;
const clearAuth = () => {

console.log('身份过期,请重新登录');
window.location.replace('/login');
// 清空数据
sessionStorage.clear();
return Promise.reject(error);

};
// 为了节省多余的代码,这里仅展示处理状态码为401的情况
if (statusCode === 401) {

// accessToken失效
// 判断本地是否有缓存有refreshToken
const refreshToken = sessionStorage.get('refresh') ?? null;
if (!refreshToken) {
  clearAuth();
}
// 提取请求的配置
const { config } = error;
// 判断是否refresh失败且状态码401,再次进入错误拦截器
if (config.url?.includes('refresh')) {
clearAuth();
}
// 判断当前是否为刷新状态中(防止多个请求导致多次调refresh接口)
if (!isRefresh) {
  // 设置当前状态为刷新中
  isRefresh = true;
  // 如果重发次数超过,直接退出登录
  if (currentCount > MAX_ERROR_COUNT) {
    clearAuth();
  }
  // 增加重试次数
  currentCount += 1;

  try {
    const {
      data: { access },
    } = await UserAuthApi.refreshToken(refreshToken);
    // 请求成功,缓存新的accessToken
    sessionStorage.set('token', access);
    // 重置重发次数
    currentCount = 0;
    // 遍历队列,重新发起请求
    queue.forEach((cb) => cb(access));
    // 返回请求数据
    return ApiInstance.request(error.config);
  } catch {
    // 刷新token失败,直接退出登录
    console.log('请重新登录');
    sessionStorage.clear();
    window.location.replace('/login');
    return Promise.reject(error);
  } finally {
    // 重置状态
    isRefresh = false;
  }
} else {
  // 当前正在尝试刷新token,先返回一个promise阻塞请求并推进请求列表中
  return new Promise((resolve) => {
    // 缓存网络请求,等token刷新后直接执行
    queue.push((newToken: string) => {
      Reflect.set(config.headers!, 'authorization', newToken);
      // @ts-ignore
      resolve(ApiInstance.request<ResponseDataType<any>>(config));
    });
  });
}

}

return Promise.reject(error);
};
复制代码
抽离代码
把上面关于调用刷新token的代码抽离成一个refreshToken函数,单独处理这一情况,这样子做有利于提高代码的可读性和维护性,且让看上去代码不是很臃肿
// refreshToken.ts
export default async function refreshToken(error: AxiosError<ResponseDataType>) {

/* 
将上面 if (statusCode === 401) 中的代码贴进来即可,这里就不重复啦
代码仓库地址: https://github.com/QC2168/axios-bz/blob/main/Interceptors/hooks/refreshToken.ts
*/

}
复制代码
经过上面的逻辑抽离,现在看下拦截器中的代码就很简洁了,后续如果要调整相关逻辑直接在refreshToken.ts文件中调整即可。
import refreshToken from './refreshToken.ts'
export default async (error: AxiosError<ResponseDataType>) => {
const statusCode = error.response?.status;

// 为了节省多余的代码,这里仅展示处理状态码为401的情况
if (statusCode === 401) {

refreshToken()

}

return Promise.reject(error);
};

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

ruby-on-railsnode.jsspring

扩展阅读

加个好友,技术交流

1628738909466805.jpg