Refresh token trong Axios như thế nào?

Sau đây, mình sẽ chia sẻ 1 tính năng đó là refresh token mà mình đã thực hiện trong dự án của mình. Mình nghĩ sẽ có nhiều dự án sẽ cần đến, sẽ giúp đến bạn :))))

Về việc triển khai refresh token có thể không còn xa lạ đối với nhiều Frontend Dev trong chúng ta. Sau khi đăng nhập thành công, token sẽ được trả lại từ API (Thông thường api sẽ trả về access_tokenrefresh_token).

Chúng ta sẽ lưu trữ nó lại ở localStorage, cookie, etc… để đính vào headers của mỗi request cho việc xác thực các request mà chúng ta gửi đi.

Khi token này hết hạn, chúng ta sẽ cần gửi một yêu cầu để lấy một token mới bằng việc gửi token hết hạn trước đó hoặc mã refresh_token ban đầu được trả về từ API. Việc này phụ thuộc vào thiết kế API đó.

Trước khi biết khi nào token hết hạn, chúng ta cần đính token vào mỗi request gửi đi. Bạn có thể hình dung việc chúng ta đính token vào mỗi request như thế này, để đảm bảo chúng ta luôn gửi token đến API để xác thực và biết khi nào token sẽ hết hạn (Tránh tự ý kiểm tra token ở client, việc đó không thực sự an toàn).

JavaScript
const axiosClient = axios.create({
  baseURL: process.env.PR_API_HOST,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add a request interceptor
axiosClient.interceptors.request.use(
  (config) => {
    const storage = useStorage();
    const accessToken = storage.getItem(StorageKeyEnum.AccessToken);
  
    if (accessToken && config.headers) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

Khi mỗi request cần xác thực được gửi đi với token kèm theo, chúng ta sẽ biết token đó còn hợp lệ hay không bằng việc API sẽ giúp chúng ta xác minh token đó. khi token hết hạn, thông thường chúng ta sẽ nhận được mã phản hồi là 401 hoặc 403.

Khi đó, chúng ta sẽ kiểm tra trên mỗi response nhận về. Nếu nó rơi vào các mã lỗi trên, thực hiện kịch bản refresh token mà chúng ta muốn. Ở bên dưới đoạn code này, hiện tại dự án của mình khi hết hạn token trả về mã lỗi là 401 và kèm theo 1 message Signature has expired nên mình đang thực hiện check theo điều kiện như vậy nhé =)))

JavaScript
axiosClient.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    let message = '';
    const originalConfig = error.config;
    const code = error.response && Number(error.response.status);
    switch (code) {
      case 401:
        if (error.response.data.error === RESPONSE_ERROR_MESSAGE.SIGNATURE_HAS_EXPIRED) {
          // Chúng ta sẽ refresh token tại đây
        break;
      case ...:
        break;
      default:
        message = ErrorHttpMessageEnum.Default;
      
    }
    return message;
  },
);

Tiếp theo, để đảm bảo chỉ một request refresh token được tạo ra khi nhiều request với token hết hạn phát sinh khi tải trang, chúng ta sẽ cần sử dụng Promise để delay và chỉ tạo duy nhất một yêu cầu cuối cùng. Kèm theo đó, là kiểm tra xem có request refresh token nào đã được gọi thông qua biến trạng thái isRefreshing

JavaScript
// refresh token is false, to call refresh token api
if (!isRefreshing) {
// update status isRefreshing to be true
  isRefreshing = true;
  // this method fetches the new token
  refreshTokensLoginApi().then(() => {
    const storage = useStorage();
    const accessToken = storage.getItem(StorageKeyEnum.AccessToken);
    onRefreshed(accessToken);
    // after that, to clear all setup
    refreshSubscribers = [];
    isRefreshing = false;
  });
}

// setup callback to change token in headers authorization
const retryOrigReq = new Promise((resolve) => {
  subscribeTokenRefresh((token: string) => {
    // replace the expired token and retry
    originalConfig.headers.Authorization = `Bearer ${token}`;
    resolve(axiosClient(originalConfig));
  });
});

return retryOrigReq;

const subscribeTokenRefresh = (cb: (token: string) => void) => {
  refreshSubscribers.push(cb);
};

const onRefreshed = (token: string) => {
  refreshSubscribers.map((cb) => cb(token));
};

Ở đây, useStorage() là mình đang viết 1 hook để tương tác get, set, remove token với các storage. Mục đích mình viết hooks để mình tái sử dụng lại cho được nhiều components trong dự án đó, thay vì mình phải viết lại từng nơi. Nó giúp code trong dự án mình tường minh, dễ quản lý hơn, tránh lặp lại code.

Tóm gọn lại, chúng ta sẽ có một hàm refresh token như bên dưới.

JavaScript
import axios from 'axios';
import {
  ErrorHttpMessageEnum, StorageKeyEnum, RESPONSE_ERROR_MESSAGE
} from 'src/constant';
import { refreshTokensLoginApi, clearTokenRedirectLogin } from 'src/helpers/auth';
import { useStorage } from 'src/hooks/useStorage';

let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];

const axiosClient = axios.create({
  baseURL: process.env.PR_API_HOST,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add a request interceptor
axiosClient.interceptors.request.use(
  (config) => {
    const storage = useStorage();
    const accessToken = storage.getItem(StorageKeyEnum.AccessToken);
    
    if (accessToken && config.headers) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Add a response interceptor
axiosClient.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    let message = '';
    const originalConfig = error.config;
    const code = error.response && Number(error.response.status);
    
    switch (code) {
      case 401:
        if (error.response.data.error === RESPONSE_ERROR_MESSAGE.SIGNATURE_HAS_EXPIRED) {
          if (!isRefreshing) {
            isRefreshing = true;
            refreshTokensLoginApi().then(() => {
              const storage = useStorage();
              const accessToken = storage.getItem(StorageKeyEnum.AccessToken);
              onRefreshed(accessToken);
              
              isRefreshing = false;
              refreshSubscribers = [];
            });
          }

          const retryOrigReq = new Promise((resolve) => {
            subscribeTokenRefresh((token: string) => {
              // replace the expired token and retry
              originalConfig.headers.Authorization = `Bearer ${token}`;
              resolve(axiosClient(originalConfig));
            });
          });

          return retryOrigReq;
        }
        clearTokenRedirectLogin();

        message = ErrorHttpMessageEnum.Code401;
        break;
      case ...:
        break;
      default:
        message = ErrorHttpMessageEnum.Default;
    }
    return message;
  },
);

const subscribeTokenRefresh = (cb: (token: string) => void) => {
  refreshSubscribers.push(cb);
};

const onRefreshed = (token: string) => {
  refreshSubscribers.map((cb) => cb(token));
};

export default axiosClient;

Kết luận

Có rất nhiều cách để chúng ta triển khai refresh token và phía trên là một trong số cách hiện tại của mình đang sử dụng để refresh token trong dự án. Cảm ơn mọi người đã dành thời gian để đọc 🙏

Have a nice day!

0 Shares:
1 comment
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like