Nếu không có 4 lớp bảo mật API này, hacker sẽ dễ dàng xâm nhập trái phép và chiếm quyền điều khiển toàn bộ hệ thống của bạn. Thiệt hại có thể khiến công ty phá sản ngay lập tức chỉ sau một đêm.
Trong bài viết này, mình không chỉ hướng dẫn bạn xây dựng 4 lớp bảo mật API quan trọng mà bất kỳ hệ thống backend nào cũng phải có, mà còn sẽ demo thực tế cách hacker khai thác từng lỗ hỏng như thế nào.
Đặc biệt, đến cuối bài viết, bạn sẽ có trong tay bộ tư duy bảo mật mà chỉ những senior dev giàu kinh nghiệm mới nắm được, đây cũng là mục tiêu cốt lõi mà Khóa Học Full Stack 1 Kèm 1 của LetDiv hướng tới.
OK, giờ thì chúng ta bắt đầu vào nội dung chính thôi!
1. 4 lớp bảo mật API không thể thiếu
Bạn có thể tải về toàn bộ mã nguồn ví dụ trên GitHub.
1.1 Input Validation (Xác thực dữ liệu)
Đầu tiên là Input Validation, đây là lớp bảo mật giúp đảm bảo mọi dữ liệu mà người dùng gửi lên đều hợp lệ.
Một sai lầm rất phổ biến mà nhiều lập trình viên thường mắc phải, là chỉ tiến hành validate ở frontend nhưng không validate ở API.
Đây là một lỗ hổng cực kỳ nguy hiểm, vì hacker có thể dễ dàng sử dụng các công cụ như Postman để gửi request trực tiếp đến API, qua mặt toàn bộ lớp kiểm tra dữ liệu ở frontend.
Bạn hãy xem 1 ví dụ cụ thể như sau. Giả sử mình có một form đăng ký người dùng bao gồm 3 ô nhập liệu name, email và password.

Phía frontend đã có cơ chế xác thực, yêu cầu người dùng phải nhập đủ thông tin.

Tuy nhiên, mình có thể sử dụng Postman để gọi thẳng vào API đăng ký và cố tình không cung cấp đầy đủ dữ liệu.
Như bạn thấy, hệ thống backend lại không báo lỗi gì cả, và tiến hành lưu vào database như thông thường, dẫn đến làm sai lệch dữ liệu cho cả hệ thống.

Để hiểu rõ nguyên nhân, chúng ta hãy cùng xem đoạn code backend Node.js ví dụ sau đây:

Như bạn thấy, API này nhận dữ liệu rồi lưu thẳng vào database, hoàn toàn bỏ qua bước kiểm tra tính hợp lệ của dữ liệu.
Okay, giờ hãy cùng nhau khắc phục lỗ hỏng này.
Mình sẽ thêm vào một lớp bảo mật xác thực dữ liệu bằng cách sử dụng một thư viện rất phổ biến là express-validator
.
Mình sẽ định nghĩa một vài quy tắc cho email và password. Để cho đơn giản, trước mắt, mình chỉ cần yêu cầu chúng không được để trống.
// Định nghĩa các quy tắc xác thực dữ liệu const registerValidationRules = [ body('name').escape().not().isEmpty().withMessage('Tên không được để trống.'), body('email').not().isEmpty().withMessage('Email không được để trống.'), body('password').not().isEmpty().withMessage('Password không được để trống.'), ]; // API Endpoint để đăng ký người dùng app.post('/register', apiLimiter, registerValidationRules, async (req, res) => { // Kiểm tra kết quả của validation const errors = validationResult(req); if (!errors.isEmpty()) { // Nếu có lỗi, trả về status 400 cùng với danh sách lỗi return res.status(400).json({ errors: errors.array() }); } try { const { name, email, password } = req.body; // Kiểm tra xem email đã tồn tại trong database chưa const existingUser = await User.findOne({ where: { email } }); if (existingUser) { return res.status(400).json({ errors: [{ msg: 'Email đã tồn tại.' }] }); } // Tạo người dùng mới trong database const newUser = await User.create({ name, email, password }); console.log('✅ Người dùng mới đã được tạo:', newUser.toJSON()); // Trả về thông báo thành công res.status(201).json({ message: 'Đăng ký thành công!' }); } catch (error) { console.error('❌ Lỗi khi đăng ký người dùng:', error); res.status(500).json({ errors: [{ msg: 'Lỗi server, không thể đăng ký người dùng.' }] }); } });
Logic ở đây rất đơn giản: nếu dữ liệu gửi lên vi phạm quy tắc, API sẽ trả về lỗi ngay lập tức. Còn nếu mọi thứ đều hợp lệ, thì dữ liệu mới đi tiếp vào phần xử lý chính.
Giờ mình kiểm tra lại với Postman. Như bạn thấy, API đã trả về lỗi chính xác như chúng ta mong đợi. Dữ liệu không hợp lệ đã bị chặn lại!

