Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu CORS (Cross-Origin Resource Sharing) — phân biệt preflight vs simple request.
- Áp dụng
tower-http::CorsLayervới 4 config chính:allow_origin,allow_methods,allow_headers,max_age. - Phân biệt CORS dev permissive vs production strict theo môi trường.
- Implement 6 OWASP security headers: X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy, Permissions-Policy, Strict-Transport-Security.
- Hiểu pattern SameSite cookie Strict vs Lax vs None và lý do Shop API skip.
- Áp Shop API multi-env config (Local / Staging / Production) qua
ALLOWED_ORIGINSenv.
CORS Là Gì + Preflight Request
Browser thực thi Same-Origin Policy (RFC 6454): JavaScript chỉ gọi được API cùng origin (scheme + host + port). Khi frontend https://shop.blogcode.vn muốn fetch API https://api.blogcode.vn, browser block mặc định để chống cross-site request forgery.
CORS (Cross-Origin Resource Sharing) là cơ chế server cấp phép cross-origin qua response header Access-Control-Allow-*. Browser kiểm tra header này trước khi expose response cho JavaScript đọc.
Browser chia request thành 2 loại theo Fetch spec:
Simple request — browser gửi thẳng (không preflight). Điều kiện: method ∈ {GET, POST, HEAD} + Content-Type ∈ {text/plain, application/x-www-form-urlencoded, multipart/form-data} + chỉ header CORS-safelisted. Browser check Access-Control-Allow-Origin trên response — nếu match origin client thì expose body, ngược lại JavaScript nhận lỗi CORS.
Preflight request — browser gửi OPTIONS request trước, chờ server cấp phép, sau đó mới gửi request thật. Trigger preflight khi: method ∈ {PATCH, PUT, DELETE}, hoặc Content-Type application/json, hoặc có custom header (vd Authorization, Idempotency-Key).
Ví dụ frontend gọi PATCH /api/v1/products:
--- Browser gửi preflight OPTIONS ---
OPTIONS /api/v1/products HTTP/1.1
Origin: https://shop.blogcode.vn
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type, Authorization
--- Server trả allow ---
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.blogcode.vn
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Max-Age: 86400
--- Browser cache preflight 24h rồi gửi request thật ---
PATCH /api/v1/products HTTP/1.1
Origin: https://shop.blogcode.vn
Content-Type: application/json
Authorization: Bearer eyJ...
{"name": "iPhone 17"}
Browser cache preflight theo Access-Control-Max-Age (24h là phổ biến) → các request kế tiếp cùng origin + method + header skip OPTIONS, giảm overhead.
Hai pitfall thường gặp:
- Không cấu hình CORS → frontend nhận "CORS error: No 'Access-Control-Allow-Origin' header", không gọi được API.
Access-Control-Allow-Origin: *kèm cookie → RFC 6454 cấm: server phải echo origin cụ thể khiallow_credentials = true, không được wildcard.
tower-http CorsLayer Cấu Hình Production
Crate tower-http đã có sẵn từ workspace root (B10 lock + B50 mở rộng feature compression-full + decompression-full). Feature cors đã enable mặc định khi dùng full; nếu chưa, xác nhận trong shop/Cargo.toml:
# File: shop/Cargo.toml — workspace root
[workspace.dependencies]
tower-http = { version = "0.6", features = [
"compression-full",
"decompression-full",
"cors", # B77 — CorsLayer
] }
Tạo file mới crates/shop-api/src/middleware/cors.rs chứa helper factory:
// File: crates/shop-api/src/middleware/cors.rs
use std::time::Duration;
use axum::http::{header, HeaderName, HeaderValue, Method};
use tower_http::cors::{AllowOrigin, CorsLayer};
use crate::config::{AppConfig, Environment};
/// Build CorsLayer với cấu hình theo Environment.
///
/// Local: AllowOrigin::any() — permissive cho dev/Postman/curl.
/// Staging/Production: AllowOrigin::list(env_origins) — strict.
/// Panic fail-fast nếu Staging/Production mà ALLOWED_ORIGINS empty.
pub fn cors_layer(config: &AppConfig) -> CorsLayer {
let origins: Vec<HeaderValue> = config
.allowed_origins
.iter()
.filter_map(|s| HeaderValue::from_str(s).ok())
.collect();
let allow_origin = match config.environment {
Environment::Local => AllowOrigin::any(),
Environment::Staging | Environment::Production => {
assert!(
!origins.is_empty(),
"ALLOWED_ORIGINS env MANDATORY trong {:?} — list rỗng \
sẽ block toàn bộ frontend",
config.environment
);
AllowOrigin::list(origins)
}
};
CorsLayer::new()
.allow_origin(allow_origin)
.allow_methods([
Method::GET,
Method::POST,
Method::PATCH,
Method::DELETE,
Method::OPTIONS,
Method::HEAD,
])
.allow_headers([
header::CONTENT_TYPE,
header::AUTHORIZATION,
HeaderName::from_static("idempotency-key"),
HeaderName::from_static("x-request-id"),
header::IF_NONE_MATCH,
])
.expose_headers([
HeaderName::from_static("x-request-id"),
header::ETAG,
header::LOCATION,
HeaderName::from_static("x-total-count"),
])
.max_age(Duration::from_secs(86_400))
.allow_credentials(false)
}
Bảng lock vĩnh viễn các quyết định CORS Shop API:
- Local dùng
AllowOrigin::any()— dev/Postman/curl không cần khai báo origin cụ thể. - Staging / Production dùng
AllowOrigin::list(env_origins)— strict;assert!fail-fast crash startup nếu list rỗng, tránh deploy production mà miss config. allow_credentials(false)MANDATORY — Shop API stateless JWT Bearer (B11 lock continued); không gửi cookie cross-origin nên không cần credentials.- Methods explicit list 6 method (GET, POST, PATCH, DELETE, OPTIONS, HEAD); KHÔNG dùng
Anyđể giảm attack surface. - Headers allowed:
Content-Type(JSON body),Authorization(Bearer JWT),Idempotency-Key(B66 lock),X-Request-Id(B39 lock),If-None-Match(ETag conditional GET B62 lock). - Headers expose:
X-Request-Id(frontend log correlation),ETag(cache validator),Location(POST 201 redirect target),X-Total-Count(pagination total — lock B4). max_age 86400(24h) — cache preflight 1 ngày, giảm OPTIONS subsequent request.
AppConfig Thêm allowed_origins Env
AppConfig đã có 8 field từ B56 (database_url, pool tuning, server_bind, environment). Bài này thêm 1 field allowed_origins: Vec<String> parse từ env ALLOWED_ORIGINS CSV format.
// File: crates/shop-api/src/config.rs (snippet — chỉ phần thêm B77)
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct AppConfig {
pub database_url: String,
pub pool_max_connections: u32,
pub pool_min_connections: u32,
pub pool_acquire_timeout: Duration,
pub pool_idle_timeout: Duration,
pub pool_max_lifetime: Option<Duration>,
pub pool_statement_cache: usize,
pub server_bind: String,
pub environment: Environment,
// B77 — CORS allowed origins (CSV env)
pub allowed_origins: Vec<String>,
}
impl AppConfig {
pub fn from_env() -> anyhow::Result<Self> {
// ... existing 8 field parse (B56)
// B77 — parse CSV "https://a.com,https://b.com" → Vec<String>
let allowed_origins = std::env::var("ALLOWED_ORIGINS")
.unwrap_or_default()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<String>>();
Ok(Self {
// ... existing field
allowed_origins,
})
}
}
Cập nhật .env.example commit kèm template cho onboarding dev mới:
# File: shop/.env.example (snippet thêm B77)
# CORS — danh sách origin cho phép (production CSV).
# Local dev: để trống → CorsLayer dùng AllowOrigin::any() permissive.
# Staging/Production: MANDATORY, panic startup nếu rỗng.
ALLOWED_ORIGINS=https://shop.blogcode.vn,https://admin.blogcode.vn
Pattern parse CSV graceful: split(',') + trim() + filter(!empty) đảm bảo trailing comma hoặc whitespace dư không gây entry rỗng. Production set qua orchestrator (Docker secret, K8s ConfigMap, fly.io secret) — KHÔNG copy .env lên prod.
6 Security Headers — OWASP Standard
OWASP HTTP Headers Cheat Sheet 2024 liệt kê 6 header phòng thủ mỗi response API/web đều nên inject. Shop API lock cả 6:
X-Frame-Options: DENY— cấm browser nhúng response vào<iframe>. Chống clickjacking: kẻ tấn công nhúng admin dashboard vào iframe trong suốt rồi lừa user click.DENYtuyệt đối;SAMEORIGINchỉ cho cùng domain (dùng khi cần preview nội bộ).X-Content-Type-Options: nosniff— cấm browser đoán MIME type khác vớiContent-Typeserver gửi. Chống MIME sniffing attack: file upload.pngchứa JS payload, browser cũ đoán là HTML execute.Strict-Transport-Security: max-age=31536000; includeSubDomains(HSTS) — buộc browser dùng HTTPS suốt 1 năm cho domain + subdomain. Chống SSL stripping attack man-in-the-middle ép HTTP. Lưu ý: chỉ inject khi serve qua HTTPS thật; localhost HTTP browser sẽ ignore.Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'none'— chống XSS injection.default-src 'self'chỉ load resource cùng origin;frame-ancestors 'none'như X-Frame-Options DENY hiện đại hơn;base-uri 'none'cấm<base>tag relocate URL. Shop API là API-only nên policy strict tối đa; nếu serve HTML admin dashboard sẽ cầnscript-src 'self' 'nonce-xxx'riêng.Referrer-Policy: strict-origin-when-cross-origin— gửi đầy đủ Referer (origin + path) cho same-origin; chỉ gửi origin (không path) cho cross-origin HTTPS; bỏ trống khi HTTPS → HTTP. Cân bằng analytics + privacy, KHÔNG leak path nhạy cảm (vd/orders/12345) sang third-party.Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()— disable hẳn 4 browser API; nếu kẻ tấn công XSS hoặc iframe nhúng cũng không request được. Shop API API-only KHÔNG dùng feature này → disable hết là an toàn nhất.
Cả 6 header lock vĩnh viễn cho mọi response Shop API (kể cả error response 4xx/5xx). Áp qua middleware global thay vì inject thủ công mỗi handler.
Custom SecurityHeadersLayer (from_fn)
Theo lock B76 (90/10 rule), middleware đơn giản không stateful dùng axum::middleware::from_fn thay vì viết custom Layer struct. Security headers chỉ inject 6 header static post-response → fit pattern from_fn hoàn hảo.
Tạo file mới crates/shop-api/src/middleware/security_headers.rs:
// File: crates/shop-api/src/middleware/security_headers.rs
use axum::{
extract::Request,
http::HeaderValue,
middleware::Next,
response::Response,
};
/// Inject 6 OWASP security header vào mọi response.
///
/// Áp dụng post-process (sau khi handler chạy xong) qua axum::middleware::from_fn.
/// Pattern B76 90/10 rule lock — stateless + static value → from_fn.
pub async fn security_headers_middleware(req: Request, next: Next) -> Response {
let mut response = next.run(req).await;
let headers = response.headers_mut();
headers.insert(
"X-Frame-Options",
HeaderValue::from_static("DENY"),
);
headers.insert(
"X-Content-Type-Options",
HeaderValue::from_static("nosniff"),
);
headers.insert(
"Strict-Transport-Security",
HeaderValue::from_static("max-age=31536000; includeSubDomains"),
);
headers.insert(
"Content-Security-Policy",
HeaderValue::from_static(
"default-src 'self'; frame-ancestors 'none'; base-uri 'none'",
),
);
headers.insert(
"Referrer-Policy",
HeaderValue::from_static("strict-origin-when-cross-origin"),
);
headers.insert(
"Permissions-Policy",
HeaderValue::from_static(
"geolocation=(), microphone=(), camera=(), payment=()",
),
);
response
}
Dùng HeaderValue::from_static cho 6 giá trị literal — không alloc heap, không có error path runtime (compiler check static byte string hợp lệ ASCII). Toàn bộ middleware là 1 hàm 30 dòng đơn giản, không struct, không bound generic phức tạp.
Cập nhật crates/shop-api/src/middleware/mod.rs re-export 2 file mới:
// File: crates/shop-api/src/middleware/mod.rs
pub mod cors;
pub mod error_enrich;
pub mod request_id;
pub mod security_headers;
pub use cors::cors_layer;
pub use error_enrich::enrich_error_response;
pub use request_id::request_id_middleware;
pub use security_headers::security_headers_middleware;
Wire Stack — Thêm 2 Middleware Mới
Cập nhật crates/shop-api/src/router.rs wire 2 layer mới. Lưu ý ordering bottom-up đã lock B76 — đọc TỪ DƯỚI LÊN để hiểu execution order:
// File: crates/shop-api/src/router.rs (snippet — phần middleware stack B77)
use axum::{middleware, routing::get, Router};
use tower_http::{
compression::{CompressionLayer, CompressionLevel},
decompression::DecompressionLayer,
};
use crate::{
middleware::{
cors_layer, enrich_error_response, request_id_middleware,
security_headers_middleware,
},
routes,
state::AppState,
};
pub fn build_router(state: AppState) -> Router {
let api_v1 = Router::new()
.merge(routes::products::routes())
.merge(routes::orders::routes())
.merge(routes::cart::routes())
.merge(routes::users::routes())
.merge(routes::brands::routes())
.merge(routes::categories::routes());
Router::new()
.merge(routes::health::routes())
.route("/metrics", get(routes::metrics::metrics))
.nest("/api/v1", api_v1)
.merge(routes::webhooks::routes())
//
// Middleware stack — đọc TỪ DƯỚI LÊN để hiểu execution order:
.layer(middleware::from_fn(enrich_error_response)) // INNER 6 — wrap error envelope
.layer(middleware::from_fn(request_id_middleware)) // INNER 5 — inject Extension<RequestId>
.layer(middleware::from_fn(security_headers_middleware)) // INNER 4 — B77 inject 6 OWASP header
.layer(cors_layer(&state.config)) // INNER 3 — B77 CORS preflight + origin check
.layer(DecompressionLayer::new().gzip(true).br(true)) // INNER 2 — decompress body
.layer( // OUTERMOST 1 — compress response
CompressionLayer::new()
.gzip(true)
.br(true)
.quality(CompressionLevel::Default),
)
.with_state(state)
}
Ordering rationale:
- CORS INNER hơn compression — CORS xử lý preflight OPTIONS sớm, không cần compress 204 No Content rỗng body.
- security_headers INNER hơn CORS — CORS check origin pass trước, security headers inject sau khi response đã build (kể cả khi CORS reject vẫn inject security header để defense in depth).
- security_headers OUTER hơn request_id — security header không phụ thuộc
RequestId, có thể inject độc lập. - enrich_error INNER nhất — wrap envelope error gần handler nhất, đảm bảo lấy đúng
RequestIdtừ Extension.
Stack giờ 6 layer global (4 cũ B50 + 2 mới B77) + 1 per-route Idempotency B66. Mỗi .layer() comment INNER/OUTERMOST rõ ràng theo lock B76 — team đọc code không phải suy luận.
Verify End-To-End
Khởi động Shop API local (mặc định APP_ENV=local → AllowOrigin::any()):
cargo run -p shop-api
# > shop-api listening addr=0.0.0.0:3000 environment=Local
Test 1 — Preflight OPTIONS request:
curl -i -X OPTIONS http://localhost:3000/api/v1/products \
-H 'Origin: https://shop.blogcode.vn' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: Content-Type, Authorization'
# Expected response:
# HTTP/1.1 200 OK
# access-control-allow-origin: *
# access-control-allow-methods: GET,POST,PATCH,DELETE,OPTIONS,HEAD
# access-control-allow-headers: content-type,authorization,idempotency-key,...
# access-control-max-age: 86400
# x-frame-options: DENY
# x-content-type-options: nosniff
# ...
Test 2 — Cross-origin POST với origin allowed (Local any):
curl -i -X POST http://localhost:3000/api/v1/products \
-H 'Origin: https://shop.blogcode.vn' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 11111111-1111-1111-1111-111111111111' \
-d '{"name":"iPhone 17","slug":"iphone-17","price":"30000000.00","stock":5,"metadata":{}}'
# Expected:
# HTTP/1.1 201 Created
# location: /api/v1/products/iphone-17
# access-control-allow-origin: *
# access-control-expose-headers: x-request-id,etag,location,x-total-count
Test 3 — Production strict (set APP_ENV=production + ALLOWED_ORIGINS):
APP_ENV=production \
ALLOWED_ORIGINS=https://shop.blogcode.vn \
cargo run -p shop-api
# Cùng request từ origin KHÔNG nằm trong list:
curl -i -X POST http://localhost:3000/api/v1/products \
-H 'Origin: https://malicious.com' \
-H 'Content-Type: application/json' -d '{}'
# Response từ server vẫn 4xx (request thật vẫn chạy):
# HTTP/1.1 422 Unprocessable Entity
# (KHÔNG có access-control-allow-origin header)
# → Browser thực thi Same-Origin Policy block JavaScript đọc response,
# curl không enforce nên vẫn thấy body. Đây là behavior đúng.
Test 4 — Verify 6 security header inject mọi response:
curl -is http://localhost:3000/api/v1/products | \
grep -iE "x-frame|x-content-type|strict-transport|content-security|referrer-policy|permissions-policy"
# Expected 6 dòng:
# x-frame-options: DENY
# x-content-type-options: nosniff
# strict-transport-security: max-age=31536000; includeSubDomains
# content-security-policy: default-src 'self'; frame-ancestors 'none'; base-uri 'none'
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: geolocation=(), microphone=(), camera=(), payment=()
Test 5 — SameSite cookie scenario (Shop API skip):
SameSite chỉ áp dụng khi server SET cookie qua header Set-Cookie. Shop API stateless JWT Bearer (B11 lock) KHÔNG set cookie → SameSite không liên quan. Nếu future thêm admin dashboard session cookie (preview G11 B106), cookie sẽ wire 3 attribute MANDATORY: HttpOnly (cấm JS đọc), Secure (chỉ HTTPS), SameSite=Strict (chỉ gửi cookie khi navigate từ cùng origin — chống CSRF triệt để).
Tổng Kết
- CORS preflight (OPTIONS) vs simple request — browser trigger preflight khi method PATCH/DELETE hoặc
Content-Type: application/jsonhoặc có custom header. tower-http::CorsLayerlock cho Shop API CORS handling — KHÔNG viết tay.- Multi-env CORS lock: Local
AllowOrigin::any(), Staging/ProductionAllowOrigin::list(env)vớiassert!fail-fast. allow_credentials(false)MANDATORY — Shop API stateless JWT Bearer (B11 continued).max_age 86400cache preflight 24h, giảm OPTIONS subsequent request.- 4 method allowed: GET, POST, PATCH, DELETE + OPTIONS + HEAD — explicit list không
Any. - 5 header allowed: Content-Type, Authorization, Idempotency-Key, X-Request-Id, If-None-Match.
- 4 header expose: X-Request-Id, ETag, Location, X-Total-Count.
- 6 OWASP security header: X-Frame-Options DENY, X-Content-Type-Options nosniff, HSTS 1 năm, CSP strict, Referrer-Policy strict-origin, Permissions-Policy disable browser features.
security_headers_middlewaretừfrom_fnpattern (B76 lock continued — stateless static value).- 6 layer stack sau B77 (4 cũ + 2 mới): compression > decompression > cors > security_headers > request_id > enrich_error > handler.
- SameSite cookie SKIP Shop API stateless JWT KHÔNG cookie — pattern chỉ áp khi future thêm session cookie (G11).
- File path lock: NEW
middleware/cors.rs+middleware/security_headers.rs; updatedmiddleware/mod.rs+router.rs+config.rs+.env.example.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- CORS preflight vs simple request — khi nào browser gửi OPTIONS? 3 điều kiện trigger preflight.
AllowOrigin::any()vsAllowOrigin::list(...)— tại sao production KHÔNG dùng any? Cho ví dụ attack scenario.allow_credentials(true)vsfalse— Shop API stateless JWT chọn false. Trade-off cookie vs token.- 6 security header OWASP — phân biệt vai trò mỗi header. Cho ví dụ CSP strict vs permissive scenario.
- SameSite cookie Strict vs Lax vs None — khi nào dùng nào? Shop API skip vì lý do gì?
Đáp án
- Simple request browser gửi thẳng không preflight, server trả response có header
Access-Control-Allow-Originrồi browser quyết định expose body hay không cho JavaScript. Điều kiện simple request (CORS-safelisted theo Fetch spec): (a) method ∈{GET, POST, HEAD}; (b) Content-Type ∈{text/plain, application/x-www-form-urlencoded, multipart/form-data}; (c) chỉ có CORS-safelisted header (Accept, Accept-Language, Content-Language, Content-Type ở 3 value trên, Range cho byte fetch). Preflight request browser gửi OPTIONS trước với 2 headerAccess-Control-Request-Method+Access-Control-Request-Headers, chờ server trả 204 hoặc 200 vớiAccess-Control-Allow-*, sau đó mới gửi request thật. 3 điều kiện trigger preflight: (a) method ∈{PATCH, PUT, DELETE, CONNECT, OPTIONS, TRACE}— vd Shop API PATCH product, DELETE cart item; (b)Content-Type: application/json(hoặc khác 3 value safelisted) — vd POST tạo product với JSON body; (c) có custom header non-safelisted — vdAuthorization: Bearer,Idempotency-Key,X-Request-Id. Shop API gần như 100% endpoint trigger preflight vì dùng JSON + Bearer auth → cấu hình CorsLayer đầy đủ là MANDATORY. Browser cache preflight theoAccess-Control-Max-Age(Shop API lock 86400 = 24h) → request kế cùng tuple (origin, method, header set) skip OPTIONS, giảm overhead xuống ~0. - Tại sao production KHÔNG dùng
AllowOrigin::any(): (a)any()trả headerAccess-Control-Allow-Origin: *cho mọi origin — bất kỳ website nào trên Internet đều fetch được API thành công, JavaScript đọc được response; (b) kết hợp với cookie session (nếu app dùng) sẽ vi phạm RFC 6454 (CORS spec cấm wildcard + credentials); (c) attack scenario CSRF-like — kẻ tấn công host sitehttps://attacker.com, dụ user (đã login Shop API) truy cập, JavaScript của attacker fetchhttps://api.shop.com/me/ordersvới CORS pass → đọc được data nhạy cảm user; (d) attack scenario data scraping — competitor scrape catalog products qua JavaScript browser (không qua server-side để né rate limit IP-based), CORS open = không có raincoat. PatternAllowOrigin::list([...])chỉ echo origin cụ thể (vdAccess-Control-Allow-Origin: https://shop.blogcode.vn) khi request Origin match list — origin khác không có header → browser block JavaScript đọc response. Lock Shop API: Local dùngany()vì dev gọi từ Postman/curl/localhost:5173 SPA dev server đa dạng port, không có user nhạy cảm thật; Staging/Production dùnglist(env_origins)+assert!fail-fast nếu empty (tránh deploy quên config → block toàn bộ frontend). Pattern decision tree: list env → MUST production, any() → CHỈ local dev. allow_credentials(true)cho phép browser gửi cookie + Authorization header + client TLS cert cross-origin. Server PHẢI echo origin cụ thể (KHÔNG được*) — RFC 6454 cấm wildcard + credentials đồng thời (tránh security leak). Frontend phải setfetch(url, {credentials: 'include'}). Use case: server-rendered web app dùng session cookie (vd admin dashboard truyền thống Rails/Django), SPA cùng tenant dùng cookie.allow_credentials(false)browser KHÔNG gửi cookie cross-origin; gửi Authorization header bình thường (header này không bị credentials flag chặn từ CORS spec, chỉ cookie/client cert bị chặn). Shop API lockfalseMANDATORY vì: (a) stateless JWT Bearer (B11 lock continued) — auth qua headerAuthorization: Bearer eyJ..., KHÔNG cookie; (b) simplify CORS config — không cần lock origin cụ thể với credentials, có thể wildcard nếu cần (Localany()không vi phạm spec); (c) scale dễ — không có server-side session store, mọi instance Shop API stateless thuần. Trade-off cookie vs token: cookie pros — auto attach mọi request không code, browser quản lý expiry,HttpOnly+SameSite=Strict+Securechống XSS + CSRF triệt để; cons — CSRF risk nếu thiếu SameSite, scale stateful (Redis session store), cross-domain phức tạp. Token pros — stateless scale tốt, cross-domain dễ (mobile app, partner API), revoke tinh tế qua jti blacklist; cons — phải code attach header mỗi request (interceptor axios/fetch wrapper), XSS đọc localStorage = leak token (nên dùngHttpOnlyrefresh token cookie + memory access token hybrid pattern G12). Shop API quyết định: API thuần token vì target client đa platform (web SPA + mobile + partner integration); admin dashboard tương lai (B106) sẽ dùng session cookie + SameSite=Strict riêng (KHÔNG share auth scheme với public API).- 6 OWASP security header phân biệt vai trò: (a)
X-Frame-Options: DENYchống clickjacking — kẻ tấn công nhúng app admin vào<iframe>trong suốt, lừa user click vào button "Xác nhận" tưởng click vào game/promo;DENYcấm browser render trong frame bất kỳ,SAMEORIGINchỉ cho frame cùng domain; CSPframe-ancestors 'none'là phiên bản hiện đại thay thế. (b)X-Content-Type-Options: nosniffchống MIME sniffing — browser cũ đoán type từ nội dung byte đầu, vd file upload.pngchứa<script>tag browser cũ render HTML execute JS;nosniffép browser tuân thủContent-Typeserver. (c)Strict-Transport-Security: max-age=31536000; includeSubDomainschống SSL stripping — MITM ép user xuống HTTP, browser cache HSTS 1 năm → tự upgrade HTTP request thành HTTPS, không gửi clear-text bao giờ;includeSubDomainsapply cho mọi*.blogcode.vn(cẩn thận: nếu subdomain nào không có HTTPS sẽ bị block); chỉ inject khi serve HTTPS thật, localhost HTTP browser ignore. (d)Content-Security-Policychống XSS injection — limit nguồn resource browser load; Shop API API-only strict tối đadefault-src 'self'+frame-ancestors 'none'+base-uri 'none'; strict scenario: API-only, không serve HTML — strict triệt để như Shop API là an toàn nhất, kẻ XSS không inject được external script; permissive scenario: web app cần CDN script (Tailwind, Google Fonts, Google Analytics) — phải mởscript-src 'self' https://cdn.tailwindcss.com https://www.googletagmanager.com 'nonce-xxx', mỗi request server generate nonce ngẫu nhiên inject vào<script nonce="xxx">inline tránh XSS inject script không có nonce. (e)Referrer-Policy: strict-origin-when-cross-originchống information leak — same-origin gửi full URL Referer, cross-origin HTTPS chỉ gửi origin (không path), HTTPS → HTTP bỏ trống; chuẩn 2024 balance analytics + privacy, KHÔNG leak path nhạy cảm như/orders/12345/payment-statuscho third-party (vd link click sang social media). (f)Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()chống iframe escalation — nếu kẻ tấn công nhúng app vào iframe (dù X-Frame-Options đã block) hoặc XSS thành công, Permissions-Policy disable hẳn 4 API ở browser level → request không trigger được prompt user. Shop API API-only KHÔNG dùng feature này → disable tất cả là default tốt nhất. - SameSite cookie 3 mode: (a)
SameSite=Strict— cookie CHỈ gửi khi navigate từ cùng origin (cùng scheme + host + port); link từ Google search click vào Shop API sẽ KHÔNG gửi cookie → user phải login lại nếu vào qua external link; bảo vệ CSRF triệt để vì kẻ tấn công host site khác không trigger gửi cookie được. (b)SameSite=Lax(default 2024+ browser modern) — gửi cookie khi navigate top-level GET (link click, type URL, redirect), KHÔNG gửi khi POST/PATCH/DELETE từ form/JavaScript cross-origin hoặc iframe/img/script subresource; balance bảo mật + UX (user click link từ Google vẫn được login). (c)SameSite=None+Secure(MANDATORY) — gửi cookie cross-origin mọi context kể cả iframe + JS fetch; use case duy nhất: 3rd-party widget (Stripe Checkout, OAuth popup, embed video) cần state cross-domain; nguy hiểm: CSRF protection mất gần như hoàn toàn, phải bù bằng CSRF token double-submit. Decision tree: app standalone không có 3rd-party embed → Strict (admin dashboard internal); app có deep link Google/social cần UX trơn tru → Lax (default modern); app embed 3rd-party widget bắt buộc → None + Secure + CSRF token. Shop API skip SameSite vì 4 lý do lock vĩnh viễn: (a) stateless JWT Bearer (B11 + B112) — auth qua header KHÔNG cookie, SameSite không có chỗ áp dụng; (b) API-only không serve HTML — không có top-level navigation form submit cần cookie; (c) scale stateless không có server session store Redis cross-instance share; (d) client đa dạng (web + mobile + partner integration) — mobile native không có cookie jar browser, partner backend-to-backend không có browser context → token là chuẩn cross-platform duy nhất. Future admin dashboard B106 nếu thêm sẽ wire session cookie riêng:HttpOnly + Secure + SameSite=Strict+ CSRF token double-submit (B107) — KHÔNG share auth scheme với public API.
Bài Tiếp Theo
Bài 78: Rate Limiting — Per-IP + Per-User — implement rate limiting với governor crate, in-memory token bucket, áp Shop API per-IP cho /auth + per-user cho /orders, Redis-backed cho production (preview G15), response 429 Too Many Requests + header Retry-After + X-RateLimit-Remaining.
