Kỷ Nguyên Stateless: Quyền Năng Của JWT & Nỗi Đau "Đăng Xuất" Không Lối Thoát
Chúng ta đã bàn về Authentication (Xác thực). Ở kỷ nguyên Web 1.0, các cụ hay dùng Session lưu trong RAM của server. User đăng nhập -> Server phát cho một cái Session ID -> Lưu vào RAM. Cách này gọi là Stateful (Có trạng thái).
Nhưng khi dự án của bạn scale lên 10 con Server, user đăng nhập ở Server A, nhưng request tiếp theo Load Balancer lại ném qua Server B. Server B ngơ ngác: "Ủa anh là ai, RAM tôi đâu có Session của anh?". Bắt user đăng nhập lại!
Để giải quyết nỗi đau đó, giới công nghệ sinh ra một đấng cứu thế mang tên JWT (JSON Web Token) - Nền tảng của kiến trúc Stateless (Không trạng thái). Dưới đây là bài viết mổ xẻ trái tim của JWT và mặt tối đẫm máu của nó!
Một Vibe Coder phải nằm lòng triết lý của JWT: "Tôi không cần nhớ anh là ai, tôi chỉ cần nhìn cái thẻ (Token) anh cẩm là biết!".
Với JWT, Server không lưu bất kỳ thứ gì trong RAM hay Database để xác thực bạn cả. Khi bạn đăng nhập thành công, Server "nhào nặn" ra một chuỗi JWT, đưa cho Frontend và nói: "Cầm lấy cái này, từ nay gọi API cứ kẹp nó vào. Tao không lưu mày trong đầu nữa đâu!".
1. Giải phẫu chuỗi JWT (3 Phần Huyết Mạch)
Nhìn bề ngoài, JWT là một chuỗi ký tự lằng nhằn trông như bị lỗi font, được ngăn cách bởi 2 dấu chấm ( . ). Nó gồm 3 phần: Header . Payload . Signature.
- Phần 1 - Header (Đỏ): Khai báo loại token (JWT) và thuật toán mã hóa (VD: HS256).
- Phần 2 - Payload (Tím): Cái bụng chứa dữ liệu. Đây là nơi bạn nhét user_id, role, và exp (thời gian hết hạn).
- Cảnh báo đẫm máu: Cả Header và Payload chỉ được mã hóa Base64 chứ KHÔNG PHẢI MẬT MÃ (Encryption). Bất kỳ ai nhặt được token này đều có thể lên web dịch ngược ra đọc được data bên trong. TUYỆT ĐỐI KHÔNG lưu password hay secret vào đây!
- Phần 3 - Signature (Xanh): Trái tim của JWT. Chữ ký này được tạo ra bằng cách lấy (Header + Payload) băm với một cái Secret Key (Chìa khóa bí mật chỉ Server mới biết).
Nếu hacker nhặt được token, lên mạng sửa Payload role: "user" thành role: "admin" rồi gửi lên Server. Server sẽ lấy cái Payload vừa bị sửa đó, băm lại với Secret Key. Chữ ký mới sinh ra sẽ KHÁC HOÀN TOÀN với Chữ ký cũ trên token. Server lập tức chửi: "Mày lừa tao, cút!" (Lỗi 401).
2. Nỗi đau chí mạng: Đăng xuất (Logout) một Stateless Token kiểu gì?
JWT tuyệt vời vì Server không cần nhớ nó. Nhưng đó cũng là tử huyệt của nó.
Giả sử bạn phát cho user một JWT có hạn sử dụng (exp) là 30 ngày.
Đến ngày thứ 2, user vào quán Net, quên tắt máy. Hacker copy được cái JWT đó.
User chạy về nhà, hoảng hốt lên web bấm "Đăng xuất" (Logout).
Ở phía Frontend, hàm Logout đơn giản là xóa token khỏi LocalStorage. UI của user hiện ra chữ "Đăng xuất thành công". NHƯNG ở phía Hacker, hắn đã lưu file text cái token đó rồi. Hắn quăng token vào Postman và gọi API.
Và Server... vẫn cho qua! (HTTP 200 OK)
Tại sao? Vì JWT là Stateless. Khi Hacker gửi lên, Server chỉ lấy Secret Key ra kiểm tra Chữ ký. Chữ ký đúng, hạn sử dụng vẫn còn 28 ngày. Server gật đầu: "Token chuẩn, xin mời vào". Server KHÔNG HỀ BIẾT user đã bấm Logout ở đâu đó!
Bạn không thể vào Database để "xóa" JWT, vì ngay từ đầu bạn đã không lưu nó trong DB! Bạn bế tắc hoàn toàn.
3. Vibe Coder giải quyết cơn ác mộng này như thế nào?
Để phá vỡ sự bất tử của JWT khi bị lộ, chúng ta có 3 cảnh giới xử lý:
Cảnh giới 1: Sống gấp (Short-lived Token)
Đừng bao giờ set JWT sống 30 ngày. Một Vibe Coder chỉ cho phép Access Token sống 15 phút. Nếu hacker lấy được token, nó cũng chỉ xài được tối đa 15 phút là token tự thiu. Để user không bị văng ra bắt login lại mỗi 15 phút, ta dùng thêm cơ chế Refresh Token (Sẽ nói sâu ở bài sau).
Cảnh giới 2: Cuốn Sổ Đen (The Redis Blacklist)
Đây là cách các hệ thống lớn dùng để làm tính năng "Logout mọi thiết bị".
- Khi user bấm Logout, Frontend bắn cái JWT đó lên API
/logout. - Backend cầm lấy JWT đó, thay vì xóa, Backend nhét nó vào Redis (Cuốn sổ đen). Nhớ set TTL của Redis đúng bằng thời gian sống còn lại của JWT để đỡ rác RAM.
- Lần sau, bất kỳ ai cầm JWT gọi API, Middleware của bạn phải làm 2 việc: 1. Check chữ ký hợp lệ. 2. Chọc vào Redis xem token này có nằm trong Sổ đen không. Nếu có, cấm cửa! (Cách này biến hệ thống từ Stateless thành có một chút Stateful, nhưng vì Redis cực nhanh nên sự đánh đổi này hoàn toàn xứng đáng).
Cảnh giới 3: Token Versioning (Chặn đứt cội rễ)
Nếu user đổi Mật khẩu, bạn muốn tất cả JWT cũ trên mọi thiết bị lập tức bốc hơi?
- Hãy thêm 1 cột
token_version(int, default = 1) vào bảngUserstrong Database. - Khi gen JWT, nhét cái
token_version: 1vào Payload. - Khi user đổi pass, bạn update
token_versiontrong DB lên 2. - Middleware khi nhận JWT, lấy
user_idchọc vào DB lấytoken_versionhiện tại. Nếu version trong DB (2) > version trong JWT (1) -> Token lỗi thời, cấm cửa!
Lời kết
Công nghệ nào cũng có Trade-off (Sự đánh đổi). JWT giải phóng Server khỏi việc nhớ dung mạo hàng triệu User, giúp hệ thống Scale lên vô tận. Nhưng bù lại, nó đẩy trách nhiệm quản lý phiên đăng nhập sang những kịch bản phức tạp hơn. Hiểu thấu ranh giới này, bạn sẽ làm chủ hoàn toàn các hệ thống Microservices hiện đại.
Chủ đề tiếp theo: Kịch Bản 15 Phút - Múa Màn Refresh Token Mượt Như Lụa
Ở Cảnh giới 1 mình có nhắc đến việc ép Access Token chỉ sống được 15 phút. Vậy cứ 15 phút user đang xem phim lại bị văng ra bắt nhập lại username/password à? Trải nghiệm như vậy thì app sẽ bị đánh 1 sao ngay lập tức!
Để giải quyết, khi login ta cấp 2 cái token: AccessToken (sống 15 phút) và RefreshToken (sống 7 ngày). Làm sao để khi Access Token vừa ngắc ngoải chết, Frontend tự động cầm Refresh Token đi xin một Access Token mới mà User không hề hay biết (âm thầm chạy ngầm dưới background)?
Ở bài viết tới, mình sẽ đàm đạo về Luồng Refresh Token kết hợp Axios Interceptor trên Frontend. Một tuyệt kỹ kết nối hoàn hảo giữa Client và Server. Anh em đón đọc nhé!
All Rights Reserved