Sức mạnh của express-validator
không chỉ dừng lại ở đó, nó còn cung cấp rất nhiều hàm tiện ích khác. Ví dụ như kiểm tra định dạng email, kiểm tra độ dài chuỗi, v.v.
Đặc biệt hơn nữa là tính năng sanitize – tức là “làm sạch” dữ liệu. Tính năng này cực kỳ quan trọng để ngăn chặn các cuộc tấn công XSS, khi mà hacker cố tình chèn mã độc vào các ô nhập liệu.
Để thực hiện tính năng sanitize, cách làm rất đơn giản. Ví dụ với trường name, mình chỉ cần gọi thêm phương thức .escape()
là xong!

Tóm lại, lớp bảo mật Input Validation rất quan trọng và có thể giúp bạn tránh được phần lớn các cuộc tấn công phổ biến từ hacker.
Hãy luôn ghi nhớ quy tắc vàng sau “Đừng bao giờ tin tưởng dữ liệu từ frontend”. Luôn luôn phải xác thực và làm sạch dữ liệu tại backend.
Đến đây, thì mình có một câu hỏi khá thú vị dành cho bạn: Nếu backend đã có lớp bảo mật xác thực rồi, vậy thì việc validate tại frontend có dư thừa hay ko?
Nếu dư thừa thì tại sao hầu hết các ứng dụng vẫn đầu tư công sức để làm validate cả frontend lẫn backend?
Bạn nghĩ sao về vấn đề này? Hãy chia sẻ quan điểm của bạn ở phần bình luận nhé!
1.2 Rate Limiting (Giới hạn tần suất truy cập)
API đăng nhập là một trong những mục tiêu bị hacker nhắm đến nhiều nhất. Hacker có thể dễ dàng viết một đoạn script để tự động gửi hàng triệu yêu cầu đến API đăng nhập, thử vô số các email và mật khẩu ngẫu nhiên nhằm chiếm quyền truy cập trái phép. Kiểu tấn công này được gọi là kiểu “tấn công vét cạn” (brute-force attack).
Không chỉ vậy, hacker còn có thể tấn công đồng loạt nhiều API khác, dẫn đến máy chủ của bạn bị quá tải, và người dùng không thể sử dụng được dịch vụ. Đây là một tình huống xảy ra thường xuyên hơn bạn nghĩ.
Để giải quyết vấn đề này, có một giải pháp đơn giản nhưng vô cùng hiệu quả, đó là sử dụng một lớp bảo mật có tên Rate Limiting (Giới hạn tần suất truy cập). Chức năng chính của lớp bảo mật này là ngăn chặn người dùng gửi quá nhiều yêu cầu so với mức cho phép mà bạn đã quy định, từ đó đảm bảo máy chủ không bị quá tải.
Vật Rate Limiting hoạt động như thế nào?
Chúng ta hãy cùng xem một ví dụ thực tế. Đây là một đoạn script mô phỏng việc gọi API đăng nhập 10,000 lần với các thông tin email và password ngẫu nhiên. Khi script này chạy, máy chủ của mình gần như chắc chắn sẽ bị quá tải ngay lập tức.
#!/bin/bash # URL của API đăng nhập mục tiêu LOGIN_URL="http://localhost:3000/login" # Tên người dùng/email không đổi EMAIL="[email protected]" # Vòng lặp để thử 10000 mật khẩu giả định for i in {1..10000} do # Tạo mật khẩu giả định cho lần thử thứ i PASSWORD="password$i" echo "Đang thử lần thứ $i với mật khẩu: $PASSWORD" # Sử dụng curl để gửi yêu cầu POST với định dạng JSON response=$(curl -s -o /dev/null -w "%{http_code}" -X POST $LOGIN_URL \ -H 'Content-Type: application/json' \ -d "{\"email\":\"$EMAIL\", \"password\":\"$PASSWORD\"}") # Kiểm tra mã trạng thái HTTP trả về if [ "$response" -eq 200 ]; then echo "----------------------------------------" echo "THÀNH CÔNG! Mật khẩu là: $PASSWORD" echo "----------------------------------------" exit 0 # Thoát script ngay khi tìm thấy mật khẩu elif [ "$response" -eq 429 ]; then echo "!!! BỊ CHẶN! API đã áp dụng Rate Limiting (Mã trạng thái: $response). Các yêu cầu tiếp theo từ IP này sẽ bị chặn." else echo "Thất bại với mã trạng thái: $response" fi # Thêm một khoảng nghỉ nhỏ để dễ quan sát sleep 0.5 done echo "Demo hoàn tất. Đã cho thấy cơ chế Rate Limiting hoạt động."
Tuy nhiên, nếu mình trang bị Rate Limiting cho API đăng nhập, ví dụ bằng cách sử dụng thư viện express-rate-limit
, mình có thể thiết lập quy tắc đơn giản như sau:

