Session và cookie: chọn khi nào
Vì sao phải chốt cách giữ trạng thái trước khi viết dòng code auth đầu tiên
Đây là bài đầu tiên của module nền tảng xác thực. Trước khi nói tới JWT hay refresh token, ta phải trả lời một câu hỏi gốc: sau khi người dùng đăng nhập, server biết người đó là ai bằng cách nào? Mọi lỗi auth về sau (đăng xuất không mất hiệu lực, token sống quá lâu, cookie bị đọc trộm) đều bắt nguồn từ chỗ này.
Sau 20 phút, bạn sẽ:
- Phân biệt được session-based và token-based ở mức đủ để chọn cho project.
- Biết cookie là "phong bì" client giữ hộ, không phải nơi chứa bí mật.
- Cấu hình được cookie với
HttpOnly,Secure,SameSitecho một API cụ thể. - Trả lời được ba câu tôi luôn hỏi khi review: token này ai giữ, sống bao lâu, bị lộ thì sao.
Session: trạng thái đăng nhập nằm ở server
Session-based là cách kể chuyện như sau: người dùng gửi email + mật khẩu, server kiểm tra đúng thì tạo một session ID ngẫu nhiên, lưu vào session store (memory, Redis, hoặc bảng DB) kèm dữ liệu như userId, role, createdAt. Server trả session ID về cho client. Các request sau, client gửi kèm ID đó, server tra store và biết bạn là ai.
Điểm cốt lõi: server giữ sự thật, client chỉ giữ chìa khoá. Vì vậy muốn đăng xuất, muốn thu hồi một phiên bị lộ, bạn chỉ cần xoá bản ghi trong store. Đây là lý do các sản phẩm web truyền thống vẫn dùng session: thu hồi tức thì, không cần chờ token hết hạn.
flowchart TD
Login["🔑<br/>POST /login"] --> Check["Kiểm tra mật khẩu"]
Check --> Create["Tạo session ID"]
Create --> Store["💾<br/>Lưu vào Redis"]
Store --> Cookie["Set-Cookie: sid=...<br/>HttpOnly, Secure, SameSite"]
Cookie --> Browser["🌐<br/>Trình duyệt giữ cookie"]
Browser --> Req["Request sau gửi kèm sid"]
Req --> Store
Cookie: chiếc phong bì trình duyệt tự gửi kèm
Cookie là cặp name=value kèm thuộc tính, do trình duyệt lưu và tự động gửi lên đúng domain ở mỗi request. Chính chữ "tự động" này vừa là điểm mạnh (không phải viết code đính kèm) vừa là nguồn gốc của CSRF.
Ba thuộc tính quyết định phần lớn độ an toàn:
| Thuộc tính | Tác dụng | Bỏ qua thì sao |
|---|---|---|
HttpOnly |
JavaScript không đọc được cookie | Một lỗ XSS là kẻ tấn công đọc được session ID |
Secure |
Chỉ gửi cookie qua HTTPS | Cookie bay qua HTTP và bị bắt trên wifi công cộng |
SameSite |
Hạn chế gửi cookie trong request cross-site | Form giả từ site khác vẫn gửi cookie kèm, tức CSRF |
SameSite có ba mức thường dùng: Strict (không gửi khi đi từ site khác, kể cả bấm link), Lax (không gửi ở request cross-site, trừ điều hướng GET cấp cao nhất, mặc định hiện nay của trình duyệt), và None (gửi mọi nơi, bắt buộc đi kèm Secure).
Một cấu hình session hợp lý cho API Node.js:
app.use(session({
name: "sid",
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // bật ở production, sau HTTPS
sameSite: "lax",
maxAge: 1000 * 60 * 60 * 8
}
}));
Không set maxAge thì đó là session cookie, đóng trình duyệt là mất, tiện cho máy dùng chung nhưng phiền cho người dùng thật.
Bài tập 1. Một bạn trong nhóm cấu hình cookie: { sameSite: "none" } để mobile app gọi API được, không có secure, không có httpOnly. Hãy chỉ ra hai lỗi và cách sửa.
Xem gợi ý
sameSite: "none"mà thiếusecurethì trình duyệt hiện đại sẽ từ chối lưu cookie. Thêmsecure: truevà bắt buộc chạy HTTPS.- Thiếu
httpOnlythì XSS đọc được session ID. ThêmhttpOnly: true. - Lưu ý thêm: mobile app gọi API thường không cần cookie cross-site, nên hãy tự hỏi có thật sự cần
nonehay chỉ đang che một lỗi cấu hình CORS.
Session-based hay token-based: chọn theo bảng, không theo cảm tính
| Tiêu chí | Session-based | Token-based (JWT) |
|---|---|---|
| State ở đâu | Server (Redis/DB) | Client giữ token, server chỉ verify chữ ký |
| Thu hồi một phiên | Xoá là xong, tức thì | Khó, phải chờ hết hạn hoặc thêm denylist |
| Scale ngang | Cần session store dùng chung | Dễ hơn vì stateless |
| Client | Trình duyệt (tự gửi cookie) | Mobile, service-to-service, nhiều nền tảng |
| Rủi ro chính | CSRF | Token bị lộ thì sống tới lúc hết hạn |
Cách chọn thực dụng: web app có trang đăng nhập và trình duyệt là client chính, ưu tiên session + cookie. Hệ thống có mobile app, nhiều service gọi nhau, hoặc API cho bên thứ ba, token-based hợp hơn. Nếu chưa chắc, chọn session, vì nó cho bạn quyền thu hồi và dễ debug hơn nhiều.
Bài tập 2. Chọn cơ chế và giải thích một câu cho từng case: (a) dashboard quản trị nội bộ cho 30 nhân viên, server-render; (b) app mobile + web cùng gọi một API, cần đăng xuất khỏi mọi thiết bị khi đổi mật khẩu.
Xem gợi ý
(a) Session + cookie HttpOnly, Secure, SameSite=Lax. Ít client, cần thu hồi nhanh khi nhân viên nghỉ việc.
(b) Token-based cho API, nhưng phải có refresh token lưu ở server và cơ chế thu hồi theo phiên. Đây chính là nội dung module 2, nên tạm thời chỉ cần ghi nhận yêu cầu "đăng xuất mọi thiết bị" là một yêu cầu về thu hồi, không phải về token.
Session-based: server giữ trạng thái, client chỉ giữ _____ trong cookie. Thuộc tính _____ ngăn JavaScript đọc cookie. Cookie chỉ được gửi qua HTTPS khi bật _____. Thuộc tính _____ giảm rủi ro CSRF. Với session-based, muốn thu hồi một phiên thì _____.
Front: Session-based lưu trạng thái đăng nhập ở đâu? Back: Ở server (session store); client chỉ giữ session ID trong cookie.
Front: Cookie HttpOnly có tác dụng gì? Back: JavaScript không đọc được cookie, nên XSS khó lấy session ID.
Front: Thuộc tính Secure làm gì? Back: Chỉ cho trình duyệt gửi cookie qua HTTPS.
Front: SameSite=Lax nghĩa là gì? Back: Không gửi cookie trong request cross-site, trừ điều hướng GET cấp cao nhất.
Front: SameSite=None bắt buộc đi kèm gì? Back: Secure, nếu không trình duyệt sẽ từ chối cookie.
Front: Session ID có phải bí mật không? Back: Có. Ai có ID là đăng nhập được thay người đó, nên phải thu hồi được.
Front: Vì sao session-based dễ thu hồi hơn JWT? Back: State nằm ở server, xoá bản ghi session là phiên mất hiệu lực ngay.
Front: Session-based gặp khó gì khi scale ngang? Back: Phải dùng session store chia sẻ như Redis thay vì memory từng instance.
Front: Stateless trong token-based nghĩa là gì? Back: Server không lưu phiên, chỉ kiểm tra chữ ký của token ở mỗi request.
Front: Vì sao cookie dễ bị CSRF? Back: Trình duyệt tự gửi cookie kèm request, kể cả request phát sinh từ site khác.
Front: Cookie không set maxAge thì sao? Back: Là session cookie, mất khi đóng trình duyệt.
Front: Ba câu phải trả lời trước khi chọn cơ chế auth? Back: Token này ai giữ, sống bao lâu, bị lộ thì sao.
Ba câu hỏi trước khi chốt kiểu đăng nhập
- Ai giữ token? Trình duyệt thì dùng cookie
HttpOnly. Mobile hay service khác thì không có cookie, phải tính chuyện khác. - Sống bao lâu? Session 8 giờ cho nhân viên văn phòng là ổn; API công khai cần thời hạn ngắn hơn nhiều.
- Bị lộ thì sao? Nếu câu trả lời là "không làm gì được", bạn đang thiếu cơ chế thu hồi.
Lỗi tôi gặp nhiều nhất khi review code: đặt session ID vào localStorage cho tiện, rồi tự hỏi vì sao một lỗ XSS là mất tài khoản. Một lỗi khác: để secret session hard-code trong repo. Ba mẹo còn lại: tên cookie đừng đặt mặc định connect.sid cho dễ bị dò, luôn bật secure ở production, và nhớ rằng tăng maxAge là tăng thời gian kẻ tấn công dùng được phiên bị lộ.
Mang theo gì sang bài JWT
Tóm lại: session giữ sự thật ở server và đổi lấy khả năng thu hồi tức thì; cookie là phong bì client giữ hộ, an toàn hay không nằm ở HttpOnly, Secure, SameSite. Chọn session khi client chính là trình duyệt và bạn cần kiểm soát phiên; chọn token-based khi có nhiều loại client và nhiều service. Bài tiếp theo mở một token ra xem bên trong có gì, kiểm tra chữ ký thế nào, và những lỗi hay gặp khi đọc JWT.