iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Vue.js

打造銷售系統30天修練 - 全集中・Vue之呼吸系列 第 23

Day 23:[APIの呼吸・貳之型] 錯誤處理 - 攔截器與重試機制

  • 分享至 

  • xImage
  •  

在上一回的修煉中,我們打造了 axios 客戶端。它配備的「回應攔截器」可以在偵測到 401 錯誤時,果斷地將使用者登出並踢回登入頁。這確保了系統的安全性,但從使用者體驗的角度來看,卻顯得有些不近人情。

想像一下,你正在填寫一份複雜的表單,就在點擊「送出」的那一刻,因為 Access Token 剛好過期,你就被強制登出了!

今天,我們將改造攔截器,讓它在遇到 401 錯誤時,不先急著放棄,而是嘗試「自動刷新 Token,並重試失敗的請求」,打造無縫的使用者體驗。

Access Token 與 Refresh Token

要實現「自動刷新」,我們首先需要理解一個在現代驗證機制中非常重要的模式:Access Token + Refresh Token 的組合。

  • Access Token (存取權杖)

    • 壽命短:通常只有 15 分鐘到 1 小時。
    • 用途:用於存取受保護的資源(例如:獲取使用者資料、訂單列表等)。
    • 風險:因為壽命短,即使被竊取,駭客能搞破壞的時間也有限。
  • Refresh Token (刷新權杖)

    • 壽命長:可以是 7 天、30 天,甚至更久。
    • 用途它的唯一用途,就是去換取一個新的 Access Token。它不能直接用來存取資料。
    • 儲存:因為它很敏感,通常會被儲存在更安全的地方,例如後端的資料庫,或是前端的 HttpOnly Cookie 中。

它們的流程如下:

  1. 使用者登入時,後端同時簽發一個短命的 Access Token 和一個長壽的 Refresh Token
  2. 前端在每次請求 API 時,都帶上 Access Token
  3. Access Token 過期,後端回傳 401 錯誤。
  4. 前端的攔截器捕捉到 401,立刻發起一個特殊的 API 請求(例如 /api/auth/refresh),並帶上 Refresh Token
  5. 後端驗證 Refresh Token,如果有效,就簽發一個新的 Access Token 並回傳給前端。
  6. 前端拿到新的 Access Token 後,用它來重新發起剛才失敗的那個請求
  7. 請求成功,使用者完全不知道背後發生了這一切。

方案一:手動實現重試機制

我們可以嘗試在 axios 的回應攔截器中手動實現這個邏輯。這會讓我們更深入地理解其複雜性。

// src/api/client.js 

// ...
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    // 檢查是否是 401 錯誤,且不是因為刷新 token 本身失敗的
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true; // 標記為已重試
      
      try {
        // 呼叫刷新 token 的 API
        const { data } = await apiClient.post('/auth/refresh', { 
          refreshToken: getRefreshTokenFromSomewhere() 
        });
        
        const newAccessToken = data.accessToken;
        const authStore = useAuthStore();
        
        // 更新 Pinia 和 localStorage 中的 token
        authStore.setAccessToken(newAccessToken);
        
        // 更新原始請求的標頭
        originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
        
        // 重新發送原始請求
        return apiClient(originalRequest);

      } catch (refreshError) {
        // 如果刷新 token 也失敗了,表示 refresh token 也過期了
        // 此時才真正需要將使用者登出
        const authStore = useAuthStore();
        authStore.logout();
        router.push({ name: 'login' });
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

這個實作看起來可行,但它有一個潛在的巨大問題:併發請求 (Concurrent Requests)

想像一下,在 Access Token 過期的瞬間,頁面上有 3 個 API 請求同時發出。它們會同時收到 401,然後觸發 3 次刷新 Token 的請求,這會造成資源浪費和潛在的競態條件 (Race Condition)。

要解決這個問題,需要更複雜的邏輯(例如:用一個變數鎖定刷新狀態、用一個佇列儲存待重試的請求),手動實現起來非常繁瑣且容易出錯。

幸運的是,社群已經為我們提供了完美的解決方案。

方案二:使用 axios-auth-refresh (推薦)

axios-auth-refresh 是一個專門用來處理 axios 中認證刷新邏輯的函式庫。它優雅地解決了我們上面提到的所有問題,包括並發請求。

1. 安裝函式庫

npm install axios-auth-refresh

2. 改造 client.js

使用它非常簡單,我們只需要提供一個「刷新邏輯函式」給它即可。

// src/api/client.js
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import { useAuthStore } from '../stores/auth';
import router from '../router';

// 建立 axios 實例 (與之前相同)
const apiClient = axios.create({ ... });

// 定義「刷新邏輯函式」
const refreshAuthLogic = async (failedRequest) => {
  const authStore = useAuthStore();
  try {
    // 這裡假設我們有一個 API 可以用 refresh token 換取新的 access token
    // 並且 refresh token 是安全地儲存在 HttpOnly Cookie 中,所以請求會自動帶上
    const response = await axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/refresh`);
    const newAccessToken = response.data.accessToken;

    // 更新 Pinia Store
    authStore.setAccessToken(newAccessToken); 

    // 更新失敗請求的標頭並重新發送
    failedRequest.response.config.headers.Authorization = `Bearer ${newAccessToken}`;
    return Promise.resolve();

  } catch (error) {
    // 如果刷新也失敗,就執行登出
    authStore.logout();
    router.push({ name: 'login' });
    return Promise.reject(error);
  }
};

// 將攔截器應用到我們的 axios 實例上
createAuthRefreshInterceptor(apiClient, refreshAuthLogic);

// 我們不再需要自己寫複雜的回應攔截器來處理 401
// 但可以保留它來處理 401 以外的其他錯誤
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status !== 401) {
      // 在這裡處理 401 以外的錯誤
      console.error(`發生錯誤: ${error.response.status}`);
    }
    return Promise.reject(error);
  }
);

// 請求攔截器保持不變
apiClient.interceptors.request.use(...);

export default apiClient;

這樣一來,程式碼變得簡潔,我們只需要專注於「如何刷新 Token」的邏輯,所有關於攔截、重試、處理並發請求的複雜工作,axios-auth-refresh 都幫我們處理好了。

總結

今天,我們的 API 處理能力又提升了一個檔次。我們學到了:

  1. Access Token + Refresh Token 模式是兼顧安全與體驗的現代驗證標準。
  2. axios 攔截器中實現「自動重試」是可行的,但需要處理棘手的並發請求問題。
  3. 使用 axios-auth-refresh 函式庫,可以極大地簡化認證刷新與請求重試的邏輯,是專業專案中的首選方案。

透過這個無縫的重試機制,再也不會因為短暫的 Token 過期而粗暴地打斷使用者了。

明日,Day 24:[Systemの呼吸・壹之型] Product列表 - 商品展示與搜尋。心を燃やせ 🔥!


上一篇
Day 22:[APIの呼吸・壹之型] Backend連接 - Axios設定與封裝
下一篇
Day 24:[Systemの呼吸・壹之型] 庫存查詢 - 模擬資料與搜尋功能
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言