Đoạn code trên có ý nghĩa là mỗi địa chỉ IP chỉ được phép thực hiện tối đa 5 request trong vòng 15 phút. Nếu có bất IP nào vi phạm quy tắc này, hệ thống sẽ tự động chặn các request tiếp theo từ IP đó cho đến khi nào hết khoảng thời gian 15 phút thì mới thôi.
const rateLimit = require('express-rate-limit'); // Cấu hình rate limit const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 phút max: 5, // Giới hạn mỗi IP 5 request trong 1 khoảng thời gian (windowMs) message: 'Quá nhiều yêu cầu từ IP này, vui lòng thử lại sau 15 phút.', standardHeaders: true, legacyHeaders: false, });
Bây giờ, nếu mình chạy lại đoạn script tấn công, bạn sẽ thấy một kết quả hoàn toàn khác. Sau đúng 5 request đầu tiên, hệ thống sẽ chặn tất cả các request sau đó, và logic của API đăng nhập sẽ không được thực thi nữa. Do đó, máy chủ của mình vẫn hoạt động ổn định.

Rate Limiting không phải là một tính năng “nên có”, mà là một yêu cầu bắt buộc mà bất kỳ API nào cũng phải được trang bị. Đây là tuyến phòng thủ cơ bản và quan trọng giúp bạn chống lại các cuộc tấn công phổ biến như brute-force hay DDoS, đảm bảo hệ thống luôn ổn định để phục vụ người dùng một cách tốt nhất.
1.3 Authentication (Xác thực)
Không phải API nào cũng có thể truy cập công khai. Nhiều API chứa dữ liệu nhạy cảm, như thông tin cá nhân, và yêu cầu bạn phải đăng nhập mới có thể sử dụng được. Cơ chế này chính là Authentication (Xác thực).
Hãy tưởng tượng bạn đang xem lịch sử đơn hàng trên một trang website thương mại điện tử. Bạn bắt buộc phải đăng nhập thì ứng dụng mới biết bạn là ai để hiển thị đúng những đơn hàng mà bạn đã mua. Cơ chế Authentication đảm bảo rằng không một ai khác có thể xem được thông tin này ngoại trừ bạn.
Để triển khai Authentication, thì chúng ta có một số phương pháp phổ biến như JWT, Session và OAuth 2.0. Trong bài viết này, mình sẽ tập trung vào cách phổ biến nhất là JWT (JSON Web Token).
Ý tưởng của JWT rất đơn giản:
- Đăng nhập thành công: Hệ thống sẽ tạo ra một “token” và gửi lại cho người dùng.
- Gửi yêu cầu kèm token: Kể từ đó, mỗi khi người dùng muốn truy cập một API được bảo vệ, họ chỉ cần đính kèm token này vào Header của request.
Bây giờ, chúng ta hãy cùng xem ví dụ thực tế. Giả sử, hệ thống của mình có một API là /profile
để lấy thông tin cá nhân của người dùng hiện tại, và API này yêu cầu phải xác thực.
Đầu tiên, mình sẽ gọi API /login
với email và password chính xác để hệ thống trả về một token.

