Ứng dụng của bạn giờ đã có người dùng thật và dữ liệu thật. Bài này bao gồm ba nền tảng mà mọi shipper phải nắm — xác thực (authentication), lưu mật khẩu, và SQL injection — cùng cách chúng xếp thành phòng thủ nhiều lớp.
← / → để chuyển slide · chuyển EN/VI bên dưới · tổng quan trước, code khi hữu ích
Khi bạn lưu người dùng, đơn hàng và tiền, ba việc sau thành trách nhiệm của bạn.
Mỗi request phải tự giới thiệu trước khi chạm vào bất cứ thứ gì. Sessions, tokens, API keys — xác thực trả lời 'ai đây?'
Mật khẩu phải được lưu theo cách mà ngay cả bạn — và cả CSDL của bạn — cũng không đọc lại được.
Input của attacker phải luôn là dữ liệu. Nếu nó thành một phần SQL, một ô nhập liệu có thể đọc hoặc xoá mọi thứ.
Bảo mật là các lớp, không phải một bức tường. Mỗi lớp chặn thứ các lớp kia bỏ sót — nắm chắc nền tảng là bạn đã vượt trước.
Sessions, JWT, API keys, OAuth — bốn cách một request chứng minh nó là ai, và khi nào dùng từng cái.
Chứng minh danh tính: đăng nhập bằng mật khẩu, token, hay key. Server trả lời: 'Đúng, bạn là Alice.'
Câu hỏi: AI đang hỏi?
Quyết định quyền: Alice xem được đơn của mình, nhưng không xoá được đơn của Bob. Kiểm tra sau khi đã biết danh tính.
Câu hỏi: bạn ĐƯỢC làm gì?
Internet đầy rẫy script cố hành động thay bất kỳ ai.
Không có auth, một script có thể gọi API của bạn với bất kỳ user id nào. 'Tôi là user 42' không phải bằng chứng — chỉ là lời tuyên bố.
Khi người dùng có tài khoản, chiếm một tài khoản là chiếm dữ liệu, đơn hàng, thậm chí tiền của họ. Attacker tấn công đăng nhập trước tiên.
HTTP không có trí nhớ và không có khuôn mặt. Danh tính duy nhất là thứ bạn kiểm tra được: session, token, hay key.
Mọi thứ khác — quyền, rate limit, dữ liệu theo user — chỉ có ý nghĩa sau khi đã xác định được 'ai'.
POST /login kèm email + mật khẩu. Server băm (hash) mật khẩu và so với hash đã lưu (chi tiết ở Phần 2).
Một id ngẫu nhiên như sid=abc123 được lưu phía server, gắn với 'alice đang đăng nhập'. Server giữ bộ nhớ.
Response mang Set-Cookie: sid=abc123. Browser tự động lưu lại.
Từ giờ, browser gắn Cookie: sid=abc123 vào mọi request tới site của bạn.
Mỗi request: kiểm tra sid, tìm session, biết ai đang hỏi. Đăng xuất = xoá dòng session.
Cookie là tấm vé; session store là danh sách khách.
Token không có trạng thái do client mang theo — không cần session store.
Sau đăng nhập, server ký một khối JSON nhỏ (id user + thời hạn) bằng một bí mật. Kết quả — JWT — được gửi cho client, client gửi nó trong header Authorization.
Server không lưu gì cho mỗi user — stateless, scale tốt qua nhiều server, hợp lý với mobile app và API.
Không lưu nghĩa là không thu hồi được: JWT bị đánh cắp vẫn hợp lệ đến khi hết hạn. Giữ thời hạn ngắn; với web, sessions thường đơn giản hơn.
API do app dùng, microservices gọi nhau, mobile. Với site browser truyền thống, hãy bắt đầu bằng sessions.
Chuỗi ngẫu nhiên dài mà dịch vụ gửi kèm mọi request: X-Api-Key: sk_live_9f2c.... Không có form đăng nhập — key CHÍNH là danh tính.
Máy móc không có mật khẩu. Stripe, GitHub, API thời tiết — một key cho mỗi dịch vụ, dễ tạo, xoay vòng, hoặc tắt.
Key rất mạnh — coi chúng như mật khẩu: không để trong code bị commit, không để trong browser, quyền tối thiểu cho mỗi key.
App của bạn hỏi Google: 'người dùng này nói họ là alice@gmail.com — xác nhận nhé?' Google cho họ đăng nhập và trả lời có. App của bạn không bao giờ thấy mật khẩu của họ.
OAuth còn cho phép user đưa bạn một vé hạn chế: 'cho app này đọc lịch của tôi, không gì khác'. Scope là kích thước của tấm vé.
Nút 'Tiếp tục với Google/Apple', bot CI đẩy lên GitHub, Google Drive đồng bộ. Cùng một giao thức, khác scope.
Khi danh tính đã có sẵn ở nơi khác (Google), hoặc khi bạn muốn chia sẻ tài nguyên với quyền hạn chế thay vì chia sẻ mật khẩu.
Sessions cho đăng nhập web, JWT cho API stateless, key cho máy móc, OAuth khi người khác sở hữu danh tính.
| Phương pháp | Danh tính nằm ở | Trạng thái lưu ở | Dùng cho |
|---|---|---|---|
| Session + cookie | cookie (sid) | server (session store) | đăng nhập browser truyền thống |
| JWT | token (Authorization) | không ở đâu — nó đã được ký | mobile, API, microservices |
| API key | key (X-Api-Key) | không ở đâu — chỉ để đối chiếu | dịch vụ gọi dịch vụ |
| OAuth | người dùng, ở IdP | ở nhà cung cấp danh tính | 'đăng nhập bằng Google', ủy quyền |
Endpoint không xác thực là vụ lộ dữ liệu có thêm bước. Mọi endpoint chạm vào dữ liệu đều cần kiểm tra danh tính + quyền.
Browser có thể sửa mọi thứ. 'Tôi là user 42' từ client là gợi ý, không phải sự thật — danh tính đến từ session/token do bạn cấp.
Sessions và JWT cần thời hạn. Token hợp lệ mãi mà bị lộ trong log là cánh cửa mở — vĩnh viễn.
Cookie session nên có HttpOnly (JS không đọc được), Secure (chỉ HTTPS), SameSite (gây khó cho CSRF).
Hash, salt, bcrypt: đăng nhập hoạt động thế nào khi ngay cả bạn cũng không đọc lại được mật khẩu.
Không plaintext, không 'đã mã hoá', không đảo ngược được. Tuyệt đối không.
SQL injection, backup cấu hình sai, laptop bị đánh cắp, export cẩu thả. Hãy giả định cột mật khẩu SẼ bị đọc bởi người không được phép.
Một dòng bị lộ cho attacker mật khẩu thật — dùng được trên site bạn và mọi site khác mà họ dùng chung mật khẩu đó.
Nhân viên support không được phép trả lời 'mật khẩu bạn là gì?'. Hệ thống không nên có khả năng đó — đó mới là mục đích.
Hãy lưu hash: giá trị tính từ mật khẩu nhưng không thể đảo ngược. Mật khẩu thật không bao giờ chạm vào CSDL.
Hàm biến input thành dấu vân tay cỡ cố định: input giống nhau luôn ra hash giống nhau; đổi 1 ký tự ra kết quả hoàn toàn khác.
hash('hunter2') → 9f86d081884c7d659a2feaa0c55ad015
Tính hash nhanh tức thì. Đảo ngược thì không làm được — phải thử mật khẩu cho tới khi có cái khớp. Đó là lý do hash an toàn để lưu.
Hash bị đánh cắp ≠ mật khẩu bị đánh cắp. Đó là ổ khoá không có chìa.
Không có salt, 'password123' của 10.000 user sẽ ra cùng một chuỗi 10.000 lần — attacker bẻ được một lần là đăng nhập thay tất cả.
Mỗi user có một salt ngẫu nhiên. Giá trị lưu là hash(password + salt). Hai user cùng mật khẩu giờ lưu hai chuỗi khác nhau.
Các bảng bẻ sẵn giả định không có salt. Có salt riêng cho mỗi user, mỗi mật khẩu trong vụ lộ đều cần một cuộc brute-force riêng.
bcrypt/argon2 tự động sinh và nhúng salt vào chuỗi lưu — thư viện lo hết; bạn chỉ gọi hashpw().
MD5/SHA-256 được tạo để nhanh — GPU thử hàng tỷ lần mỗi giây. Bạn cần ngược lại: hash chậm cho tất cả, nhất là cho họ.
bcrypt/argon2 có mức 'cost'; mỗi mức tăng làm hash chậm gấp đôi. Đăng nhập của bạn vẫn tức thì — GPU của attacker vừa nghèo đi.
Bạn không tự viết: bcrypt/argon2 lo salt, cost, và so sánh an toàn. Chọn một — cả hai đều là câu trả lời đúng.
Việc đúng thì dễ với bạn (chỉ cần một function call), việc sai thì đắt với họ (hàng tỷ lần thử chậm chạp).
import bcrypt
def register(email, password):
# bcrypt generates a fresh random salt for this user
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
db.execute("INSERT INTO users(email, pw) VALUES(?, ?)", [email, hashed])
def login(email, password):
row = db.execute("SELECT pw FROM users WHERE email = ?", [email]).fetchone()
if row and bcrypt.checkpw(password.encode(), row[0]):
return create_session(email)
return None
Mật khẩu chỉ tồn tại trong bộ nhớ thoáng chốc. Thứ được lưu — và thứ vụ lộ để lộ — là hash.
Cách phổ biến nhất khiến web app bị phá — và cũng là cách dễ phòng nhất.
App của bạn ghép SQL từ input của user. CSDL không biết phần nào là của bạn.
Nhiệm vụ của CSDL là thực thi đúng chuỗi SQL nó nhận. Nó không biết một nửa trong đó đến từ ô nhập liệu.
App của bạn ghép câu truy vấn: mẫu cố định + giá trị của user. Nếu ghép bằng nối chuỗi, văn bản của user có thể thay đổi cả câu.
Nhập văn bản 'đóng' giá trị và chèn mệnh đề mới: ' OR 1=1 -- biến câu tra cứu thành 'trả về tất cả'.
Cũ (1998), ở khắp nơi (mọi app ghép SQL), và một dòng có thể đổ hoặc xoá cả CSDL. Nhưng cách sửa chỉ là một thói quen.
def login(email, password):
# email & password come from a text box — untrusted input
q = "SELECT * FROM users WHERE email='" + email + "'"
q = q + " AND password='" + password + "'"
row = db.execute(q).fetchone() # the DB runs whatever q says
if row:
return "logged in"
return None
Ô email là dữ liệu — nhưng Python ghép nó vào câu SQL trước khi CSDL kịp nhìn thấy.
-- the user types into the email field:
' OR 1=1 --
-- your string-building code produces:
SELECT * FROM users WHERE email='' OR 1=1 -- '
AND password='whatever'
-- 1=1 is always true, and -- comments out the rest:
-- the password check is gone. every row comes back.
Payload chỉ là một chuỗi đối với bạn — nhưng với CSDL nó là chỉ thị: bỏ qua bước kiểm tra mật khẩu.
def login(email, password):
# parameters: the query and the data travel separately
row = db.execute(
"SELECT * FROM users WHERE email = ? AND password = ?",
[email, password], # data stays data — never parsed as SQL
).fetchone()
if row and bcrypt.checkpw(password.encode(), row[0]):
return create_session(email)
return None
CSDL nhận câu truy vấn và dữ liệu như hai thứ tách biệt. Dữ liệu không bao giờ có thể thay đổi câu truy vấn.
Mẫu truy vấn do code của bạn quyết định, trước khi có input. Giá trị của user được gửi dưới dạng tham số — CSDL giữ chúng như dữ liệu.
Nhập ' OR 1=1 -- vào tham số thì vô dụng: nó chỉ là một giá trị lạ mà không user nào có.
Không bao giờ ghép SQL bằng cách nối hay chèn input của user. Không f-string, không +, không .format() — chỉ dùng placeholder.
Nếu bạn bắt gặp mình nối input của user vào mệnh đề WHERE, hãy dừng lại. Đó chính là lỗi.
Coi mọi input của người dùng là thù địch. Nó có thể là một cái tên, một bình luận — hoặc một cuộc tấn công. Code của bạn quyết định nó là dữ liệu hay lệnh.
HTTPS, quản lý bí mật, quyền tối thiểu — ba dòng nữa khép lại hầu hết các lỗ hổng.
Mã hoá mọi thứ giữa browser và server: form đăng nhập, cookie, mọi request. Kẻ nghe lén trên mạng chỉ thấy nhiễu.
Wi-Fi quán cà phê, nhà mạng nghe lén, man-in-the-middle. Không có nó, mật khẩu 'đã băm' vẫn bay qua mạng dạng plaintext.
Chỉ HTTPS, ở khắp nơi — kể cả endpoint API và redirect. Nhà cung cấp hiện đại cấp chứng chỉ miễn phí (Let's Encrypt).
# .env — on the server only, NEVER committed to git
DATABASE_URL=sqlite:////data/shop.db
SESSION_SECRET=9f2c41d8e7b3...8812
SMTP_PASSWORD=correct-horse-battery
# .gitignore
.env
Code đi vào git (và có thể vào fork, log CI, hoặc đến tay người tuyển dụng). Bí mật ở lại server. Một dòng trong .gitignore giữ chúng ở ngoài.
Mọi user, key, và tài khoản dịch vụ chỉ nhận đúng quyền cần — không thêm. Session của người bình luận blog không mở được trang quản trị.
Nếu một thông tin đăng nhập bị lộ, phạm vi ảnh hưởng chính là quyền của nó. Quyền tối thiểu biến 'mất tất cả' thành 'mất một thứ nhỏ'.
CSRF (request giả mạo từ site khác) và XSS (script chèn vào trang) là những chương tiếp theo — cookie SameSite và escape output là lớp phòng thủ đầu tiên của chúng.
Không lớp nào hoàn hảo. Đặt chung lại, chúng làm cho tấn công trở nên đắt đỏ ở mỗi bước.
Sáu thói quen đưa bạn vượt trước hầu hết các vụ lộ dữ liệu thực tế.
bcrypt hoặc argon2, salt tự động. Plaintext không bao giờ chạm vào CSDL.
Chỉ dùng placeholder. Không nối chuỗi, không f-string, không ngoại lệ.
Auth ở mọi endpoint, hết hạn ở mọi nơi, cookie HttpOnly/Secure/SameSite.
Mọi trang, mọi API, mọi redirect. Không ngoại lệ.
Biến môi trường trên server, .env trong .gitignore, xoay vòng mọi thứ từng bị commit.
Quyền tối thiểu cho mỗi user, key, và tài khoản dịch vụ.
Bảo mật không phải tính năng thêm vào cuối — nó là một tập thói quen xây từ đầu: băm mật khẩu, tham số hoá query, kiểm chứng mọi request.
Tiếp theo: triển khai, CI/CD và giám sát — dặm cuối của From Coder to Shipper.