→ From Coder to Shipper
From Coder to Shipper · Bảo mật

Bảo mật phía backend

Ứ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

Toàn cảnh

App của bạn giờ có người dùng thật — và mục tiêu thật

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.

(a) Biết ai đang hỏi

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?'

(b) Bảo vệ bí mật

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.

(c) Ngăn lệnh bị chèn

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ứ.

Phòng thủ nhiều lớp

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.

01
Phần 1

Xác thực — biết AI đang hỏi

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.

Phân biệt cốt lõi

Authentication (xác thực) vs authorization (ủy quyền)

Authentication — bạn là ai?

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?

Authorization — bạn được làm gì?

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ì?

Mối đe doạ

Vì sao cần xác thực

Internet đầy rẫy script cố hành động thay bất kỳ ai.

Ai cũng có thể tuyên bố

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ố.

Tài khoản = danh tính

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.

Server không thấy được con người

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.

Auth là cánh cửa trước

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'.

Phương pháp 1 · phổ biến nhất

Đăng nhập cookie + session, từng bước

  1. 1

    Người dùng gửi form

    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).

  2. 2

    Server tạo session

    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ớ.

  3. 3

    Cookie được gửi lại

    Response mang Set-Cookie: sid=abc123. Browser tự động lưu lại.

  4. 4

    Browser gửi kèm mọi request

    Từ giờ, browser gắn Cookie: sid=abc123 vào mọi request tới site của bạn.

  5. 5

    Server tra cứu

    Mỗi request: kiểm tra sid, tìm session, biết ai đang hỏi. Đăng xuất = xoá dòng session.

Cơ chế

Mọi request sau khi đăng nhập

Cookie là tấm vé; session store là danh sách khách.

một lần, khi đăng nhập
Browsernhập email + mật khẩu
→① POST /login
Server của bạnbăm mật khẩu → khớp
→② Set-Cookie:
sid=abc123
Session storesid=abc123 → "alice"
mọi request sau đó
BrowserCookie: sid=abc123
→③ gửi kèm tự động
Server của bạntra sid → 'alice'
→④ đã biết danh tính
Trang được bảo vệdữ liệu của cô ấy, hành động của cô ấy
Phương pháp 2

JWT — server quên, token nhớ

Token không có trạng thái do client mang theo — không cần session store.

Nó là gì

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.

Vì sao dùng

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.

Đánh đổi

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.

Khi nào nên dùng

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.

Phương pháp 3

API keys — cho dịch vụ, không cho con người

Nó là gì

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.

Vì sao dùng

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.

Quy tắc

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.

Phương pháp 4

OAuth — 'đăng nhập bằng Google' và truy cập theo ủy quyền

Ý tưởng

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ọ.

Truy cập theo ủy quyền

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ơi bạn đã thấy

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 nào dùng

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.

Chọn công cụ

Phương pháp auth nào, khi nào

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ápDanh tính nằm ởTrạng thái lưu ởDùng cho
Session + cookiecookie (sid)server (session store)đăng nhập browser truyền thống
JWTtoken (Authorization)không ở đâu — nó đã được kýmobile, API, microservices
API keykey (X-Api-Key)không ở đâu — chỉ để đối chiếudịch vụ gọi dịch vụ
OAuthngười dùng, ở IdPở nhà cung cấp danh tính'đăng nhập bằng Google', ủy quyền
Sai lầm phổ biến

Sai lầm auth khiến bạn bị hack

'Đây chỉ là API thôi'

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.

Tin tưởng client

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.

Token không bao giờ hết hạn

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 thiếu cờ

Cookie session nên có HttpOnly (JS không đọc được), Secure (chỉ HTTPS), SameSite (gây khó cho CSRF).

02
Phần 2

Mật khẩu — đừng bao giờ lưu

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.

Quy tắc

Đừng bao giờ lưu mật khẩu

Không plaintext, không 'đã mã hoá', không đảo ngược được. Tuyệt đối không.

CSDL sẽ bị lộ

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.

Plaintext = chiếm quyền ngay

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 đó.

Ngay cả bạn cũng không nên đọc

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 một dấu vân tay một chiều

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.

Cơ chế

Hashing — dấu vân tay một chiều

Hash là gì

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

Chỉ một chiều

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.

Một nguyên liệu nữa

Salt — một chút muối ngẫu nhiên cho mỗi user

Vấn đề nó giải quyết

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ả.

Salt hoạt động thế nào

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.