Tiếp theo, mình thử gọi API /profile
mà không đính kèm token. Bạn có thể thấy, hệ thống sẽ báo lỗi ngay lập tức.

Nhưng nếu mình đính kèm token vào Header, API sẽ trả về đúng thông tin người dùng.

Vậy, làm thế nào để có thể tạo ra lớp bảo mật xác thực cho API /profile
. Hãy cùng xem mã nguồn nhé:
// Middleware để xác thực token function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Lấy token từ header "Bearer <token>" if (token == null) { return res.status(401).json({ message: 'Unauthorized: Yêu cầu cần có access token.' }); } jwt.verify(token, JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ message: 'Forbidden: Token không hợp lệ hoặc đã hết hạn.' }); } req.user = user; // Lưu thông tin người dùng đã xác thực vào request next(); // Đi tiếp tới route handler }); } // API Endpoint để lấy thông tin profile (yêu cầu xác thực) app.get('/profile', authenticateToken, async (req, res) => { try { // req.user được gán từ middleware authenticateToken const user = await User.findOne({ where: { id: req.user.id } }); if (!user) { return res.status(404).json({ errors: [{ msg: 'Không tìm thấy người dùng.' }] }); } // Trả về thông tin người dùng (không bao gồm mật khẩu) res.status(200).json({ id: user.id, name: user.name, email: user.email, }); } catch (error) { console.error('❌ Lỗi khi lấy thông tin profile:', error); res.status(500).json({ errors: [{ msg: 'Lỗi server, không thể lấy thông tin.' }] }); } });
Trước khi đi vào logic chính của API /profile
, mọi request phải đi qua một “lớp kiểm soát” gọi là middleware.

Tại đây, middleware sẽ:
- Kiểm tra xem request có đính kèm token hay không, và xác minh xem token có hợp lệ hay không.
- Nếu mọi thứ okay, request sẽ đi tiếp vào bên trong logic chính của API
/profile
. Ngược lại, request sẽ bị chặn lại và báo lỗi.
Tóm lại, nếu không có lớp bảo mật Authentication, bất kỳ ai cũng có thể truy cập vào dữ liệu nhạy cảm và thực hiện các hành động trái phép. Authentication đảm bảo rằng, chỉ những người dùng hợp lệ mới có quyền truy cập vào đúng dữ liệu của họ, giúp cho toàn bộ hệ thống của bạn có độ tin cậy cao.
Tham khảo các Khóa Học 1 Kèm 1
1.4 Authorization (Phân quyền)
Nếu Authentication trả lời câu hỏi “Bạn là ai?”, thì Authorization (Phân quyền) sẽ trả lời câu hỏi tiếp theo: “Bạn được phép làm gì?”.
Đây là cơ chế phân quyền dựa trên vai trò của một người dùng sau khi họ đã đăng nhập thành công.
Ví dụ, trong một hệ thống chỉ có admin mới được quyền xóa dữ liệu, còn user thông thường chỉ được xem hoặc chỉnh sửa. Đó chính là Authorization.
Authorization hoạt động như thế nào?
Giả sử, mình có một API DELETE /products
dùng để xóa sản phẩm. Quy tắc mình thiết lập ở đây là: chỉ Admin mới được dùng API này.
Trường hợp 1: Đăng nhập với vai trò User
Đầu tiên, mình đăng nhập bằng tài khoản user và nhận về một token.

