Mỗi khi ra mắt một tính năng mới – từ gửi mã OTP xác nhận số điện thoại, đăng nhập tài khoản cho tới tra cứu đơn hàng – điều khiến lập trình viên lo lắng nhất không phải là code có lỗi hay không, mà là kịch bản bị kẻ xấu dùng tool tự động spam hàng chục nghìn lượt gọi mỗi phút.
Một đoạn mã kiểm tra đăng nhập đơn giản nếu không có rào chắn giới hạn tần suất truy cập (Rate Limiting) sẽ trở thành mồi ngon cho các cuộc tấn công vét cạn (Brute-force). Hậu quả là không chỉ là nguy cơ lộ tài khoản người dùng, mà máy chủ còn bị treo cứng vì cạn kiệt CPU hoặc cháy ngân sách SMS/Email OTP chỉ sau một đêm.
Bài viết này sẽ hướng dẫn cho bạn hiểu đúng bản chất của Rate Limiting, thuật toán trượt thời gian (Sliding Windown) và cách triển khai bộ lọc chặn spam hiệu quả. bằng Redis.
1. Rate Limiting thực chất là gì và hoạt động ở tầng nào?
Rate Limiting là kỹ thuật giới hạn số lượng yêu cầu mà một máy khách được phép gửi đến máy chỉ trong một khoảng thời gian cố định. Nếu vượt quá ngưỡng này, máy chủ sẽ từ chối xử lý ngay lập tức và trả về mã trạng thái HTTP 429 Too Many Requests.
Tùy vào quy mô của hệ thống, cơ chế giới hạn này có thể được đặt ở nhiều vị trí:
- Tầng mạng/CDN biên (Cloudflare WAF):Chặn các đợt tấn công của DDoS quy mô khổng lồ trước khi gói tin chạm tới máy chủ của bạn.
- Tầng Reverse Proxy (Nginx/Caddy): Giới hạn tần suất kết nối thô theo từng địa chỉ IP với cấu hình cực nhẹ và không tốn tài nguyên ứng dụng.
- Tầng Ứng dụng (Application Middleware): Nơi kiểm soát chi tiết nhất, cho phép giới hạn linh hoạt dựa trên danh tính người dùng (User ID), số điện thoại nhận OTP hoặc loại tài khoản (gói Free hay Pro).
2. Vì sao thuật toán Fixed Windown lại dễ bị xuyên thủng?
Cách làm ngây thơ nhất mà nhiều người thường áp dụng là: chia thời gian thành các khung cố định, ví dụ “tối đa 100 yếu cầu từ 10:00 đến 10:01”.
Điểm yếu chí mạng của cách này nằm ở hiện tượng tăng vọt ở ranh giới:
- Kẻ tấn công có thể gửi 100 yêu cầu vào đúng giây thứ 59 của phút 10:00.
- Ngay sau đó 2 giây, tức giây thứ 1 của phút 10:01, hệ thống reset về 0 và cho phép thêm 100 yêu cầu nữa.
- Kết quả: Trong khoảng thời gian chỉ 2 giây quanh mốc ranh giới, hệ thống đã phải gánh tới 200 yêu cầu – gấp đôi giới hạn bạn mong muốn.
Giải pháp chuẩn xác hơn là sử dụng Thuật toán Cửa sổ trượt (Sliding Windown Counter), đo lường chính xác lượng request trong 60 giây trôi qua tính từ thời điểm hiện tại.
3. Triển khai bộ đếm trượt thời gian với Redis
Nhờ cấu trúc dữ liệu Sorted Set (ZSET) kết hợp tính năng xóa theo điểm số, Redis cho phép bạn dựng bộ đếm cửa sổ trượt cực kỳ chính xác và nhanh chóng.
Dưới đây là hàm middleware mẫu viết bằng JavaScript / Node.js:
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
/**
* Kiểm tra giới hạn tần suất truy cập theo IP hoặc Identifier
* @param {string} identifier - Chuỗi định danh (IP hoặc User ID)
* @param {number} limit - Số request tối đa
* @param {number} windowInSeconds - Khung thời gian trượt (tính bằng giây)
*/
async function checkRateLimit(identifier, limit = 10, windowInSeconds = 60) {
const key = `ratelimit:${identifier}`;
const now = Date.now();
const clearBefore = now - (windowInSeconds * 1000);
// Sử dụng Redis Pipeline / Multi để thực thi nguyên khối (Atomic)
const results = await redis
.multi()
.zremrangebyscore(key, 0, clearBefore) // 1. Xóa các request cũ nằm ngoài khung trượt
.zcard(key) // 2. Đếm số request còn lại trong khung hiện tại
.zadd(key, now, `${now}-${Math.random()}`) // 3. Ghi nhận request mới
.expire(key, windowInSeconds) // 4. Đặt thời gian sống cho key
.exec();
const currentRequests = results[1][1];
if (currentRequests >= limit) {
return {
allowed: false,
remaining: 0,
retryAfter: windowInSeconds
};
}
return {
allowed: true,
remaining: limit - currentRequests - 1,
retryAfter: 0
};
}
4. Ba điều tối quan trọng khi cấu hình Rate Limit thực tế
- Trả về Response Headers đầy đủ: Đừng chỉ trả về mã lỗi 429 cụt lủn. Hãy đính kèm các header tiêu chuẩn như
X-RateLimit-Limit,X-RateLimit-RemainingvàRetry-After: 30để phía giao diện người dùng hiển thị đồng hồ đếm ngược thân thiện cho khách. - Xử lý IP phía sau Proxy: Nếu website chạy qua Cloudflare hay Load Balancer,
req.ipmặc định của server có thể là địa chỉ IP nội bộ của Cloudflare. Hãy cấu hình lấy IP thật từ headerCF-Connecting-IPhoặcX-Forwarded-Forđể tránh việc một người vi phạm khiến toàn bộ người dùng khác bị chặn oan. - Tách riêng ngưỡng cho từng luồng nhạy cảm: Các API đọc dữ liệu thông thường có thể đặt ngưỡng rộng (60 lần/phút), nhưng các endpoint nhạy cảm như gửi OTP qua SMS hay form đăng nhập nên siết chặt ở mức 3–5 lần/phút.
Rate Limiting không đơn thuần là một công cụ chống phá hoại, mà là giải pháp bảo đảm tính công bằng về tài nguyên cho mọi người dùng trên hệ thống. Một vài dòng lệnh kiểm soát lưu lượng được thiết kế bài bản ở tầng biên và tầng logic sẽ giúp ứng dụng của bạn luôn đứng vững trước các đợt bùng nổ truy cập bất ngờ, đồng thời bảo vệ nguồn ngân sách vận hành không bị bốc hơi vì những cuộc tấn công tự động.