Rainbow table hết thời

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.

Bạn không bao giờ thấy nó

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().

Dùng hash nào

bcrypt / argon2 — chậm là cố tình

Hash nhanh là lợi cho attacker

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ọ.

Chậm theo núm vặn

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.

Cứ dùng thư viện

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.

Cả thiết kế trong một dò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).

Cơ chế, trong code

Đăng ký & đăng nhập bằng hash có salt

auth.py
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.

Phần thưởng

Vụ lộ dữ liệu thực sự để lộ gì

ops@vps — zsh
$ cat users_leak.csv # stolen database, 2026-05-12
1,alice@example.com,$2b$12$N9qo8uLOickgx2ZMRZoMyeIj4ZVxK...
2,bob@example.com,$2b$12$Xy3Qm...
3,carol@example.com,$2b$12$...
# attacker's verdict:
no plaintext → no instant logins; each hash needs a slow, loud crack
# with plaintext, this file would already be a login for everyone
03
Phần 3

SQL injection — khi dữ liệu thành code

Cách phổ biến nhất khiến web app bị phá — và cũng là cách dễ phòng nhất.

Cơ chế

SQL injection là gì

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.

SQL là dữ liệu với CSDL

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.

Bạn là người ghép

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.

Mẹo của attacker

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ả'.

Vì sao nó kinh điển

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.

Lỗ hổng

Đăng nhập có lỗ hổng

login_vulnerable.py
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.

Cú tấn công

Giờ nhập vào ô email dòng này

what-the-database-receives.sql
-- 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.

Hậu quả

Attacker nhận được gì

ops@vps — zsh
$ sqlite3 shop.db "SELECT * FROM users WHERE email='' OR 1=1 -- "
1 | alice@example.com | $2b$12$N9qo8u...
2 | bob@example.com | $2b$12$Xy3Qm...
3 | carol@example.com | $2b$12$...
# the whole user table — every account can now be hijacked
# with a bit more work: DELETE, UPDATE, or exfiltrate anything
Cách sửa

Query tham số hoá — dữ liệu vẫn là dữ liệu

login_fixed.py
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.

Vì sao nó hiệu quả

Nguyên tắc đằng sau cách sửa

Hai kênh, một ranh giới

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ó.

Thói quen

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.
Thói quen duy nhất ngăn SQL injection
04
Phần 4

Những lớp còn lại

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.

Lớp 1

HTTPS ở khắp nơi — bạn đã có rồi

Nó làm gì

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.

Mối đe doạ nó chặn

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.

Tiêu chuẩn

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).

Lớp 2

Bí mật trong biến môi trường, không phải trong code

.env
# .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.

Lớp 3

Quyền tối thiểu — ai cũng chỉ nhận mức tối thiểu

Ý tưởng

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ị.

Vì sao quan trọng

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ỏ'.

Và hai thứ bạn sẽ gặp sau

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.

Toàn cảnh

Phòng thủ nhiều lớp — toàn bộ stack

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.

0
HTTPS (TLS)đường truyền giữa browser và server
nghe lén, sửa dữ liệu khi truyền
1
Xác thựcsession · JWT · API key · OAuth
'tôi là user 42' — danh tính chưa được chứng minh
2
Hash mật khẩu có saltbcrypt · argon2
CSDL bị đánh cắp biến thành các tài khoản đăng nhập
3
Query tham số hoádữ liệu không bao giờ là SQL
SQL injection — đọc hoặc xoá mọi thứ
4
Bí mật trong env + quyền tối thiểuquyền tối thiểu cho mỗi thông tin đăng nhập
một thông tin đăng nhập bị lộ mở toang cả hệ thống
Checklist của shipper

Làm đúng những nền tảng này

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ế.

Băm mật khẩu

bcrypt hoặc argon2, salt tự động. Plaintext không bao giờ chạm vào CSDL.

Tham số hoá mọi query

Chỉ dùng placeholder. Không nối chuỗi, không f-string, không ngoại lệ.

Sessions hoặc tokens — đúng cách

Auth ở mọi endpoint, hết hạn ở mọi nơi, cookie HttpOnly/Secure/SameSite.

Chỉ HTTPS

Mọi trang, mọi API, mọi redirect. Không ngoại lệ.

Bí mật khỏi git

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

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.
From Coder to Shipper

Giờ đi ship — với những cánh cửa có khoá

Tiếp theo: triển khai, CI/CD và giám sát — dặm cuối của From Coder to Shipper.