關於無感刷新 Token,我是這樣子做的

作者:_island

https://juejin.cn/post/7170278285274775560

什麼是 JWT

JWT是全稱是JSON WEB TOKEN,是一個開放標準,用於將各方數據信息作爲 JSON 格式進行對象傳遞,可以對數據進行可選的數字加密,可使用RSAECDSA進行公鑰 / 私鑰簽名。

使用場景

JWT最常見的使用場景就是緩存當前用戶登錄信息,當用戶登錄成功之後,拿到JWT,之後用戶的每一個請求在請求頭攜帶上Authorization字段來辨別區分請求的用戶信息。且不需要額外的資源開銷。

相比傳統 session 的區別

比起傳統的session認證方案,爲了讓服務器能識別是哪一個用戶發過來的請求,都需要在服務器上保存一份用戶的登錄信息(通常保存在內存中),再與瀏覽器的cookie打交道。

爲什麼說 JWT 不需要額外的開銷

JWT爲三個部分組成,分別是HeaderPayloadSignature,使用.符號分隔。

// 像這樣子
xxxxx.yyyyy.zzzzz

標頭 header

標頭是一個JSON對象,由兩個部分組成,分別是令牌是類型(JWT)和簽名算法(SHA256RSA

{
  "alg""HS256",
  "typ""JWT"
}

負荷 payload

負荷部分也是一個JSON對象,用於存放需要傳遞的數據,例如用戶的信息

{
  "username""_island",
  "age"18
}

此外,JWT 規定了 7 個可選官方字段(建議)

toAt4b

簽章 signature

這一部分,是由前面兩個部分的簽名,防止數據被篡改。在服務器中指定一個密鑰,使用標頭中指定的簽名算法,按照下面的公式生成這簽名數據

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

在拿到簽名數據之後,把這三個部分的數據拼接起來,每個部分中間使用.來分隔。這樣子我們就生成出一個了JWT數據了,接下來返回給客戶端儲存起來。而且客戶端在發起請求時,攜帶這個JWT在請求頭中的Authorization字段,服務器通過解密的方式即可識別出對應的用戶信息。

JWT 優勢和弊端

優勢

弊端

關於 refreshToken

refreshTokenOauth2認證中的一個概念,和accessToken一起生成出來的。

當用戶攜帶的這個accessToken過期時,用戶就需要在重新獲取新的accessToken,而refreshToken就用來重新獲取新的accessToken的憑證。

爲什麼要有 refreshToken

當你第一次接觸的時候,你有沒有一個這樣子的疑惑,爲什麼需要refreshToken這個東西,而不是服務器端給一個期限較長甚至永久性的accessToken呢?

抱着這個疑惑我在網上搜尋了一番,

其實這個accessToken的使用期限有點像我們生活中的入住酒店,當我們在入住酒店時,會出示我們的身份證明來登記獲取房卡,此時房卡相當於accessToken,可以訪問對應的房間,當你的房卡過期之後就無法再開啓房門了,此時就需要再到前臺更新一下房卡,才能正常進入,這個過程也就相當於refreshToken

accessToken使用率相比refreshToken頻繁很多,如果按上面所說如果accessToken給定一個較長的有效時間,就會出現不可控的權限泄露風險。

使用 refreshToken 可以提高安全性

總體來說有了refreshToken可以降低accessToken被盜的風險

關於 JWT 無感刷新 TOKEN 方案(結合 axios)

業務需求

在用戶登錄應用後,服務器會返回一組數據,其中就包含了accessTokenrefreshToken,每個accessToken都有一個固定的有效期,如果攜帶一個過期的token向服務器請求時,服務器會返回 401 的狀態碼來告訴用戶此token過期了,此時就需要用到登錄時返回的refreshToken調用刷新Token的接口(Refresh)來更新下新的token再發送請求即可。

話不多說,先上代碼

工具

axios作爲最熱門的http請求庫之一,我們本篇文章就藉助它的錯誤響應攔截器來實現token無感刷新功能。

具體實現

本次基於 axios-bz[1] 代碼片段封裝響應攔截器 可直接配置到你的項目中使用 ✈️ ✈️

利用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);
};

參考資料

[1] axios-bz: https://github.com/QC2168/axios-bz

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Pt3ySxBWCmNswYsKIebPgQ