Sau đó, mình dùng token này để gọi API xóa sản phẩm. Như bạn thấy, hệ thống sẽ từ chối và phản hồi về lỗi “Không có quyền thực hiện”.

Trường hợp 2: Đăng nhập với vai trò Admin
Lần này, mình đăng nhập bằng tài khoản admin và lấy token tương ứng.

Khi dùng token này để gọi lại API xóa sản phẩm, hệ thống đã thực hiện thành công.

Vậy, làm thế nào mà mình có thể tạo ra lớp bảo mật Authorization cho API xóa sản phẩm. Hãy cùng xem mã nguồn nhé:
// Middleware để phân quyền dựa trên vai trò (role) function authorize(role) { return (req, res, next) => { if (req.user.role !== role) { return res.status(403).json({ message: 'Forbidden: Không có quyền thực hiện hành động này.' }); } next(); }; } // API Endpoint để xóa sản phẩm (yêu cầu quyền Admin) app.delete('/products/:id', authenticateToken, authorize('admin'), (req, res) => { // Logic để xóa sản phẩm ở đây // Bên dưới chỉ là code ví dụ: const productId = req.params.id; console.log(`Sản phẩm với ID: ${productId} đã được xóa bởi Admin.`); res.status(200).json({ message: `Sản phẩm với ID: ${productId} đã được xóa thành công.` }); });
Để bảo vệ cho API xóa sản phẩm, mình không chỉ dùng một, mà là hai lớp middleware liên tiếp:
Middleware 1 – Xác thực (Authentication): Middleware này dùng để xác thực Token của người dùng có hợp lệ không?. Nếu có, nó sẽ cho request đi tiếp.
Middleware 2: Phân quyền (Authorization): Khi token hợp lệ rồi, nó sẽ kiểm tra token có phải là của admin hay không? Nếu token này là của admin, request mới được đi đến logic cuối cùng là xóa sản phẩm. Ngược lại, nó sẽ bị chặn và hệ thống báo lỗi.

