Token vs Session - Cuộc Chiến Xác Thực Đẫm Máu & Cú Lừa "Stateless
Nếu bạn lướt qua các hội nhóm lập trình hiện nay, bạn sẽ thấy một giáo phái cuồng nhiệt tôn sùng JWT (JSON Web Token). Họ ra rả rằng: "Session đã lỗi thời rồi!", "Dùng JWT đi cho nó Stateless (phi trạng thái)!", "JWT tốn ít RAM, scale hệ thống dễ dàng!".
Nhưng sự thật phũ phàng là: 90% các bạn Fresher/Junior đang triển khai JWT sai cách, tự rước lỗ hổng bảo mật vào hệ thống, và cuối cùng lại phải đi "phát minh lại chiếc bánh xe" Session một cách cực kỳ cồng kềnh.
Hôm nay, một Vibe Coder sẽ lột trần sự thật về cuộc chiến này. Pha cà phê đi, bài này sẽ đập vỡ khá nhiều kiến thức cũ của bạn đấy!
PHẦN 1: KẺ CẦM QUYỀN CŨ - SESSION & COOKIE (STATEFUL)
Mô hình này đã tồn tại từ thuở sơ khai của Web.
- Cách hoạt động: Khi bạn đăng nhập thành công, Server tạo ra một cái vé (Session ID) là một chuỗi ngẫu nhiên (VD:
acb-123. Server ghi vào sổ cái (Thường là Redis):"Vé abc-123 thuộc về user Hiếu". Sau đó, Server nhét cái vé này vào túi quần của bạn (Cookie). - Mỗi lần bạn gọi API: Trình duyệt tự động móc túi lấy Cookie abc-123 đưa cho Server. Server cầm vé, chạy đi lật cuốn sổ Redis ra dò. Nếu thấy vé hợp lệ, nó cho bạn đi qua.
Ưu điểm tuyệt đối:
Quyền sinh sát nằm 100% trong tay Server. Nếu phát hiện bạn bị hack tài khoản, Admin chỉ cần vào Redis XÓA BỎ cái vé abc-123. Lần tới bạn gọi API, Server tìm sổ không thấy vé -> Bạn lập tức bị đá văng ra ngoài.
Nhược điểm:
- Tốn RAM: 1 triệu user online là 1 triệu dòng lưu trong Redis.
- Khó Scale (nếu không biết cách): Nếu bạn có 3 con Server App mà chỉ lưu Session ở bộ nhớ RAM nội bộ của 1 con, user load balancer nhảy sang con khác sẽ bị bắt đăng nhập lại.
(Dĩ nhiên, Vibe Coder giải quyết cái một bằng cách dùng Redis làm Centralized Cache như các bài trước đã học).
PHẦN 2: KẺ THÁCH THỨC - JWT VÀ ẢO ẢNH "STATELESS"
Để giải quyết bài toán tốn RAM, JWT ra đời với triết lý Stateless (Phi trạng thái): Bắt Client tự nhớ thông tin của nó, Server không thèm nhớ gì cả!
- Cách hoạt động: Bạn đăng nhập thành công. Server không tạo Session, cũng không ghi vào Redis. Thay vào đó, nó gói thông tin của bạn (ID, Name, Role) thành một chuỗi JSON, lấy Chìa khóa bí mật (Secret Key) của Server đóng dấu thập tự (Signature) lên đó. Chuỗi đó gọi là JWT. Server đưa JWT cho bạn giữ.
- Mỗi lần gọi API: Bạn gửi kèm cái JWT đó lên. Server lấy cái Secret Key ra để kiểm tra con dấu. Nếu dấu chuẩn chỉ, không bị làm giả, Server lập tức tin bạn là "Hiếu" và cho qua, MÀ KHÔNG CẦN CHỌC XUỐNG DATABASE HAY REDIS NỮA!
Nghe quá đỉnh đúng không? Không tốn 1 byte RAM nào trên Server, tốc độ xác thực bằng vận tốc ánh sáng (toán học thuần túy).
Nhưng...
PHẦN 3: CÚ LỪA VĨ ĐẠI - KHI "THẦN DƯỢC" TRỞ THÀNH "THUỐC ĐỘC"
Bạn dùng JWT, code chạy ngon, sếp khen. Đến một ngày, một User bị lộ mật khẩu, Hacker đang cầm điện thoại của User đó và phá hoại hệ thống. User đó gọi điện lên tổng đài kêu cứu: "Khóa tài khoản của tôi lại ngay!"
Bạn (Admin) lật đật vào Database, đổi trạng thái User đó thành Banned (Bị cấm). Đổi cả mật khẩu. Bạn thở phào. Nhưng 5 phút sau, hệ thống vẫn báo Hacker đang rút tiền! Tại sao???
Tử huyệt của JWT: Vì bản chất của JWT là Stateless. Khi Hacker gọi API rút tiền bằng cái JWT cũ (vẫn còn thời hạn), con Server của bạn chỉ làm toán kiểm tra con dấu Signature. Con dấu vẫn đúng! Server KHÔNG HỀ truy vấn xuống Database để biết user đó đã bị Ban. Thế là nó vẫn cho phép Hacker rút tiền!
Giải pháp của các "Thợ gõ": Họ cuống cuồng tạo ra một cái bảng trong Redis gọi là JWT_Blacklist (Danh sách đen). Khi Ban ai đó, họ nhét cái JWT của người đó vào Redis. Từ đó về sau, mỗi lần JWT gọi lên, Server lại... chạy xuống Redis để dò xem JWT có nằm trong Blacklist không.
Chờ chút! Có gì đó sai sai ở đây? Nếu mỗi lần gọi API, bạn vẫn phải chọc xuống Redis để kiểm tra... Vậy thì cái sự "Stateless" tự hào của JWT đã hoàn toàn tan vỡ. Chúc mừng, bạn vừa đi một vòng trái đất để "Phát minh lại Session", nhưng với một kiến trúc cồng kềnh, kém an toàn và tốn băng thông hơn gấp 10 lần!
LỜI KẾT & KIẾN TRÚC CỦA VIBE CODER
Cuộc chiến này không có kẻ chiến thắng tuyệt đối. Một Vibe Coder sẽ không chọn phe, mà chọn đúng công cụ cho đúng bài toán.
Quy tắc thiết kế hệ thống chuẩn mực:
- Hầu hết các Web App thông thường (Dashboard, E-commerce, Admin Panel): Hãy quay về với Session + Redis + HttpOnly Cookie. Nó bảo mật tuyệt đối trước lỗi XSS, dễ dàng Logout mọi thiết bị, và kiểm soát quyền lực 100%. Đừng làm phức tạp hóa vấn đề.
- Giao tiếp Server-to-Server (Microservices): Tuyệt vời! Lúc này JWT là vua. Server A ký một JWT sống đúng 1 phút rồi đưa cho Server B. Không cần quan tâm chuyện thu hồi vì 1 phút sau nó tự chết.
- Hệ thống Mobile App / SPA bắt buộc dùng Token: Tuyệt đối không cấp một JWT sống tới tận 30 ngày. Hãy dùng mô hình Access Token (JWT) + Refresh Token (Opaque String):
- Access Token: Là JWT, sống cực ngắn (5 - 15 phút). Dùng để xác thực API cực nhanh không cần DB.
- Refresh Token: Là một chuỗi ngẫu nhiên (giống Session ID), lưu ở Database, sống 30 ngày. Khi Access Token chết, Client đem Refresh Token gọi lên Server. Server lôi DB ra dò (lúc này mới check xem User có bị Ban hay không). Nếu ổn, cấp cho cặp Token mới. Nếu muốn Ban user? Xóa Refresh Token trong DB là xong! Hacker cùng lắm chỉ phá được trong 15 phút cuối cùng.
Chủ đề tiếp theo: Đăng Nhập Không Cần Mật Khẩu - Giải Phẫu Luồng Chạy OAuth2 (Google/Facebook Login)
Chúng ta đã biết cách giữ cửa bằng Session và Token. Nhưng người dùng thời nay rất lười. Họ ghét phải nhớ mật khẩu. Họ chỉ muốn bấm một nút "Login with Google" là xong.
Làm sao để ứng dụng của bạn lấy được Avatar, Email của người dùng từ Google mà Google lại KHÔNG BAO GIỜ giao mật khẩu của người dùng cho bạn? Sự tin tưởng ủy quyền đó được xây dựng trên một giao thức vĩ đại mang tên OAuth 2.0.
Ở bài viết tới, chúng ta sẽ bóc tách từng bước nhảy (Dance) của giao thức OAuth2, từ bước xin cấp mã Code (Authorization Code) đến khi đổi lấy Token. Đừng bỏ lỡ bài học quan trọng nhất về ủy quyền bảo mật này nhé!
All Rights Reserved