Access Token & Refresh Token: Bộ đôi bảo mật hoàn hảo trong Authentication
Khi thiết kế hệ thống xác thực (Authentication) bằng JWT (JSON Web Token), các lập trình viên thường phải đối mặt với một tình huống tiến thoái lưỡng nan liên quan đến Thời gian sống (Expiration Time) của Token.
Bản chất của JWT là Stateless (Phi trạng thái). Nghĩa là Server chỉ cần dùng "chữ ký bí mật" (Secret Key) để kiểm tra tính hợp lệ của Token mà không cần chọc vào Database. Sự tiện lợi này đi kèm với một cái giá rất đắt: Bạn không thể dễ dàng thu hồi một JWT trước khi nó hết hạn.
Điều này dẫn đến hai trường hợp dở khóc dở cười:
Trường hợp 1 (An toàn nhưng phiền phức): Bạn set Token sống trong 15 phút. Hệ thống cực kỳ an toàn (hacker có trộm được qua lỗi
XSScũng chỉ dùng được 15 phút), nhưng người dùng sẽ chửi thề vì cứ đang điền dở cái form lại bị văng ra trang Login.Trường hợp 2 (Tiện lợi nhưng nguy hiểm): Bạn set Token sống 30 ngày. Người dùng lướt web vô cùng mượt mà. Nhưng nếu Token này bị lộ, hacker có trọn vẹn 30 ngày để đổi mật khẩu, trộm dữ liệu mà hệ thống không thể cản lại (vì Token vẫn còn hạn).
Để giải quyết bài toán "Bảo mật" và "UX" cùng lúc, kiến trúc chia tách Access Token và Refresh Token đã ra đời và trở thành tiêu chuẩn của ngành công nghiệp phần mềm.
1. Phân biệt Access Token và Refresh Token
Thay vì cấp một "chìa khóa vạn năng", Server sẽ cấp cho Frontend 2 loại "giấy tờ" với đặc tính và nhiệm vụ hoàn toàn trái ngược nhau:
1.1. Access Token (Thẻ thông hành dùng hàng ngày)
Chức năng: Là "tấm vé" được đính kèm vào
Header(thường làAuthorization: Bearer <token>) của mọi API calls để chứng minh danh tính của bạn với Server. Nó thường chứa các thông tin cơ bản (Payload) nhưuserId,role,emailđể Server xử lý logic nhanh chóng.Thời gian sống (Lifespan): Rất ngắn. Thường chỉ từ
15 phútđến1 giờ.Mức độ rủi ro: Khá cao. Vì nó liên tục "bay" qua lại giữa Client và Server trên mỗi request, nguy cơ bị đánh chặn là có. Tuy nhiên, nhờ thời gian sống cực ngắn, dù hacker có lấy được thì "tấm vé" này cũng sẽ nhanh chóng biến thành giấy lộn.
1.2. Refresh Token (Giấy phép gốc cất trong két an toàn)
Chức năng: Tuyệt đối không được dùng để gọi các API nghiệp vụ (như lấy Profile, xem Giỏ hàng). Nhiệm vụ duy nhất của nó là mang lên Server để "đổi" lấy một
Access Tokenmới khi cái cũ đã hết hạn. Nó thường là một chuỗi ngẫu nhiên (Opaque Token) được lưu trong Database của Server.Thời gian sống: Rất dài. Thường từ
7 ngày,30 ngày, hoặc lâu hơn.Mức độ rủi ro: Rất thấp vì nó nằm im một chỗ, thỉnh thoảng mới được lôi ra gửi đi 1 lần. Đặc biệt, vì Refresh Token được lưu ở Database của Server, Backend có quyền tước đoạt (Revoke) nó bất cứ lúc nào (ví dụ: khi người dùng ấn nút Đăng xuất, hoặc khi Admin phát hiện tài khoản có dấu hiệu bị hack).
2. Luồng hoạt động: Thuật toán Silent Refresh (Làm mới âm thầm)
Trải nghiệm đỉnh cao của kiến trúc này là người dùng hoàn toàn không biết việc Token của họ liên tục hết hạn và được cấp mới. Mọi thứ được xử lý ngầm qua sự hỗ trợ của Axios Interceptor (mà chúng ta đã tìm hiểu ở bài trước):
Bước 1 (Đăng nhập): Người dùng nhập
username/password. Server xác thực thành công và trả về cảAccess TokenvàRefresh Token.Bước 2 (Hoạt động): Frontend đính kèm
Access Tokenvào API để lấy dữ liệu. Mọi thứ mượt mà.Bước 3 (Biến cố hết hạn): Phút thứ 16,
Access Tokenhết hạn. Frontend gọi API và Server từ chối phục vụ, trả về mã lỗi401 Unauthorized.Bước 4 (Trạm đánh chặn ra tay):
Response Interceptorcủa Axios lập tức "tóm" lấy lỗi 401. Nó tạm dừng (pause) luồng API hiện tại lại, không cho lỗi này văng ra giao diện người dùng.Bước 5 (Đổi Token): Trạm đánh chặn âm thầm lấy
Refresh Tokengửi lên API/api/auth/refresh-tokencủa Server.Bước 6 (Kiểm duyệt & Cấp mới): Server đối chiếu
Refresh Tokentrong Database. Nếu hợp lệ (chưa bị thu hồi, chưa hết hạn), Server sinh ra mộtAccess Tokenmới toanh và gửi về.Bước 7 (Tiếp tục hành trình): Interceptor lấy
Access Tokenmới, thay thế vào cái API đang bị tạm dừng ở Bước 4, và tự động gọi lại API đó. Giao diện vẫn hiển thị dữ liệu thành công như chưa hề có cuộc chia ly!
Vấn đề nâng cao (Race Condition): Điều gì xảy ra nếu tại giây phút thứ 16, Frontend của bạn gọi cùng lúc 5 API (như load ảnh, load text, load comment)? Cả 5 API sẽ cùng tạch 401. Nếu code không khéo, Axios sẽ gọi API đổi Token 5 lần liên tiếp! Lập trình viên giỏi sẽ phải cấu hình một "hàng đợi" (Queue) trong Interceptor: API đầu tiên rớt 401 sẽ đi đổi Token, 4 API còn lại bị
Hold(giữ lại), chờ Token mới về thì cả 5 cùng chạy tiếp.
3. Lưu trữ Token ở đâu để không thành "mồi ngon" cho Hacker?
Kiến trúc hay đến mấy mà lưu trữ sai chỗ thì cũng vô nghĩa. Nơi lưu trữ (Storage) là yếu tố sống còn quyết định sự thành bại của hệ thống. Dựa vào bài viết về HttpOnly Cookie trước đó, ta có quy tắc vàng sau:
Đối với Refresh Token (Bắt buộc): PHẢI được lưu trong
HttpOnly Cookie. Đây là chiếc chìa khóa tối thượng có thể sinh ra vô hạn Token khác. Nếu bạn lưu ởlocalStorage, chỉ cần một đoạn mã độcJavaScript (XSS)chạy vào trang web của bạn, hacker sẽ trộm được nó dễ dàng.HttpOnlysẽ khiến JavaScript bị "mù", bảo vệ két sắt của bạn tuyệt đối an toàn.Đối với Access Token (Có 2 lựa chọn):
Cách 1 (Lưu ở biến State / Memory của React): Rất an toàn trước XSS. Điểm trừ là mỗi khi người dùng F5 tải lại trang, biến bị xóa, ứng dụng sẽ phải âm thầm dùng Refresh Token chọc lên Server để xin lại Access Token một lần.
Cách 2 (Lưu tiếp vào một HttpOnly Cookie thứ hai): Cách này bảo mật tuyệt đối và được các "ông lớn" công nghệ ưa chuộng. Tuy nhiên, bạn sẽ phải mất thêm công sức cấu hình Backend để chống lại các cuộc tấn công giả mạo yêu cầu (
CSRF).
4. Tổng kết
Việc chia tách thành Access Token (ngắn hạn) và Refresh Token (dài hạn) giúp hệ thống Web của bạn đạt được sự hoàn hảo trên cả 3 phương diện:
Bảo mật tối đa: Access Token dù lộ cũng vô dụng sau vài phút. Refresh Token thì được trình duyệt khóa chặt trong
HttpOnly Cookie.Kiểm soát chủ động: Backend nắm đằng chuôi. Khi muốn khóa tài khoản hoặc ép đăng xuất trên mọi thiết bị, Server chỉ cần xóa Refresh Token trong Database là xong.
Trải nghiệm tuyệt vời: Người dùng có thể lướt web cả tháng trời mà không bao giờ bị văng ra trang Login một cách vô duyên.