+2

Kịch Bản 15 Phút: Múa Màn Refresh Token Mượt Như Lụa Cùng Axios Interceptor

1. Chiến thuật "Song Kiếm Hợp Bích" (Hai Token)

Khi user đăng nhập thành công, API Login không trả về 1, mà trả về 2 cái token:

  1. Access Token: Dùng để kẹp vào Header gọi API. Sinh mệnh cực ngắn (10 - 15 phút). Rơi vào tay hacker cũng không đáng lo.
  2. Refresh Token: Dùng để "đổi" lấy Access Token mới. Sinh mệnh dài (7 ngày, 30 ngày). Cái này thường được Frontend cất cực kỳ kỹ (ví dụ: HTTP-only Cookie).

Quy trình chuẩn: Cứ mỗi khi Access Token hết hạn (API báo lỗi 401)

Frontend lôi Refresh Token ra, gọi một API tên là /refresh-token để xin lại một Access Token mới cứng, rồi tự động gọi lại cái API vừa bị tạch.

2. Trạm gác Axios Interceptor là gì?

Nếu bạn có 50 cái API (Lấy profile, Lấy bài viết, Đăng comment...), chẳng lẽ ở mỗi file bạn đều phải viết khối lệnh try...catch, nếu lỗi 401 thì gọi hàm refresh? Code như thế là tự sát.

Trong Axios, có một tính năng thần thánh gọi là Interceptors (Trạm gác/Người đánh chặn). Nó đứng ở 2 đầu:

  • Request Interceptor: Đứng ở cửa ra. Mọi API trước khi rời khỏi Frontend đều phải đi qua nó (để nhét Access Token vào Header).
  • Response Interceptor: Đứng ở cửa vào. Mọi kết quả từ Server trả về đều qua đây trước khi vào code logic của bạn. Đây chính là nơi ta "bắt" lỗi 401!

3. Kịch Bản Ác Mộng: Hàng Chục Request Bị Tạch Cùng Lúc

Viết một cái Interceptor bắt lỗi 401 rồi gọi Refresh Token thì rất dễ. Nhưng một Vibe Coder phải nhìn thấy trước "tử huyệt" này: Trang chủ của bạn gọi 3 API cùng một lúc (Lấy User Info, Lấy Notification, Lấy News Feed). Trớ trêu thay, đúng lúc đó Access Token vừa hết hạn.

  • Cả 3 API đập lên Server và cùng bị Server tát cho 3 cái lỗi 401 Unauthorized.
  • Interceptor hứng trọn 3 cái lỗi này gần như cùng một mili-giây.
  • Nếu code non tay, Interceptor sẽ hoảng loạn và bắn đi 3 cái request /refresh-token cùng lúc. Server của bạn sẽ bị dội bom, Refresh Token có thể bị thu hồi vì bị xài nhiều lần bất thường, và hệ thống sập toàn tập!

4. Code Thực Chiến: Axios Interceptor Chuẩn Vibe Coder

Để xử lý hiện tượng "Dẫm đạp" ở trên, chúng ta cần 2 biến toàn cục:

  • isRefreshing (Cờ hiệu): Để biết là có thằng nào đang đi xin Token chưa. Nếu có rồi, cấm mấy thằng sau đi xin nữa.
  • refreshSubscribers (Hàng đợi chờ): Những thằng đến sau phải chui vào mảng này ngồi đợi. Thằng đầu tiên xin được Token mới thì về phát cho cả lũ để cùng chạy lại.

Dưới đây là đoạn code "vàng ngọc" có thể copy thẳng vào dự án của bạn:

import axios from 'axios';

const apiClient = axios.create({
    baseURL: 'https://api.vibecoder.com/v1',
    timeout: 10000,
});

// Biến cờ hiệu và hàng đợi
let isRefreshing = false;
let refreshSubscribers = [];

// Hàm gom những request đến sau vào hàng đợi
const subscribeTokenRefresh = (cb) => {
    refreshSubscribers.push(cb);
};

