Cookies, Session, Token — 3 cách web 'nhớ' bạn đã đăng nhập
HTTP vốn không có trí nhớ — mỗi request là một lần gặp lạ. Cookies, Session và Token là 3 cách web 'nhớ' bạn giữa các request: cái nào bảo mật hơn, cái nào nhanh hơn, và vì sao web hiện đại thường dùng cả 3.
Mục lục bài viết(21)

Sơ đồ trao đổi HTTP cookie — bước 1: browser gửi request chưa có cookie; bước 2: server response kèm Set-Cookie header; bước 3+: browser tự động gửi cookie đó kèm mỗi request tiếp theo cho cùng domain. Đây là cơ chế gốc làm nền cho mọi hệ thống login web. Nguồn: Wikimedia Commons (public domain).
Hiểu đơn giản nhất
Web hoạt động trên HTTP — giao thức không có trí nhớ. Mỗi lần bạn click một link là một lần gặp lạ với server: server không nhớ bạn là ai, vừa làm gì.
Vậy tại sao bạn login Shopee buổi sáng, đến tối mở lại vẫn thấy "Chào bạn, Nguyễn Văn A"?
Vì có 3 kỹ thuật giúp web "nhớ" bạn giữa các request:
| Kỹ thuật | Analogy đời thường | Nơi lưu thông tin |
|---|---|---|
| Cookie | Nhãn dán trên áo khi vào nhà hàng buffet | Trên máy bạn (browser) |
| Session | Số phòng khách sạn (server tra số → ra tên khách) | Trên server (DB hoặc cache) |
| Token | Vé concert có hologram, bảo vệ chỉ check chữ ký | Trong cookie hoặc localStorage |
Cả 3 kỹ thuật không loại trừ nhau — web hiện đại thường dùng kết hợp. Ví dụ: Session ID lưu trong Cookie có flag bảo mật, còn API riêng dùng JWT Token.
Tại sao bạn cần biết
- Đọc được lỗi auth dev báo cáo. "Cookie expired" = vé hết hạn, "Invalid session" = số phòng không tồn tại, "JWT signature mismatch" = vé bị giả mạo. Hiểu khác biệt = không bị dev lừa "lỗi này khó lắm".
- Chọn đúng kiểu auth khi đặt SaaS. Web admin nội bộ → Session. Public API cho mobile/extension → Token. Chọn sai = code phức tạp gấp đôi.
- Audit security tự tin. Cookie thiếu HttpOnly là lỗi cấp High trong OWASP. Đọc Network tab DevTools → thấy ngay web nào ẩu, web nào chuẩn.
- Hiểu vì sao app bạn dùng "đăng xuất khắp các máy" hay "ghi nhớ đăng nhập 30 ngày". Đó là server invalidate session toàn cục, hoặc set expiry refresh token. Concept gốc nằm trong bài này.
Cookie — chiếc nhãn dán trên áo
Cookie là một chuỗi text ngắn server gửi cho browser, browser tự động lưu và gửi kèm mọi request tiếp theo tới cùng domain.
Trao đổi cookie diễn ra thế nào
Bước 1: Bạn vào shopee.vn lần đầu. Browser gửi request:
GET /home HTTP/1.1
Host: shopee.vn
Bước 2: Server response kèm Set-Cookie:
HTTP/1.1 200 OK
Set-Cookie: cart_id=abc123; Path=/; Max-Age=2592000; HttpOnly; Secure
Set-Cookie: visitor_id=xyz789; Path=/; Max-Age=86400
Bước 3: Browser tự động lưu 2 cookie đó. Lần sau (kể cả tuần sau), khi bạn truy cập shopee.vn, browser tự kèm:
GET /products/123 HTTP/1.1
Host: shopee.vn
Cookie: cart_id=abc123; visitor_id=xyz789
Server đọc cart_id=abc123 → biết đây là giỏ hàng của bạn → load đúng giỏ.
4 flag bảo mật của cookie
| Flag | Nghĩa | Vì sao quan trọng |
|---|---|---|
| HttpOnly | JavaScript trong trang không đọc được cookie | Chống XSS đánh cắp session |
| Secure | Chỉ gửi qua HTTPS | Chống nghe lén WiFi công cộng |
| SameSite=Lax/Strict | Không gửi cookie khi click link từ domain khác | Chống CSRF (giả mạo request) |
| Max-Age / Expires | Thời gian sống của cookie | Hết hạn → browser tự xoá |
Cookie cho login PHẢI có cả 4 flag. Cookie thiếu HttpOnly là lỗ XSS phổ biến nhất — hacker chèn được 1 dòng JS là đánh cắp session toàn bộ user.
Cookie size limit
- Mỗi cookie tối đa 4 KB
- Mỗi domain tối đa ~50 cookie
- Browser gửi cookie kèm mọi request tới domain đó → cookie càng to càng làm web chậm
Vì vậy cookie chỉ lưu vé (chuỗi ngẫu nhiên ngắn), không lưu data thực. Data thực lưu ở server (session) hoặc trong token (JWT).
Session — số phòng khách sạn
Session là dữ liệu user lưu ở server, mỗi user có một Session ID ngẫu nhiên đại diện.
Flow session-based auth
- User nhập email + password → backend verify → tạo session:
{user_id: 123, role: "ADMIN", login_at: ...}lưu trong DB hoặc Redis với keysess_abc123xyz - Backend trả về browser cookie:
Set-Cookie: session_id=sess_abc123xyz; HttpOnly; Secure - Mỗi request sau, browser kèm cookie → backend đọc
session_id→ tra DB lấy data session → biết user là ai - Logout → backend xoá session khỏi DB → cookie kia thành "số phòng không tồn tại"
Ưu / nhược
Ưu:
- Revoke nhanh — xoá 1 row trong DB là user bị đá khỏi hệ thống ngay (kể cả khi user chưa close browser)
- Data ở server → cookie nhỏ (chỉ chứa session ID), không lộ thông tin user
- Có thể update permission realtime — admin đổi role → session reload từ DB → permission mới effective ngay
Nhược:
- Tốn DB/Redis lookup mỗi request → 5-20ms thêm latency mỗi API call
- Khó scale ngang — nếu có 5 server, session phải share qua Redis chung, không thể lưu in-memory per server
- Stateful — server "có trí nhớ", reset server là mất session (trừ khi lưu Redis riêng)
Pattern session ở vietcodex.com
lib/auth.ts cấu hình better-auth với cookieCache 60 giây — middleware đọc cookie verify chữ ký, không cần lookup DB mỗi request, chỉ refresh sau 60 giây để catch role change. Cân bằng giữa performance và revoke responsiveness.
Token (JWT) — vé concert có hologram
JWT (JSON Web Token) là một chuỗi tự chứa thông tin — server không cần lookup DB, chỉ verify chữ ký là biết user.
Cấu trúc JWT
JWT gồm 3 phần, ngăn cách bởi dấu chấm:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJBRE1JTiIsImV4cCI6MTcxNjAwMDAwMH0.5K8N9p_x...
└──────── Header ────────┘ └────────── Payload ──────────┘ └── Signature ──┘
| Phần | Nội dung | Encoded thế nào |
|---|---|---|
| Header | {"alg": "HS256", "typ": "JWT"} — thuật toán ký | base64 |
| Payload | {"user_id": 123, "role": "ADMIN", "exp": 1716000000} — claims | base64 |
| Signature | HMAC-SHA256(header + payload, SECRET) | base64 |
⚠️ Payload chỉ encode base64, KHÔNG mã hoá — ai cũng decode được. Đừng nhồi password hay thông tin riêng tư vào payload. Chỉ để user_id, role, expiry.
Flow JWT auth
- User login → backend verify → tạo JWT: ký bằng
SECRET(chỉ server biết) → trả về frontend - Frontend lưu JWT vào cookie (HttpOnly) hoặc localStorage
- Mỗi request kèm JWT trong header:
Authorization: Bearer eyJhbGci... - Backend nhận JWT → tách signature → verify bằng SECRET → nếu khớp → tin payload → biết user là ai, KHÔNG cần lookup DB
- JWT hết hạn (theo claim
exp) → reject, yêu cầu login lại hoặc refresh
Ưu / nhược
Ưu:
- Stateless — server không lưu gì, scale ngang dễ
- Không lookup DB mỗi request → API public nhanh hơn
- Cross-domain — frontend ở
app.vietcodex.com, API ởapi.vietcodex.comđều dùng được token
Nhược:
- Khó revoke trước hạn — token đã phát → đến hết
expmới chết. Muốn đá user ngay phải duy trì blacklist (mất stateless). - Payload lộ — đừng để thông tin nhạy cảm
- SECRET lộ = toàn hệ thống lộ — ai có SECRET có thể ký token giả mạo bất kỳ user nào. Phải rotate SECRET định kỳ.
Khi nào dùng JWT vs Session
| Scenario | Khuyến nghị |
|---|---|
| Web app admin/dashboard | Session (revoke nhanh quan trọng) |
| Public API cho mobile app | JWT (stateless + fast) |
| Mobile app native | JWT + refresh token |
| Browser extension | JWT (extension không share cookie với web) |
| Microservices nội bộ gọi nhau | JWT (service-to-service auth) |
| SaaS như vietcodex.com | Cả 2 — cookie session cho dashboard, JWT cho public API/webhook |
Ví dụ thực tế: Bạn login Facebook trên web vs mobile
Web (facebook.com)
- Bạn nhập email + password trên
facebook.com/login - Server verify → tạo session ID trong Redis cluster
- Server set cookie:
Set-Cookie: c_user=12345; HttpOnly; Secure; SameSite=Lax; Domain=.facebook.com; Max-Age=86400 - Browser tự gửi cookie mỗi lần bạn vào facebook.com hay messenger.com (subdomain) trong 24h tới
- Bấm "Đăng xuất" → server invalidate session ID → cookie kia vô dụng
Mobile app (Facebook iOS/Android)
- Bạn login → server tạo 2 token: access (1 giờ) + refresh (60 ngày)
- App lưu cả 2 trong secure storage (Keychain iOS / EncryptedSharedPreferences Android)
- Mỗi API call kèm
Authorization: Bearer <access_token> - Access token hết hạn → app dùng refresh token gọi
/auth/refresh→ nhận access token mới - 60 ngày không mở app → refresh token hết hạn → app yêu cầu login lại
- Bạn report mất điện thoại → admin Facebook revoke refresh token đó từ server
Lưu ý sự khác biệt: web dùng cookie + session, mobile dùng token. Cùng 1 nhà cung cấp, 2 cơ chế khác nhau cho 2 platform khác nhau.
Cái gì có thể sai
| Vấn đề | Nguyên nhân | Cách fix |
|---|---|---|
| "Session expired" sau 30 phút không hoạt động | Server set Session timeout 30 phút | Tăng timeout hoặc implement keep-alive |
| Logout ở 1 tab, tab khác vẫn login | Cookie chưa expire, browser cache | Force reload toàn site, hoặc dùng BroadcastChannel sync logout |
| Login lúc HTTPS, sang HTTP mất session | Cookie Secure flag không gửi qua HTTP | Force toàn site HTTPS (web hiện đại nên 100% HTTPS) |
| Mobile app "tự đăng xuất" sau vài ngày | Refresh token hết hạn hoặc bị server revoke | Tăng expires_in refresh token, hoặc auto-relogin với face ID |
| JWT token "không revoke được" | Đặc tính của JWT — chỉ hết hạn tự nhiên | Implement blacklist trong Redis hoặc giảm exp xuống 5-15 phút |
| "Session fixation attack" | Server tái dùng session ID sau login | Sau login PHẢI regenerate session ID mới |
| Cookie "không gửi" qua API gọi cross-domain | SameSite=Strict hoặc thiếu CORS credentials | Đổi sang SameSite=Lax + fetch(..., {credentials: 'include'}) |
Tóm tắt 1 dòng
Cookie = nhãn dán trên áo, Session = số phòng khách sạn (data ở server), Token = vé có hologram (data tự chứa). Web hiện đại dùng cả 3: Cookie HttpOnly+Secure để chứa Session ID cho dashboard, JWT cho API public. Cookie thiếu HttpOnly = lỗ XSS lớn nhất hệ thống auth.
Đọc tiếp
- API là gì? Tại sao mọi app, mọi web hiện đại đều cần — JWT là cách auth phổ biến nhất cho API
- HTTPS, SSL, TLS — vì sao web không có ổ khoá xanh thì khách bỏ chạy — cookie Secure flag chỉ work qua HTTPS (sắp có)
- Mật khẩu được "băm" thế nào trong database — pair với session/token là phần backend verify (sắp có)
- Frontend vs Backend khác nhau ở đâu — auth state là ranh giới rõ nhất giữa 2 nửa