Mục lục
- Mục Tiêu Bài Học
- Cookie Là Gì? Server Set, Client Send Lại
- 3 Loại CookieJar Của axum-extra
- CookieJar Plain — Cơ Bản
- SignedCookieJar — HMAC Tamper-Proof
- PrivateCookieJar — AES-256-GCM Encrypt
- Attribute Quan Trọng: Secure, HttpOnly, SameSite
- Key Management — Dev vs Prod
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Phân biệt 3 loại CookieJar của
axum-extra:CookieJarplain (không bảo vệ, client đọc/ghi tự do),SignedCookieJar(HMAC SHA256 tamper-proof — client đọc được nhưng không sửa được),PrivateCookieJar(AES-256-GCM encrypt + sign — client không đọc được). - Hiểu use case mỗi loại jar cho Shop API (decision đã lock từ B19): plain cho preference UI public, Signed cho CSRF token client cần echo lại, Private cho session ID admin dashboard.
- Biết set/get cookie với
axum-extra:Cookie::build((name, value))builder chain attribute + return type handlerCookieJarđể axum tự renderSet-Cookieheader response. - Nắm 4 attribute quan trọng theo RFC 6265bis:
Secure(HTTPS-only prod mandatory),HttpOnly(chống XSS đánh cắp),SameSite(Strict/Lax/None chống CSRF),Max-Age(seconds preferred hơnExpiresUTC date). - Hiểu pattern key management 64 bytes: dev
Key::generate()ephemeral acceptable, production envCOOKIE_KEYbase64 share cross-instance qua secret manager; cấm hard-code, cấmKey::generate()ở prod. - Foundation cho B105 admin session (
PrivateCookieJar+ Redis session store) + B107 CSRF protection (SignedCookieJardouble-submit pattern); B105 sẽ extend AppState thêmcookie_key: Keyfield +impl FromRef<AppState> for Key.
Cookie Là Gì? Server Set, Client Send Lại
Cookie (bánh quy) là cơ chế HTTP stateful theo RFC 6265bis cho phép server lưu trữ state nhỏ phía client browser. Server response set header Set-Cookie, browser tự động lưu, mọi request sau gửi lại qua header Cookie:
┌──────────────────────────────────────────────────────────────────────┐
│ Flow 1 — Server SET cookie qua response header │
├──────────────────────────────────────────────────────────────────────┤
│ HTTP/1.1 200 OK │
│ Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; │
│ Max-Age=86400; Path=/ │
│ Content-Type: application/json │
└──────────────────────────────────────────────────────────────────────┘
↓
Browser lưu cookie vào cookie jar
↓
┌──────────────────────────────────────────────────────────────────────┐
│ Flow 2 — Browser SEND lại cookie ở mọi request kế tiếp │
├──────────────────────────────────────────────────────────────────────┤
│ GET /admin/dashboard HTTP/1.1 │
│ Cookie: session=abc123; theme=dark │
│ Host: admin.shop.com │
└──────────────────────────────────────────────────────────────────────┘
Use case điển hình:
- Session ID — admin dashboard login → server tạo session UUID → set cookie HttpOnly + Secure → request sau browser gửi lại → server lookup Redis
session:{id}→ biết user. - CSRF token — server set cookie chứa token random → form HTML embed token hidden field → server verify cookie token == form token (double-submit pattern B107).
- Preference UI — client chọn dark mode → server set cookie
theme=darkMax-Age 1 năm → mọi page reload load theme đúng.
Anti-pattern cần tránh: dùng cookie cho API stateless. Mobile app/SPA/partner integration nên dùng Bearer JWT (Authorization: Bearer <JWT> lock B4) — JWT self-contained, không cần server lookup, không có CSRF risk (không tự động gửi), debug dễ hơn. Cookie sinh ra cho browser context với origin-based security model, không phù hợp non-browser client.
Shop API decision lock B19 confirm B34: cookie CHỈ cho admin dashboard nội bộ (HTML form login → POST /admin/login → set session cookie → render server-side dashboard). API client truy cập /api/v1/* dùng thuần Bearer JWT, KHÔNG dùng cookie. Quyết định này giúp tách bạch concern: browser session (cookie + CSRF) cho admin, API stateless (JWT) cho client app.
3 Loại CookieJar Của axum-extra
axum-extra cung cấp 3 jar tách bạch theo mức độ bảo vệ (lock B19 với 3 feature flag cookie + cookie-signed + cookie-private trong workspace dependencies):
┌───────────────────────┬────────────────┬──────────────┬──────────────────────┐
│ Jar │ Bảo vệ │ Client đọc? │ Client sửa? │
├───────────────────────┼────────────────┼──────────────┼──────────────────────┤
│ CookieJar (plain) │ Không có │ Có (free) │ Có (free) │
│ SignedCookieJar │ HMAC SHA256 │ Có │ KHÔNG (sig fail) │
│ PrivateCookieJar │ AES-256-GCM │ KHÔNG │ KHÔNG │
│ │ + HMAC sign │ (bytes mã) │ (decrypt fail) │
└───────────────────────┴────────────────┴──────────────┴──────────────────────┘
CookieJar plain — cookie không bảo vệ, value lưu thẳng dạng text. Client mở DevTools đọc thoải mái, sửa value bằng JS hoặc tab Application → Cookies → edit. Không cần Key. Use case: preference UI không nhạy cảm (theme, language, sidebar collapse state). Anti-use: KHÔNG bao giờ lưu thông tin trust (user ID, role, permission) — client sửa được tức là escalation.
SignedCookieJar — HMAC SHA256 ký phía sau value, dạng wire <value>.<hmac>. Client ĐỌC được nội dung value (không encrypt), nhưng KHÔNG sửa được vì sửa value sẽ làm signature không match → server verify fail → reject cookie. Cần Key ≥ 64 bytes random. Use case: CSRF token (client cần đọc token để echo lại trong form/header).
PrivateCookieJar — AES-256-GCM authenticated encryption + sign, dạng wire là bytes encrypted base64 client không thể decode. DevTools chỉ thấy rác kiểu aGVsbG8gd29ybGQK.... Cần Key ≥ 64 bytes. Use case: session ID admin dashboard, data nhạy cảm (user email lưu cookie tránh DB query mỗi request).
Shop API mapping (lock B19):
- Plain: preference UI public (chỉ khi admin dashboard có UI customization riêng — chưa scope Group 4).
- SignedCookieJar: CSRF token (B107) — token random server sinh, set cookie Signed, form HTML embed cùng giá trị qua hidden field, server verify cookie == form (double-submit). Client đọc cookie để echo, nhưng không sửa được.
- PrivateCookieJar: session ID admin dashboard (B105) — UUID session sinh sau login thành công, set cookie Private, Redis store
session:{id}chứa user info; mọi request kế server decrypt cookie → lookup Redis.
CookieJar Plain — Cơ Bản
Plain CookieJar dùng cho cookie không cần bảo vệ — preference UI. Handler extract CookieJar ở arg, đọc cookie hiện có qua .get(name), thêm cookie mới qua .add(cookie), return jar (modified) — axum tự render Set-Cookie response:
// Pattern preview — admin preference (KHÔNG implement B34, demo concept)
use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::Cookie;
// GET /admin/preference/theme — đọc theme từ cookie, default light
async fn get_theme(jar: CookieJar) -> String {
match jar.get("theme") {
Some(cookie) => cookie.value().to_string(),
None => "light".to_string(),
}
}
// POST /admin/preference/theme/:value — set theme cookie
async fn set_theme(jar: CookieJar, axum::extract::Path(value): axum::extract::Path<String>) -> CookieJar {
let cookie = Cookie::build(("theme", value))
.path("/")
.max_age(time::Duration::days(365))
.same_site(axum_extra::extract::cookie::SameSite::Lax)
.build();
jar.add(cookie)
}
Điểm chú ý code:
- Extract
CookieJarở arg handler — axum-extra implFromRequestPartssẵn cho jar, không phải custom extractor. Jar đọcCookieheader request parse thành map name→value. - Builder
Cookie::build((name, value))— tuple(&str, String), chain attribute method.path()/.max_age()/.same_site()rồi.build()trảCookie<'static>. - Return type
CookieJar— handler trả jar đã modified, axum renderSet-Cookieheader tự động cho mỗi cookie thêm/sửa/xóa. Pattern này tránh phải build response manual. - KHÔNG cần
Key— plain jar không encrypt/sign, value đi thẳng vào header. AppState không phải store key cho plain.
Plain jar có .add(cookie), .remove(cookie) (set Max-Age=0 để browser xóa), .get(name) trả Option<&Cookie>, .iter() loop tất cả cookie. Đủ cho mọi use case preference UI public.
SignedCookieJar — HMAC Tamper-Proof
SignedCookieJar ký value bằng HMAC SHA256 với Key server-side. Client đọc được value (vd csrf=tk_abc123.signature_xyz), nhưng nếu sửa value thì signature không khớp → jar.get() trả None như cookie không tồn tại. Use case chính Shop API: CSRF token cho admin dashboard (B107):
// File: crates/shop-api/src/routes/admin/csrf.rs (preview B107 — KHÔNG implement B34)
use axum::extract::State;
use axum_extra::extract::SignedCookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use shop_common::error::{AppError, AppResult};
// GET /admin/csrf — sinh CSRF token mới, set cookie Signed
async fn generate_csrf(jar: SignedCookieJar) -> SignedCookieJar {
let token = generate_random_token(); // 32 bytes random base64
let cookie = Cookie::build(("csrf_token", token))
.secure(true) // HTTPS only (prod)
.same_site(SameSite::Strict) // chống CSRF mạnh nhất
.max_age(time::Duration::hours(1))
.path("/admin")
.build();
jar.add(cookie)
}
// POST /admin/products — verify CSRF token trước khi tạo product
async fn verify_csrf(jar: SignedCookieJar) -> AppResult<()> {
match jar.get("csrf_token") {
Some(_cookie) => {
// Signature valid → cookie chưa bị tamper
// Đối chiếu với form field cho double-submit (B107 chi tiết)
Ok(())
}
None => Err(AppError::BadRequest(
"CSRF token missing or tampered".to_string(),
)),
}
}
Điểm chú ý:
- Extract
SignedCookieJarđòi hỏi AppState provideKeyquaFromRef<AppState>impl — chi tiết Step 8 (key management). B105 sẽ wire vào AppState. - Client đọc được token — DevTools thấy
csrf=tk_abc123.signature_xyz. Form HTML render server-side embed token vào hidden field; JS client cũng đọc cookie được để gửi headerX-CSRF-Token. - Client KHÔNG sửa được — sửa
tk_abc123thànhtk_evil→ signaturesignature_xyzkhông match → server verify fail →jar.get()trảNone, handler reject. SameSite::Strictcho CSRF token cứng nhất — cookie KHÔNG gửi cross-site, browser tự động block CSRF attack ở tầng dưới. Step 7 chi tiết.
B107 sẽ implement đầy đủ double-submit pattern: cookie chứa token Signed + form field hidden chứa cùng token + server verify cookie value == form value. Pattern này chống được CSRF attack ngay cả khi attacker biết format token, vì attacker không thể đọc cross-origin cookie để echo lại form.
PrivateCookieJar — AES-256-GCM Encrypt
PrivateCookieJar dùng AES-256-GCM authenticated encryption + HMAC sign. Value cookie phía wire là bytes encrypted base64, client KHÔNG đọc được nội dung và KHÔNG sửa được (authenticated encryption phát hiện cả tampering). Cần Key ≥ 64 bytes. Use case Shop API: session ID admin dashboard (B105):
// File: crates/shop-api/src/routes/admin/auth.rs (preview B105 — KHÔNG implement B34)
use axum::{extract::State, response::Redirect, Form};
use axum_extra::extract::PrivateCookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use shop_common::error::AppResult;
// POST /admin/login — verify credentials, set session cookie encrypted
async fn admin_login(
State(state): State<AppState>,
jar: PrivateCookieJar,
Form(form): Form<LoginForm>,
) -> AppResult<(PrivateCookieJar, Redirect)> {
let user = verify_credentials(&state, &form).await?;
let session_id = create_session(&state, user.id).await?; // Redis SET session:{id}
let cookie = Cookie::build(("session", session_id))
.secure(true) // HTTPS only (prod)
.http_only(true) // JS không đọc được
.same_site(SameSite::Lax) // Lax cho login flow OK
.max_age(time::Duration::hours(24))
.path("/")
.build();
Ok((jar.add(cookie), Redirect::to("/admin")))
}
// POST /admin/logout — xóa session cookie + xóa Redis
async fn admin_logout(
State(state): State<AppState>,
jar: PrivateCookieJar,
) -> (PrivateCookieJar, Redirect) {
if let Some(cookie) = jar.get("session") {
let _ = delete_session(&state, cookie.value()).await;
}
(
jar.remove(Cookie::from("session")),
Redirect::to("/admin/login"),
)
}
Điểm chú ý:
- Return type tuple
(PrivateCookieJar, Redirect)— handler vừa modify jar vừa redirect; axum implIntoResponsecho tuple, renderSet-Cookieheader trước rồi 302 redirect. jar.remove(Cookie::from("session"))— set Max-Age=0 + empty value để browser xóa cookie phía client. Pattern logout chuẩn: xóa cả Redis session + browser cookie.- Bộ ba
secure + http_only + same_site=Laxcho session cookie là lock vĩnh viễn (xem Step 7).SameSite::LaxthayStrictvì login flow cần cookie gửi với top-level navigation (redirect từ /admin/login → /admin sau POST). - Client KHÔNG bao giờ thấy session_id raw — DevTools chỉ thấy bytes encrypted; ngay cả browser extension XSS đọc cookie cũng chỉ lấy được ciphertext không decrypt được.
B105 sẽ implement đầy đủ flow login + session middleware (extract PrivateCookieJar → lookup Redis session:{id} → load user vào Extension<AdminUser> cho handler downstream). Pattern PrivateCookieJar tách bạch transport security (encrypt value) khỏi storage logic (Redis lưu user info), đúng layering chuẩn web security.
Attribute Quan Trọng: Secure, HttpOnly, SameSite
RFC 6265bis định nghĩa nhiều attribute kiểm soát hành vi cookie. 4 attribute quan trọng nhất cho Shop API:
┌──────────────┬────────────────────────────────────────────────────────┐
│ Attribute │ Ý nghĩa │
├──────────────┼────────────────────────────────────────────────────────┤
│ Secure │ Chỉ gửi qua HTTPS — production MANDATORY │
│ │ Dev local HTTP: skip để test │
├──────────────┼────────────────────────────────────────────────────────┤
│ HttpOnly │ JS document.cookie KHÔNG đọc được │
│ │ Chống XSS đánh cắp session cookie │
│ │ Session cookie LUÔN bật, CSRF cookie có thể tắt │
├──────────────┼────────────────────────────────────────────────────────┤
│ SameSite │ Kiểm soát cross-site request có gửi cookie không │
│ Strict │ Chỉ gửi same-site → chống CSRF mạnh nhất │
│ Lax │ Gửi với top-level navigation (mặc định modern) │
│ None │ Gửi cross-site (require Secure) │
├──────────────┼────────────────────────────────────────────────────────┤
│ Max-Age │ Sống bao lâu (seconds) — preferred hơn Expires │
│ │ Không phụ thuộc clock skew client │
└──────────────┴────────────────────────────────────────────────────────┘
Secure — cookie chỉ gửi qua HTTPS connection. Production MANDATORY: HTTP plain text exposed session cookie qua mạng → MITM attack đánh cắp. Dev local HTTP skip để test ở localhost (browser modern cho phép cookie không Secure trên localhost). Pattern config theo env: .secure(state.config.app_env.is_production()).
HttpOnly — JavaScript document.cookie không đọc được cookie này, browser chỉ tự động gửi qua HTTP header. Mục tiêu: chống XSS attack lấy session cookie (attacker inject JS qua XSS lỗ hổng cũng không đọc được session). Session cookie LUÔN bật HttpOnly. CSRF cookie có thể tắt nếu cần JS đọc để gửi header X-CSRF-Token (pattern double-submit thay form field).
SameSite — RFC 6265bis định nghĩa 3 mode kiểm soát cross-site behavior:
Strict: cookie chỉ gửi với request cùng site origin. Chống CSRF mạnh nhất nhưng break flow OAuth redirect (cookie không gửi khi user click link từ email về site). Dùng cho CSRF token và cookie sensitive đặc biệt.Lax: cookie gửi với top-level navigation (user click link, browser GET URL trực tiếp), không gửi với cross-site POST/iframe/img. Mặc định modern browser từ 2020. Cân bằng UX vs security — dùng cho session cookie admin login.None: cookie gửi cross-site mọi context. Bắt buộc kèmSecure. Dùng cho widget embed third-party, hiếm khi áp dụng cho Shop API admin.
Max-Age vs Expires: cả hai set lifetime cookie, nhưng Max-Age preferred (RFC 6265bis):
Max-Age=86400— seconds count từ thời điểm browser nhận response, không phụ thuộc clock client (clock skew không ảnh hưởng).Expires=Wed, 14 Jun 2026 12:00:00 GMT— UTC date cụ thể, browser tính dựa system clock; nếu clock sai thì cookie expire sai.- Modern browser hiểu cả hai;
Max-AgeoverrideExpiresnếu có cùng cookie.
Shop API attribute lock vĩnh viễn:
- Session cookie (B105):
HttpOnly + Secure (prod) + SameSite=Lax + Max-Age=24h + Path=/— đủ cho admin login flow. - CSRF cookie (B107):
Secure (prod) + SameSite=Strict + Max-Age=1h + Path=/admin— không HttpOnly (JS cần đọc để echo header),Strictcho chống CSRF mạnh, TTL ngắn 1h refresh per session. - Preference cookie plain:
SameSite=Lax + Max-Age=1y + Path=/— không nhạy cảm, không cần Secure/HttpOnly, TTL dài.
Key Management — Dev vs Prod
SignedCookieJar và PrivateCookieJar cần cookie::Key 64 bytes random để HMAC sign + AES-256-GCM encrypt. Cookie crate (dep của axum-extra) derive HKDF 2 subkey từ master key 64 bytes: 32 bytes cho AES, 32 bytes cho HMAC.
Pattern dev:
use axum_extra::extract::cookie::Key;
// Dev — sinh key random mỗi lần bootstrap process
let key = Key::generate();
// → 64 bytes random từ OsRng; restart process sinh key mới
// → session cũ encrypted với key cũ sẽ KHÔNG decrypt được
// → acceptable cho dev: session ephemeral, restart frequent OK
Pattern production:
use axum_extra::extract::cookie::Key;
use base64::{engine::general_purpose::STANDARD, Engine as _};
// Prod — đọc key từ env, base64 decode
let key_b64 = std::env::var("COOKIE_KEY")
.context("COOKIE_KEY env required in production")?;
let key_bytes = STANDARD.decode(&key_b64)
.context("COOKIE_KEY must be valid base64")?;
if key_bytes.len() < 64 {
anyhow::bail!("COOKIE_KEY must be >= 64 bytes");
}
let key = Key::from(&key_bytes);
// → key share cross-pod qua secret manager (Vault, AWS Secrets Manager, K8s Secret)
// → mọi pod cùng key → session encrypted ở pod A decrypt được ở pod B
Sinh key 64 bytes một lần cho prod (CLI):
# Generate key một lần, lưu vào secret manager
openssl rand -base64 64 | tr -d '\n'
# → vd: "k3JZ4mPx9...random64bytes...base64encoded=="
# → store vào Vault path secret/shop/cookie-key
# → K8s Secret: kubectl create secret generic shop-cookie --from-literal=key="$KEY"
CẤM:
- Hard-code key trong source code — commit vào git = security breach. Lịch sử git lưu vĩnh viễn, attacker pull repo public/internal lấy key decrypt được mọi session.
- Dùng
Key::generate()ở production — mỗi pod sẽ có key khác nhau. Session encrypted ở pod A khi load balancer route request tiếp theo qua pod B sẽ không decrypt được → user bị logout liên tục. Đặc biệt nguy hiểm khi rolling deploy: pod cũ key A, pod mới key B, session lifespan cross deploy = invalidate. - Reuse key cũ sau rotation — rotation đúng cách (B289): grace period dual-key window 7-30 ngày verify cả key cũ + key mới, sau đó retire key cũ. KHÔNG reuse key cũ về sau (compromised once = compromised forever).
Setup pattern AppState extend (B105 implement):
// File: crates/shop-api/src/state.rs (B105 extend từ B17 + B28)
use axum::extract::FromRef;
use axum_extra::extract::cookie::Key;
use shop_common::config::AppConfig;
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub config: Arc<AppConfig>,
pub cookie_key: Key, // ← Add khi B105
// pub pool: PgPool, // G6
// pub redis: RedisPool, // G18
}
// SignedCookieJar / PrivateCookieJar extract Key qua FromRef sub-state pattern
impl FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Self {
state.cookie_key.clone() // Key có impl Clone cheap
}
}
Pattern FromRef cho phép extractor con (SignedCookieJar, PrivateCookieJar) extract sub-state Key từ AppState mà không cần handler nhận State<AppState> trực tiếp. B105 sẽ load cookie_key trong AppConfig::from_env (đọc env COOKIE_KEY hoặc Key::generate() theo app_env), wire vào AppState::new.
Rotation strategy (B289 deep dive): cookie crate hỗ trợ multi-key verify qua wrapper. Khi rotation: thêm Key mới, giữ Key cũ trong list verify; cookie cũ vẫn decrypt được bằng key cũ trong grace period; cookie mới sign bằng key mới; sau N tháng (typical 30 ngày) retire key cũ. Monitoring metric "decrypt fail rate" để biết khi nào safe cut-over hoàn toàn.
Tổng Kết
- 3 loại CookieJar của
axum-extra: Plain (không bảo vệ, client đọc/ghi tự do), Signed (HMAC SHA256 — client đọc được nhưng không sửa được), Private (AES-256-GCM encrypt + sign — client không đọc được nội dung). - Use case: Plain cho preference UI (theme, language), Signed cho CSRF token client cần echo lại (B107 double-submit), Private cho session ID admin dashboard (B105).
- Shop API decision lock B19 confirm B34: cookie CHỈ cho admin dashboard nội bộ (B105 session + B107 CSRF); API client
/api/v1/*(mobile, SPA, partner) dùng thuần Bearer JWT (lock B4) KHÔNG dùng cookie. - 4 attribute quan trọng theo RFC 6265bis:
Secure(HTTPS-only prod mandatory),HttpOnly(chống XSS đánh cắp),SameSite3 mode (Strictchống CSRF mạnh nhất /Laxmặc định modern browser /Nonecross-site require Secure),Max-Ageseconds preferred hơnExpiresUTC date (không phụ thuộc clock skew client). - Attribute lock Shop API vĩnh viễn: Session cookie
HttpOnly + Secure (prod) + SameSite=Lax + Max-Age=24h; CSRF cookieSecure (prod) + SameSite=Strict + Max-Age=1h(không HttpOnly vì JS cần đọc); Preference cookie plainSameSite=Lax + Max-Age=1y. - Key management: 64 bytes random — dev
Key::generate()ephemeral acceptable (restart sinh key mới invalidate session OK), production đọc từ envCOOKIE_KEYbase64-decoded quaKey::from(&decoded_bytes), share cross-instance qua secret manager (Vault / AWS Secrets Manager / K8s Secret). - CẤM: hard-code key trong source code (commit git = security breach),
Key::generate()ở production (mỗi pod khác key → session invalidate cross-pod khi load balancer route khác pod), reuse key cũ sau rotation. Rotation chi tiết B289 (dual-key window grace period 7-30 ngày). - File path lock: B105 sẽ extend
crates/shop-api/src/state.rsthêmcookie_key: Keyfield vàoAppState+impl FromRef<AppState> for Keycho sub-state extract;SignedCookieJar/PrivateCookieJarextractor đọcKeyquaFromRefpattern không cần handler nhậnState<AppState>trực tiếp. - Foundation cho B105 admin session (
PrivateCookieJar+ Redissession:{id}store) + B107 CSRF protection (SignedCookieJardouble-submit pattern); B34 conceptual + preview, KHÔNG tạo file thực tế Shop API — B105/B107 implement đầy đủ khi infra Redis + jsonwebtoken wire vào AppState.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 3 loại CookieJar khác nhau ở điểm gì (bảo vệ, đọc/sửa client, key requirement)? Khi nào dùng cái nào cho Shop API?
- Bộ ba attribute
Secure + HttpOnly + SameSite=Strictphù hợp cho cookie nào? Tại sao session cookie lại dùngSameSite=LaxthayStrict? - Shop API API client (mobile, frontend SPA, partner integration) dùng cookie hay Bearer JWT cho auth? Lý do tách bạch cookie cho admin và JWT cho API client?
PrivateCookieJarcầnKeybao nhiêu bytes và để làm gì? Quản lý key ở dev vs prod khác nhau ra sao? PatternFromRef<AppState> for Keygiải quyết vấn đề gì?- Anti-pattern hard-code
Keytrong source code có risk gì cụ thể? Tại saoKey::generate()không dùng được ở prod? Pattern đúng để sinh, share, rotate key prod?
Đáp án
- 3 loại CookieJar tách bạch theo mức độ bảo vệ.
CookieJarplain (featurecookie): không bảo vệ — value lưu thẳng dạng text, client mở DevTools đọc thoải mái và sửa bằng JS/UI; KHÔNG cầnKey; dùng cho preference UI không nhạy cảm (theme, language, sidebar collapse). Anti-use: KHÔNG bao giờ lưu trust info (user_id, role) vì client sửa được = privilege escalation.SignedCookieJar(featurecookie-signed): HMAC SHA256 ký phía sau value dạng wire<value>.<hmac>; client ĐỌC được nội dung value (không encrypt), KHÔNG sửa được vì sửa value sẽ làm signature không khớp → server verify fail →jar.get()trảNone; CẦNKey≥ 64 bytes; dùng cho CSRF token client cần echo lại trong form/header (B107 double-submit pattern).PrivateCookieJar(featurecookie-private): AES-256-GCM authenticated encryption + HMAC sign; value phía wire là bytes encrypted base64 client không decode được; client KHÔNG đọc nội dung và KHÔNG sửa được (authenticated encryption phát hiện cả tampering); CẦNKey≥ 64 bytes; dùng cho session ID admin dashboard (B105) và data nhạy cảm. Shop API mapping lock B19: Plain cho preference UI public (chỉ nếu admin dashboard có customization riêng — chưa scope Group 4); SignedCookieJar cho CSRF token (B107) — token random server sinh, set cookie Signed, form HTML embed cùng giá trị qua hidden field, server verify cookie == form (double-submit); PrivateCookieJar cho session ID admin dashboard (B105) — UUID session sinh sau login, set cookie Private, Redis storesession:{id}chứa user info, mọi request kế server decrypt cookie → lookup Redis. - Bộ ba
Secure + HttpOnly + SameSite=Strictphù hợp cho cookie sensitive cao cần chống cả XSS + CSRF + MITM: ví dụ admin password reset token, payment confirmation token, banking transaction token.Securechỉ gửi qua HTTPS (chống MITM đánh cắp qua mạng plaintext);HttpOnlyJS không đọc được (chống XSS exfiltration);SameSite=Strictchỉ gửi same-site request (chống CSRF mạnh nhất, không gửi cross-site bất kỳ context nào). Tại sao session cookie dùngSameSite=LaxthayStrict:SameSite=Strictphá UX login flow — khi user click link từ email (vd "Vào dashboard") browser navigate sang admin.shop.com, cookie session NOT gửi → user bị logout giả → frustrating.Laxcho phép cookie gửi với top-level navigation (user click link, browser GET URL trực tiếp) nhưng vẫn block cross-site POST/iframe/img → vẫn chống được CSRF main vector (POST từ malicious site qua hidden form). Cân bằng UX vs security — modern browser từ 2020 mặc địnhLaxnếu không khai báo. CSRF cookie thì cứng nhắc hơn dùngStrictvì cookie này chỉ dùng nội bộ form admin, không cần gửi với navigation từ ngoài. Shop API lock: session cookieHttpOnly + Secure + SameSite=Lax + Max-Age=24h, CSRF cookieSecure + SameSite=Strict + Max-Age=1h(không HttpOnly vì JS cần đọc echo headerX-CSRF-Tokentrong AJAX request). - Shop API API client (mobile app, frontend SPA, partner integration) dùng thuần Bearer JWT (lock B4 header convention
Authorization: Bearer <JWT>), KHÔNG dùng cookie. Endpoint/api/v1/*stateless 100%, mỗi request kèm JWT trong header, server verify signature + expiry không cần lookup session storage. Lý do tách bạch cookie cho admin và JWT cho API client: (a) Cookie sinh ra cho browser context với origin-based security model (SameSite, Secure, HttpOnly, browser tự động gửi) — không phù hợp non-browser client như mobile app (iOS/Android không có cookie jar native, phải implement manual = bug-prone) hoặc partner integration server-to-server (không có browser, cookie ko có ý nghĩa). (b) JWT self-contained — claims chứa user_id + role + exp + scope, server không cần DB/Redis lookup cho mỗi request → scale tốt hơn (Shop API có 60 endpoint, nếu mỗi request lookup session = 60 lần Redis call per page). (c) CSRF risk — cookie tự động gửi với mọi request từ origin → vector CSRF attack; JWT KHÔNG tự động gửi, client phải explicit add vào header → CSRF không apply (không có "cross-site JWT" attack). (d) Debug dễ hơn — JWT trong header thấy ngay qua DevTools/curl, cookie ẩn trong jar phải mở tab Application; replay request từ Postman với JWT trivial, với cookie phải copy paste cookie string. (e) Mobile native support — mobile có thư viện JWT well-established (jose-swift, JJWT Android), không có cookie jar native. (f) Multi-platform consistency — frontend SPA, mobile, partner cùng dùng JWT → 1 auth scheme cross-client, server logic đơn giản. Admin dashboard ngược lại render server-side template HTML form login → browser flow tự nhiên với cookie (HttpOnly chống XSS, SameSite chống CSRF, Secure chống MITM); session ID nhỏ (UUID 36 ký tự) trong PrivateCookieJar + Redis store cho user info detail. Tách bạch concern: browser session (cookie + CSRF + render server-side) cho admin, API stateless (JWT + JSON) cho client app — không trộn lẫn để tránh complexity và security pitfall. PrivateCookieJarcầnKey64 bytes random vì cookie crate (dep của axum-extra) derive HKDF 2 subkey từ master key 64 bytes: 32 bytes cho AES-256-GCM (authenticated encryption — encrypt value + tag auth), 32 bytes cho HMAC SHA256 (sign integrity check secondary, defense in depth). Master key 64 bytes đủ entropy cho cả hai subkey không correlate.SignedCookieJarcùng kích thước key (64 bytes) để consistent API và sẵn sàng nâng cấp Signed → Private không phải re-key. Quản lý dev vs prod: (a) Dev: sinhKey::generate()lúc bootstrap process, key ổn định trong vòng đời process — restart sinh key mới làm invalidate session cũ; chấp nhận được cho dev vì session ephemeral, dev restart frequent, mỗi developer 1 instance riêng. Code:let key = Key::generate();ngay trongAppConfig::from_envkhiapp_env == Dev. (b) Prod: đọc từ env variableCOOKIE_KEYbase64-decoded quaKey::from(&base64::decode(env)?); key sinh 1 lần quaopenssl rand -base64 64, lưu vào secret manager (HashiCorp Vault, AWS Secrets Manager, K8s Secret), inject vào pod qua env variable lúc deploy; mọi pod cùng key → session encrypted ở pod A decrypt được ở pod B (load balancer route khác pod OK). Code: validateCOOKIE_KEYtồn tại + base64 valid + bytes >= 64, fail-fast nếu missing/invalid (bootstrap không lên được, prevent silent misconfig). PatternFromRef<AppState> for Keygiải quyết vấn đề:SignedCookieJarvàPrivateCookieJarimplFromRequestParts<S> where S: Send + Sync, Key: FromRef<S>— extract sub-stateKeytừ outer stateAppStatemà không cần handler nhậnState<AppState>trực tiếp. Implement:impl FromRef<AppState> for Key { fn from_ref(state: &AppState) -> Self { state.cookie_key.clone() } }—Keycó implClonecheap (Arc internal). Handler signature gọn:async fn admin_login(jar: PrivateCookieJar, Form(form): Form<LoginForm>)không cầnState<AppState>trừ khi truy cập field khác (Redis pool, config). Pattern này áp dụng cho mọi sub-state Shop API tương lai (pool, redis) khi sub-extractor cần thay vì handler phải receive full AppState. B105 sẽ extendAppStatethêmcookie_key: Keyfield +impl FromRef.- Anti-pattern hard-code
Keytrong source code risk cụ thể: (a) Commit vào git = security breach vĩnh viễn — git history lưu mọi commit không xóa được (force push xóa được nhưng nếu repo public hoặc team đông đã pull/fork = compromised); attacker pull repo lấy key decrypt được mọi session encrypt bằng key đó, đọc nội dung cookie sensitive (user email, role), tạo session giả mạo. (b) Cross-environment leak — dev có thể chạy với cùng key như staging/prod nếu hard-code, phát triển local accidentally test với prod data. (c) Rotation không feasible — đổi key phải deploy code mới, downtime + risk khi rollback. (d) Code review không bắt được nếu encode dạng base64 trong const, người review không nhận ra là key thật. Tại saoKey::generate()không dùng được ở prod:Key::generate()sinh random từOsRngmỗi lần process start — mỗi pod sẽ có key khác nhau. Hậu quả runtime: (a) Session encrypted ở pod A khi load balancer route request tiếp theo qua pod B sẽ không decrypt được (key B khác key A) → cookie verify fail → user bị logout ngay sau khi login → support flood. (b) Rolling deploy cực kỳ nguy hiểm: pod cũ key A, pod mới key B chạy parallel 5-10 phút; user nhận cookie từ pod cũ rồi request tiếp routing pod mới = session loss; kết quả ALL user logout khi deploy = catastrophic UX. (c) Horizontal pod autoscaler scale lên thêm pod = key mới = ảnh hưởng user. (d) Restart pod (OOM kill, health check fail, manual restart) cũng làm key mới. Pattern đúng để sinh, share, rotate key prod: (1) Sinh: 1 lần duy nhất qua CLIopenssl rand -base64 64 | tr -d '\\n'trên máy admin secure (không phải laptop developer), output base64 string 88 ký tự. (2) Share: lưu vào secret manager tier 1 (HashiCorp Vault với policy admin-only read, AWS Secrets Manager với IAM role pod-specific, K8s Secret với RBAC role binding); CẤM commit git, CẤM dán Slack/email, CẤM gửi qua chat plaintext. (3) Inject: deployment template (Helm chart, K8s manifest) reference secret quaenvFrom.secretRefhoặc init-container fetch từ Vault; pod khởi động đọc envCOOKIE_KEYbase64 decode validate >= 64 bytes, fail-fast nếu missing/invalid prevent silent misconfig. (4) Rotate (B289 deep dive): cookie crate hỗ trợ multi-key verify qua wrapper; thêmKeymới vào AppState (vdcookie_keys: Vec<Key>); cookie mới sign bằng key đầu tiên (key mới nhất), verify thử với từng key trong list — cookie cũ vẫn decrypt được bằng key cũ trong grace period 7-30 ngày; sau grace period retire key cũ (remove khỏi list); monitor metric "decrypt fail rate" qua Prometheus để biết khi nào safe cut-over hoàn toàn. (5) Audit: log mỗi access key (Vault audit log), alert nếu key access bất thường (off-hours, từ IP unknown); rotate ngay nếu nghi ngờ compromise.
Bài Tiếp Theo
Bài 35: Form Extractor Cho HTML Form — chi tiết Form<T> extractor cho application/x-www-form-urlencoded (admin login page B105, Stripe webhook B197), MultiPart preview B36 cho file upload, integrate với template engine (askama, maud) cho server-side render.