// Hàm chạy lại toàn bộ request trong hàng đợi sau khi có token mới
const onRerefreshed = (token) => {
    refreshSubscribers.map((cb) => cb(token));
    refreshSubscribers = []; // Chạy xong thì xóa hàng đợi
};

// --- RESPONSE INTERCEPTOR ---
apiClient.interceptors.response.use(
    (response) => {
        return response; // Data ngon, trả về cho app xài
    },
    async (error) => {
        const { config, response: { status } } = error;
        const originalRequest = config;

        // Nếu lỗi 401 và request này chưa từng được retry
        if (status === 401 && !originalRequest._retry) {
            
            if (isRefreshing) {
                // TÌNH HUỐNG: Đang có một request khác đi xin token rồi.
                // Giải pháp: Bắt request này hóa đá (trả về một Promise chưa resolve), 
                // nhét nó vào hàng đợi. Khi nào có token mới thì nhét vào Header và chạy lại.
                return new Promise((resolve) => {
                    subscribeTokenRefresh((newToken) => {
                        originalRequest.headers['Authorization'] = 'Bearer ' + newToken;
                        resolve(apiClient(originalRequest));
                    });
                });
            }

            originalRequest._retry = true; // Đánh dấu là tao đang đi retry đây
            isRefreshing = true; // Phất cờ khóa mõm các request đến sau

            try {
                // Gọi API xin token mới
                const rs = await axios.post('https://api.vibecoder.com/v1/refresh-token', {
                    refreshToken: getRefreshTokenFromStorage(), // Lấy từ localStorage hoặc Cookie
                });

                const newToken = rs.data.accessToken;
                
                // Lưu token mới vào bộ nhớ
                saveAccessTokenToStorage(newToken);

                // Thông báo cho các request đang xếp hàng: "Có hàng mới rồi anh em ơi, chạy đi!"
                isRefreshing = false;
                onRerefreshed(newToken);

                // Gắn token mới vào request đầu tiên này và chạy lại
                originalRequest.headers['Authorization'] = 'Bearer ' + newToken;
                return apiClient(originalRequest);

            } catch (_error) {
                // TÌNH HUỐNG: Refresh Token cũng hết hạn nốt! (Ví dụ: Qua 30 ngày)
                // Cạn lời. Xóa hết data, đá user về trang Login.
                isRefreshing = false;
                refreshSubscribers = [];
                clearStorage();
                window.location.href = '/login';
                return Promise.reject(_error);
            }
        }

        return Promise.reject(error);
    }
);

export default apiClient;

Lời kết

Vậy là bạn đã có trong tay bộ kỹ năng Auth Full-stack. Từ việc hiểu tường tận Authentication vs Authorization, sức mạnh và tử huyệt của JWT ở phía Backend, cho đến việc thiết lập trạm gác Interceptor ở phía Frontend để xử lý mượt mà như một kỹ sư lành nghề.

Trải nghiệm của User (UX) và Trải nghiệm của Dev (DX) đôi khi chính là sự khác biệt giữa một sản phẩm sống thọ và một sản phẩm bị vứt xó.

Chủ đề tiếp theo: Kỷ Nguyên Thời Gian Thực (Real-time) - WebSockets vs Server-Sent Events (SSE)

Chúng ta đã đàm đạo rất nhiều về giao tiếp HTTP RESTful. Giao thức này có một nguyên tắc chết người: Client luôn phải là người chủ động hỏi (Request), Server mới trả lời (Response). Vậy nếu làm app Chat, hay thông báo (Notification) lúc có đơn hàng mới thì sao? Chẳng lẽ cứ 3 giây Frontend lại bắn 1 request lên server hỏi "Có tin nhắn mới không anh?" (Short Polling)? Làm thế thì Server cháy mất!

Ở bài viết tới, chúng ta sẽ đập bỏ giới hạn của HTTP tĩnh và bước vào thế giới Real-time. Khi nào nên dùng WebSockets (Đường cao tốc 2 chiều), và khi nào thì Server-Sent Events (Loa phát thanh 1 chiều) mới là chân ái? Anh em nhớ đón đọc nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí