
Bẫy race condition trong triển khai Refresh Token: Bài học xương máu cho mọi lập trình viên
Khám phá nguyên nhân khiến hệ thống xác thực của bạn tự động đăng xuất người dùng một cách vô lý khi xử lý đồng thời nhiều request. Bài viết phân tích sâu về cơ chế Refresh Token rotation và cách xây dựng Interceptor an toàn cho ứng dụng web.
Bài viết được dịch và tổng hợp từ tin tức gốc. Bạn có thể đọc bài viết gốc bằng tiếng Anh tại đây.
Điểm tin nhanh:
- Việc gửi đồng thời nhiều request khi Access Token hết hạn sẽ gây ra lỗi race condition trong cơ chế Refresh Token rotation.
- Các thư viện như Axios cần một cơ chế hàng đợi (queue) và khóa (lock) để đồng bộ hóa việc làm mới token.
- Hiểu sai thông báo lỗi từ backend có thể dẫn đến việc triển khai logic logout không cần thiết, làm gián đoạn trải nghiệm người dùng.
Bạn đã bao giờ rơi vào tình huống ứng dụng của mình đột ngột đẩy người dùng ra màn hình đăng nhập ngay khi họ vừa thực hiện một vài thao tác đồng thời chưa? Đây không phải là lỗi do người dùng, mà là một "quả bom hẹn giờ" nằm ngay trong cách bạn xử lý xác thực. Khi Access Token hết hạn, việc hàng loạt request cùng lúc cố gắng làm mới token sẽ tạo ra một cuộc đua (race condition) khiến hệ thống bảo mật vô hiệu hóa phiên làm việc của người dùng. Hãy cùng mổ xẻ vấn đề này dưới góc nhìn của một kỹ sư hệ thống.
Khi các request trở thành thảm họa đồng thời
Trong một ứng dụng hiện đại, khi người dùng truy cập trang Dashboard, trình duyệt thường gửi hàng loạt request cùng lúc như lấy thông tin profile, danh sách thông báo, và dữ liệu hoạt động. Nếu tất cả các request này cùng nhận phản hồi 401 Unauthorized tại cùng một thời điểm, logic xử lý của bạn sẽ bị quá tải.

Bảng so sánh hành vi hệ thống khi không có đồng bộ hóa
| Trạng thái | Request #1 | Request #2 | Request #3 | Kết quả chung |
|---|---|---|---|---|
| T = 0ms | 401 (Expired) | 401 (Expired) | 401 (Expired) | Trigger Refresh |
| T = 5ms | Gửi Refresh | Gửi Refresh | Gửi Refresh | Xung đột token |
| T = 10ms | Thành công | Thất bại (Revoked) | Thất bại (Revoked) | Logout người dùng |
Việc gửi nhiều yêu cầu làm mới token cùng lúc tới các backend sử dụng cơ chế xoay vòng (rotation) như Django REST Framework với SimpleJWT là một sai lầm nghiêm trọng. Request thứ hai sẽ vô hiệu hóa token vừa được tạo bởi request thứ nhất, dẫn đến việc toàn bộ phiên làm việc bị hủy bỏ. Điều này tương tự như việc bạn cố gắng tối ưu hóa quy trình nhưng lại vô tình tạo ra rào cản, giống như những bài học về văn hóa kỹ thuật.
Giải pháp: Xây dựng cơ chế Interceptor an toàn
Để giải quyết triệt để, chúng ta cần một cơ chế khóa (mutex) và hàng đợi (queue) trong Axios Interceptor. Thay vì để mỗi request tự xử lý lỗi, chúng ta sẽ tạm dừng các request khác cho đến khi việc làm mới token hoàn tất.

Sơ đồ luồng xử lý đồng bộ
[Request 401] ---> [Kiểm tra isRefreshing]
|
+--- (YES) ---> [Đẩy vào failedQueue] ---> [Chờ đợi]
|
+--- (NO) ---> [Set isRefreshing = true] ---> [Gọi API Refresh] ---> [Flush Queue]
Triển khai mã nguồn
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (error) prom.reject(error);
else prom.resolve(token);
});
failedQueue = [];
};
Việc quản lý trạng thái này cũng quan trọng như cách chúng ta xây dựng công cụ xác thực trạng thái công việc để tránh những thông báo giả. Khi hệ thống không được đồng bộ, nó sẽ dẫn đến các lỗi khó kiểm soát, tương tự như khi 236 bài kiểm thử đều vượt qua nhưng hệ thống vẫn sụp đổ.
Lưu ý: Đừng bao giờ kiểm tra lỗi bằng cách so sánh chuỗi thông báo từ backend (ví dụ: token_not_valid). Hãy dựa vào mã trạng thái HTTP 401 và logic kiểm tra token thực tế để tránh việc logout người dùng sai thời điểm.
Đánh giá & Lời khuyên Thực tiễn
Giải pháp sử dụng hàng đợi (queue) là tiêu chuẩn vàng cho các ứng dụng Frontend hiện nay.
- Ưu điểm: Đảm bảo tính toàn vẹn của phiên làm việc, giảm tải cho server, tránh các request không cần thiết.
- Nhược điểm: Tăng độ phức tạp cho code base, đòi hỏi quản lý bộ nhớ tốt để tránh memory leak trong hàng đợi.
- Lưu ý Production: Hãy đảm bảo rằng nếu quá trình refresh token thất bại (ví dụ: refresh token cũng hết hạn), bạn phải xóa sạch localStorage và điều hướng người dùng về trang đăng nhập một cách an toàn. Đừng để người dùng mắc kẹt trong vòng lặp vô tận.
Việc tối ưu hóa này cũng cần được cân nhắc kỹ lưỡng, giống như khi bạn tối ưu hóa hiệu năng trước khi ra mắt để đảm bảo hệ thống chịu được tải thực tế.
Câu hỏi thường gặp (FAQ)
Tại sao tôi không nên dùng biến boolean đơn giản để chặn request?
Chỉ dùng boolean sẽ khiến các request đến sau bị hủy bỏ thay vì bị tạm dừng, dẫn đến việc UI của bạn bị mất dữ liệu hoặc hiển thị lỗi không cần thiết cho người dùng.
Có cách nào khác ngoài việc dùng Axios Interceptor không?
Bạn có thể sử dụng các thư viện như React Query để quản lý cache và trạng thái xác thực, tuy nhiên Interceptor vẫn là lớp phòng thủ đầu tiên cần thiết ở cấp độ HTTP client.
Làm sao để biết khi nào refresh token đã thực sự hết hạn?
Backend nên trả về một mã lỗi riêng biệt (ví dụ: 403 hoặc 401 với code cụ thể) để phân biệt giữa việc access token hết hạn và refresh token hết hạn.
Kết luận
Việc hiểu rõ cơ chế vận hành của Refresh Token không chỉ giúp ứng dụng của bạn bảo mật hơn mà còn cải thiện đáng kể trải nghiệm người dùng. Đừng để những lỗi race condition nhỏ nhặt làm gián đoạn hành trình của khách hàng. Hãy áp dụng mô hình hàng đợi ngay hôm nay và đừng quên theo dõi hi_dev để cập nhật thêm nhiều kiến thức kỹ thuật chuyên sâu khác.
Nếu bạn đang gặp khó khăn trong việc thiết kế hệ thống, hãy tham khảo thêm các bài viết về kiến trúc hệ thống để có cái nhìn tổng quan hơn.
Do you like this post?
Upvote to push this post higher on the community feed