Nếu không có Authorization, một khi kẻ tấn công chiếm được tài khoản của một người dùng bình thường, hacker có thể tự do truy cập và phá hoại các chức năng quản trị cấp cao. Authorization chính là lớp phòng thủ thứ 2, giúp giảm thiểu thiệt hại ngay cả khi lớp bảo mật Authentication bị vượt qua.
2. Tổng kết Flow
Như vậy, chúng ta đã cùng nhau đi qua 4 lớp bảo mật API quan trọng mà bất kỳ hệ thống backend nào cũng phải có:
- Rate Limiting (Giới hạn tần suất truy cập): Giúp giới hạn tần suất truy cập để chống lại các cuộc tấn công brute-force và DDoS, đảm bảo máy chủ không bị sập vì quá tải.
- Input Validate (Xác thực dữ liệu): Lớp phòng thủ đảm bảo mọi dữ liệu đi vào hệ thống đều phải hợp lệ, giúp ngăn chặn các cuộc tấn công như XSS.
- Authentication (Xác thực): Là lớp kiểm tra danh tính, trả lời câu hỏi “Bạn là ai?”, nhằm đảm bảo chỉ những người dùng đã đăng nhập mới có thể gọi được một số API chứa dữ liệu nhạy cảm.
- Authorization (Phân quyền): Lớp kiểm soát quyền hạn, trả lời câu hỏi “Bạn được phép làm gì?”. Lớp bảo mật này sẽ quyết định bạn có đủ quyền để thực hiện một hành động cụ thể hay không sau khi đăng nhập.
Bốn lớp bảo mật này không hoạt động độc lập, mà kết hợp chặt chẽ với nhau để tạo nên một hệ thống phòng thủ đáng tin cậy và vững chắc, giúp đảm bảo sự ổn định và giảm thiểu tối đa rủi ro bị tấn công.
Tuy nhiên, việc áp dụng 4 lớp bảo mật này vào dự án thực tế với hàng chục, hàng trăm API khác nhau không phải là một điều đơn giản. Bạn sẽ phải đối mặt với các vấn đề như:
- Làm sao để tổ chức code cho các lớp bảo mật để dễ bảo trì và mở rộng?
- Làm sao để giám sát và đảm bảo các lớp bảo mật luôn hoạt động đúng?
- Nếu phát sinh lỗi, làm sao để phát hiện và sửa lỗi một cách nhanh chóng nhất?
- Ngoài ra, trong dự án thực tế, người ta còn có những lớp bảo mật nào khác không?
Chắc chắn, mình không thể nào trả lời hết toàn bộ những câu hỏi này chỉ trong một bài viết. Tại vì đây là một bức tranh lớn về quy trình xây dựng một hệ thống backend hoàn chỉnh, mà mình muốn trang bị cho bạn trong Khóa Học Lập Trình 1 Kèm 1 của LetDiv.
Khi tham gia khóa học, chúng ta sẽ cùng nhau xây dựng một hệ thống đầy đủ từ con số 0. Giảng viên không chỉ dạy bạn code mà con chia sẻ những kinh nghiệm “xương máu” trong việc xử lý các lỗi bảo mật phổ biến trong dự án thực tế.
Ngoài ra, bạn còn được review code trực tiếp, học cách tự phát hiện lỗi bảo mật trong chính dự án của mình, và học cách tư duy như một lập trình viên chuyên nghiệp.
Nếu quan tâm, hãy tham khảo ngay khóa học 1 Kèm 1 của LetDiv tại đây nhé!
3. 4 Nguyên tắc vàng trong bảo mật (Dành cho người mới)
Đây là 4 nguyên tắc bạn phải “học thuộc lòng” và tuân thủ khi xây dựng bất kỳ một API nào, dù là nhỏ nhất:
- Không bao giờ tin tưởng frontend: Luôn luôn kiểm tra (validate) và làm sạch (sanitize) mọi dữ liệu gửi lên từ frontend, ngay cả khi frontend đã kiểm tra rồi. Hãy mặc định rằng hacker có thể bỏ qua frontend để tấn công trực tiếp vào API của bạn.
- Phân quyền tối thiểu: Trước khi cấp bất kỳ quyền nào, hãy tự hỏi: “Với vai trò này (ví dụ: ‘Nhân viên’), họ chỉ cần làm được những gì? Đọc bài viết, sửa bài viết, hay cả xóa bài viết?”. Hãy chỉ cấp vừa đủ những gì họ cần, không cấp dư thừa.
- Bảo mật nhiều lớp: Đừng chỉ dựa vào một lớp bảo mật duy nhất. Hãy kết hợp nhiều lớp bảo mật lại với nhau. Ví dụ: Để bảo vệ API xóa sản phẩm, mình cần cả hai lớp bảo mật:
- Lớp 1 (Authentication): Người dùng có đăng nhập không?
- Lớp 2 (Authorization): Người dùng có phải là Admin không?
- Cập nhật thư viện đều đặn: Các thư viện (libraries) bạn dùng có thể chứa lỗ hỏng bảo mật mà bạn không hề hay biết. Hãy luôn tạo thói quen kiểm tra và cập nhật thường xuyên để đảm bảo luôn sử dụng phiên bản an toàn và mới nhất.
Nếu thấy bài viết này hay và bổ ích, đừng ngần ngại cho mình một like và chia sẻ nhé. Chào bạn và hẹn gặp lại!