Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. @nestjs/throttler
- 2.1 Modul, guard va nomlangan limitlar
- 2.2 Biz ishga tushirdik
- 2.3 Kalit va proxy ortida IP
- 2.4 Storage
- 3. Kanon o'lchovi
- 3.1 Narx
- 3.2 Xulq
- 3.3 Nega kanonda yo'q
- 3.4 Throttler'ni moslab bo'lmasmidi?
- 3.5 Qayerga qanday limit
- 4. Kanon: CheklovGuard
- 4.1 Nega middleware emas, guard
- 4.2 Zanjirdagi o'rni
- 4.3 Guard
- 4.4 Ochiq xato 1: lockout DoS → CAPTCHA
- 4.5 Fail open: ongli qaror
- 5. Hujumchi nigohi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): sliding window narxi
- 3-mashq (qiyin): "oshsa — CAPTCHA"
- 8. Loyiha qadami: CheklovGuard va CaptchaGuard
- 8.1 Fayllar
- 8.2 Tekshiruv: to'rt kutilgan xato
- 9. Real ishda
- Xulosa
- Manbalar
@nestjs/throttler: NestJS'da tezlik cheklovi, nomlangan limitlar va CAPTCHA
Qisqacha:
@nestjs/throttler— Nest'ning rasmiy tezlik cheklovchisi (rate limiter).ThrottlerModule.forRoot({ throttlers: [...] })limitlarni beradi,ThrottlerGuardglobal guard sifatida ularni qo'llaydi,@Throttle()va@SkipThrottle()esa marshrut darajasida o'zgartiradi. Tuzoq: nomlangan limitlar bilan@SkipThrottle()faqatdefaultnomli limitni o'chiradi — biz/healthni ozod qildik deb o'yladik, u 429 oldi. Kanon o'lchovida throttler xotirada kanon limiteridan ~2 baravar sekin ishladi, IETF sarlavhalarini bermadi va "limitdan oshsa — CAPTCHA" ilgagi yo'q edi. Shuning uchunvazifalar-apida 28-qism limiteriCheklovGuardsifatida qoldi va login lockout DoS'i CAPTCHA bilan tuzatildi.
Bu darsda
ThrottlerModule,ThrottlerGuard, bir nechta nomlangan limit,@Throttleva@SkipThrottleni ishlatasiz.- Throttler kalitni qanday hosil qilishini va proxy ortida IP'ni qanday to'g'ri olishni bilasiz.
- O'z storage'ingizni (Postgres) yozasiz va Redis storage qachon kerakligini tushunasiz.
- Kanon o'lchovi bilan throttler va 28-qism limiterini solishtirasiz.
- Login lockout DoS'ini "limitdan keyin — CAPTCHA" bilan tuzatasiz.
Oldin bilishingiz kerak: OAuth va tayyor auth yechimlari NestJS'da, Rate limiting, CAPTCHA tokenini serverda tekshirish, Global auth guard, @Public va @Roles, Distributed lock va rate limit Redis'da.
O'tgan darsda auth controller'lari qo'shildi va har metod ustida @Cheklov(...) dekoratori turardi — fayl esa hali yo'q edi. Bu darsda o'sha faylni yozamiz. Lekin avval Nest'ning o'z yechimini to'liq ko'ramiz.
1. Nega bu kerak?
Rate limiting darsida 28-qism limiterini qurgandik: Postgres'da fixed window, bir nechta qoida (IP, hisob+IP, email), IETF RateLimit sarlavhalari va "cheklagich buzilsa — so'rov o'tadi" (fail open). U Express middleware'i edi.
Nest dunyosida bu ishni odatda @nestjs/throttler qiladi. U Nest jamoasining rasmiy paketi, hujjatda "Rate limiting" bo'limi shunga bag'ishlangan. Ko'p loyihada app.module.ts dagi birinchi qatorlardan biri — ThrottlerModule.forRoot(...).
O'xshatish: metro turniketi. Bir kishi bir soniyada ikki marta o'tolmaydi. Throttler — tayyor turniket: o'rnatish oson, lekin uning qoidalari zavodda yozilgan. 28-qism limiteri — o'zimiz yasagan turniket: har eshikka alohida qoida, "kartasi tugasa — kassaga yubor" (CAPTCHA) kabi funksiya bilan.
2. @nestjs/throttler
2.1 Modul, guard va nomlangan limitlar
Demo loyihada @nestjs/throttler@6.7.1 (2026-09-24). Ikki limit: qisqa (10 soniyada 3 ta) va uzun (daqiqada 5 ta):
@Controller()
class VazifalarController {
@Get("vazifalar") royxat() { return { ok: 1 }; }
// shu yo'lda "qisqa" limit yumshoqroq — "uzun" ishlaydi
@Get("teglar")
@Throttle({ qisqa: { limit: 100, ttl: 10_000 } })
teglar() { return { ok: 1 }; }
@Get("health")
@SkipThrottle() // ozod deb o'ylaymiz...
health() { return { holat: "ok" }; }
@Get("health/toliq")
@SkipThrottle({ qisqa: true, uzun: true }) // har nom aniq
healthToliq() { return { holat: "ok" }; }
}
@Module({
imports: [ThrottlerModule.forRoot({
throttlers: [
{ name: "qisqa", ttl: 10_000, limit: 3 }, // 10 s da 3 ta
{ name: "uzun", ttl: 60_000, limit: 5 }, // 1 daqiqada 5 ta
],
})],
controllers: [VazifalarController],
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
class M {}ttl— oyna, millisoniyada (5-versiyadan beri; eski maqolalarda soniya).- Har so'rov hamma nomlangan limitdan o'tishi kerak. Birortasi oshsa — 429.
@Throttle({ qisqa: {...} })— shu marshrut uchun aniq limitni almashtiradi.@SkipThrottle(...)— limitdan ozod qiladi. Qavs ichidagi obyektga e'tibor bering.
2.2 Biz ishga tushirdik
Har yo'lga ketma-ket so'rovlar (NestJS 12.2.0, @nestjs/throttler 6.7.1). Chiqishda ikki narsaga qarang: sarlavha nomlari oxiridagi -qisqa / -uzun va /health qatoridagi 429:
1-javob sarlavhalari:
x-ratelimit-limit-qisqa: 3
x-ratelimit-limit-uzun: 5
x-ratelimit-remaining-qisqa: 2
x-ratelimit-remaining-uzun: 4
x-ratelimit-reset-qisqa: 10
x-ratelimit-reset-uzun: 60
GET /vazifalar ×3: 200,200,429
429 javobi:
retry-after-qisqa: 10
tana: {"statusCode":429,"message":"ThrottlerException: Too Many Requests"}
GET /teglar ×7: 200,200,200,200,200,429,429
GET /health ×10: 200,200,200,429,429,429,429,429,429,429
GET /health/toliq ×10: 200,200,200,200,200,200,200,200,200,200To'rt narsani o'qiymiz:
- Sarlavhalar — har nom uchun alohida:
x-ratelimit-limit-qisqa,…-uzun.reset— soniyada. /vazifalar— birinchi so'rov + uchta: to'rtinchisi 429. Qisqa limit (3) ishladi./teglar— qisqa limit 100 ga ko'tarilgan, endi uzun (5) ishladi: oltinchisi 429. Hisob/vazifalardan alohida — throttler kalitni marshrut (handler) bo'yicha yasaydi./health— darsning tuzog'i.@SkipThrottle()qavssiz{ default: true }degani, ya'ni faqatdefaultnomli limitni o'chiradi. Bizda bunday nom yo'q — qisqa limit ishlayveradi va 4-so'rovdan 429. Orkestrator har 10 soniyada health so'rasa ham, boshqa tekshiruvlar qo'shilsa — server "o'lik" bo'lib qoladi./health/toliqda har nom aniq yozilgan — hammasi 200.
Nega Nest shunday qilgan? @SkipThrottle paket manba kodida sukut qiymatli parametr bilan yozilgan: skip = { default: true }. Dekorator obyektdagi har kalit uchun "shu nomli limitni o'tkazib yubor" degan metama'lumot qo'yadi. Limitga nom bermasangiz, throttler unga o'zi default nomini beradi — shuning uchun hujjatdagi bitta limitli misol to'g'ri ishlaydi. Limitlarga nom qo'yganingiz zahoti default nomli limit yo'qoladi. Dekorator esa endi mavjud bo'lmagan nomni "o'chiradi" — xato ham, ogohlantirish ham chiqmaydi.
Rasmiy hujjat buni bitta gap bilan aytadi: obyekt bermasangiz, sukut — { default: true }. Lekin u yerdagi misol faqat bitta limit bilan. Saboq: nomlangan limitlarga o'tganingizda loyihadagi hamma @SkipThrottle() ni qidirib toping va nomlarni aniq yozing. Yana yaxshisi — /health uchun alohida test: "10 marta so'rasam ham 200".
Tekshirib ko'ring:
/teglaruchun hisob/vazifalardan alohida ekan. Bu login uchun qanday muammo tug'diradi?
Javob
Hujumchi bitta email'ga parol tanlashni bir nechta yo'l orqali qila oladi: /auth/kirish, /auth/otp/kirish, /auth/2fa/tekshir. Throttler har biriga alohida hisob ochadi. 28-qismdagi kirish-email qoidasi esa nom bo'yicha umumiy: qaysi yo'ldan kelsa ham, bitta email hisobiga yoziladi. Throttler'da buni qilish uchun generateKey() ni o'zingiz qayta yozishingiz kerak.
2.3 Kalit va proxy ortida IP
Throttler kalitni shunday yasaydi (kanon tajribasi bazadagi yozuvni ko'rsatdi):
throttler kaliti (generateKey — sha256(klass-handler-throttler-tracker)): th:olchov:c2a06a03ef9e882cd3bf373bfc12d38a4fe87586d4a678fdde08d9802852fca8tracker — "kim": sukut bo'yicha req.ip. Ilova nginx ortida bo'lsa, req.ip — har doim nginx manzili (127.0.0.1), ya'ni hamma foydalanuvchi bitta hisobda: bitta odam hammaga 429 olib keladi. Davosi — Express'ga proxy'ga ishonishni aytish: app.set("trust proxy", 1) (Reverse proxy). Shunda req.ip X-Forwarded-For dagi haqiqiy manzil bo'ladi.
Teskari xavf ham bor. trust proxy proxy'siz yoqilsa, hujumchi X-Forwarded-For: 1.2.3.4 ni o'zi yuboradi va har so'rovda "yangi odam" bo'ladi. 28-qismdagi test aynan shuni tekshiradi; biz uni Nest'da yurgizdik:
✓ test/cheklov.test.ts > API > muvaffaqiyatli kirish hisob+IP hisobini tozalaydi; X-Forwarded-For soxtasi trust proxy'siz yordam bermaydi, trust proxy bilan — bypass (faqat proxy orqasida yoqing!) 4759msKalitni o'zgartirish kerak bo'lsa (masalan, IP emas, foydalanuvchi id'si), ThrottlerGuard dan meros olib getTracker() ni qayta yozasiz:
@Injectable()
export class FoydalanuvchiThrottlerGuard extends ThrottlerGuard {
protected override getTracker(req: Record<string, any>) {
return Promise.resolve(String(req.egasi ?? req.ip));
}
}2.4 Storage
Sukut bo'yicha hisob jarayon xotirasida. Ilova ikki nusxada ishlasa — har nusxa o'z hisobini yuritadi va limit amalda ikki baravar bo'ladi. Ko'p nusxa uchun umumiy storage kerak. Jamiyat paketlari Redis storage beradi (biz sinamadik); ThrottlerStorage interfeysi esa oddiy — uni o'zingiz ham yozasiz. Kanon tajribasi 28-qismdagi Postgres jadvali ustida yozdi:
/** @nestjs/throttler storage — 28 dagi o'sha Postgres jadvali
* (tezlik_chegaralari) ustida: ko'p nusxada ham umumiy hisob */
class PgThrottlerStorage implements ThrottlerStorage {
readonly #c: PgCheklagich;
constructor(p: pg.Pool) {
this.#c = new PgCheklagich(p);
}
async increment(key: string, ttl: number, limit: number,
_blockDuration: number, nom: string,
): Promise<ThrottlerStorageRecord> {
const n = await this.#c.ol(`th:${nom}:${key}`, limit, ttl);
return { totalHits: n.son, timeToExpire: n.tiklanishS,
isBlocked: !n.ruxsat,
timeToBlockExpire: n.ruxsat ? 0 : n.tiklanishS };
}
}Ulash: ThrottlerModule.forRoot({ throttlers: [...], storage: new PgThrottlerStorage(pool) }). Bitta metod: "shu kalitga yana bitta zarba qo'sh va holatni ayt".
3. Kanon o'lchovi
3.1 Narx
Kanon bitta marshrutni besh xil yurgizdi: cheklovsiz, kanon limiteri (xotira va Postgres) va throttler (xotira va Postgres). Limit juda katta — 429 yo'q, faqat hisoblash narxi. 32 parallel ulanish, 3 soniya, ikki aylanish:
- Cheklovsiz100 %
- Kanon, xotira91 %
- Throttler, xotira46 %
- Kanon, Postgres36 %
- Throttler, Postgres36 %
Manba: IlmHamroh kanon o'lchovi, 2026-10-31: Node 24.21.0, NestJS 12.2.0, @nestjs/throttler 6.7.1, Postgres 18; ikki aylanish o'rtachasi (7490 / 6789 / 3472 / 2713 / 2719 req/s); bitta mashina, bir vaqtda, mashina band
Grafikdan:
- Xotirada throttler kanon limiteridan ~2 baravar sekin (46 % va 91 %).
- Postgres bilan ikkalasi teng (36 %) — vaqtni baza oladi.
Nega xotirada farq? Kanon izohiga ko'ra, throttler'ning xotira storage'i har kalitga zarba vaqtlari massivini saqlaydi va har so'rovda eskilarini filtrlaydi (sliding window). Kanon limiteri — fixed window: bitta son va oyna oxiri. Paketsiz model (brauzerda ishlaydi):
function sliding(limit, oyna) {
const zarbalar = [];
let amal = 0;
return (t) => {
const qolgan = zarbalar.filter((z) => z > t - oyna);
amal += zarbalar.length; // filter har elementni ko'radi
zarbalar.length = 0;
zarbalar.push(...qolgan, t);
return zarbalar.length <= limit;
};
}
function fixed(limit, oyna) {
let son = 0, tugash = 0, amal = 0;
return {
ol(t) {
amal++;
if (t >= tugash) { son = 0; tugash = t + oyna; }
return ++son <= limit;
},
amal: () => amal,
};
}
const s = sliding(1000, 60_000);
const f = fixed(1000, 60_000);
for (let t = 0; t < 1000; t++) { s(t); f.ol(t); }
console.log("fixed amallar:", f.amal());Konsolda:
fixed amallar: 1000Fixed window 1000 so'rovga 1000 amal qildi — har so'rovga bitta. Sliding hisobini o'zingiz chiqarib ko'ring (2-mashq): limit katta bo'lsa, har so'rov oldingi hamma zarbani ko'radi. Modelda soddalashtirdik: haqiqiy throttler ichki tuzilishi boshqacha bo'lishi mumkin, lekin "limit katta — narx o'sadi" kanon xulosasi.
Unda nega hamma fixed window ishlatmaydi? Uning ham narxi bor — oyna chegarasidagi portlash. Fixed window hisobni oyna oxirida birdan nolga tushiradi. Hujumchi oyna oxirida limitni to'liq ishlatadi, keyingi oyna boshida yana. Limit "daqiqasiga 5" bo'lsa ham, ikki soniya ichida deyarli ikki baravar so'rov o'tadi. Modelda ko'ramiz (brauzerda ishlaydi):
function fixedOyna(limit, oyna) {
let son = 0, tugash = 0;
return (t) => {
if (t >= tugash) { son = 0; tugash = t + oyna; }
return ++son <= limit;
};
}
const ol = fixedOyna(5, 60_000);
ol(0); // birinchi so'rov oynani ochadi: 0–60 s
const vaqtlar = [59_000, 59_200, 59_400, 59_600,
60_000, 60_200, 60_400, 60_600, 60_800];
const otdi = vaqtlar.filter((t) => ol(t));
console.log("59–61 s oralig'ida o'tdi:", otdi.length);
console.log("vaqtlar (s):", otdi.map((t) => t / 1000).join(", "));Konsolda:
59–61 s oralig'ida o'tdi: 9
vaqtlar (s): 59, 59.2, 59.4, 59.6, 60, 60.2, 60.4, 60.6, 60.8Nima bo'ldi? Birinchi oynada 0-soniyadagi so'rov va 59-soniyadagi to'rttasi — jami 5, limit ichida. 60-soniyada oyna yangilandi va yana beshta o'tdi. Ikki soniyadan kam vaqtda 9 so'rov — "daqiqasiga 5" qoidasi bilan. Sliding window buni ko'rmaydi: u har zarbada "oxirgi 60 soniyada nechta edi?" deb so'raydi va 6-sidan boshlab rad etadi. Narxi — har zarba vaqtini saqlash, ya'ni yuqoridagi amallar soni.
Login uchun bu portlash xavfli emas: 15 daqiqada 5 urinish o'rniga eng yomoni 10 ta, parol tanlash uchun bu baribir juda kam. Shuning uchun kanon arzon fixed window'ni ongli tanlagan. Pul yoki tashqi kvota bilan bog'liq limitda (masalan, tashqi API'ning qat'iy kvotasi) esa sliding window yoki undan ham qattiqroq algoritm kerak bo'lishi mumkin.
3.2 Xulq
Kanon chiqishidan, limit 5 / 60 s:
throttler-xotira holatlar 200,200,200,200,200,429; 429: retry-after-olchov=60; tana: {"statusCode":429,"message":"ThrottlerException: Too Many Requests"}
throttler-pg holatlar 200,200,200,200,200,429; 429: retry-after-olchov=60; tana: {"statusCode":429,"message":"ThrottlerException: Too Many Requests"}Xulq to'g'ri: oltinchisi 429. Lekin format vazifalar-api shartnomasiga mos emas: tana Problem Details emas, sarlavhalar esa X-RateLimit-*-<nom> va Retry-After-<nom>. Standart Retry-After ni (RFC 9110) mijoz kutubxonalari tushunadi, retry-after-olchov ni — yo'q.
3.3 Nega kanonda yo'q
| Mezon | @nestjs/throttler | Kanon CheklovGuard |
|---|---|---|
| Tezlik, xotira | ~2× sekin | tez |
| Tezlik, Postgres | teng | teng |
| Sarlavhalar | X-RateLimit-*-<nom> |
IETF RateLimit, RateLimit-Policy, Retry-After |
| Kalit | marshrut bo'yicha | qoida nomi bo'yicha (umumiy kirish-email) |
| 429 tanasi | {"statusCode":429,…} |
Problem Details |
| "Oshsa — CAPTCHA" | yo'q | bor |
Kanon qarori (docs/AUTH-QAROR.md): 28-qism limiteri qoladi, endi Nest guard'i sifatida.
3.4 Throttler'ni moslab bo'lmasmidi?
Bo'lardi — qisman. ThrottlerGuard ning himoyalangan (protected) metodlari bor: generateKey() — kalitni o'zingiz yasaysiz (masalan, marshrutsiz, faqat email bo'yicha), getTracker() — "kim"ni o'zingiz aniqlaysiz, throwThrottlingException() — 429 o'rniga o'z xatoingizni tashlaysiz (Problem Details filtri uni formatlaydi). Modul sozlamasida setHeaders ham bor — o'z sarlavhalaringizni qo'yish uchun sukutdagilarni o'chirish mumkin.
Lekin ro'yxatni sanang: kalit, kuzatuvchi, xato, sarlavhalar, storage, CAPTCHA tarmog'i. Oxir-oqibat throttler'dan faqat "dekorator + guard" qobig'i qoladi, ichki mantiq esa yana o'zimizniki. Shu qobiq bizda allaqachon bor va 28-qism testlari bilan sinalgan. Shuning uchun kanon moslashtirish o'rniga o'z guard'ini yozdi. Yangi loyihada esa teskarisi to'g'ri bo'lishi mumkin: throttler'dan boshlang va faqat kerakli metodni qayta yozing.
3.5 Qayerga qanday limit
Limitni tanlash — kod emas, siyosat. vazifalar-api dagi qoidalar mantig'i:
- Kirish — eng qattiq va bir nechta qatlamli: IP, hisob+IP, email. Har biri boshqa hujumni to'xtatadi.
- SMS va xat yuborish — soatiga 10 ta IP'dan. Har SMS pul turadi (SMS pumping).
- Kod tekshirish (email tasdiq, OTP, 2FA) — 15 daqiqada 30 ta: kod qisqa, tanlash oson.
- Ma'lumot yo'llari — foydalanuvchi bo'yicha daqiqasiga 600: oddiy ishlatishga xalaqit bermaydi, skript esa to'xtaydi.
Umumiy qoida: kalit hujum turiga mos bo'lsin, limit esa haqiqiy foydalanuvchi hech qachon yetmaydigan darajada.
4. Kanon: CheklovGuard
4.1 Nega middleware emas, guard
28-qismda limiter Express middleware'i edi. Nest'da tabiiy yo'l — MiddlewareConsumer.forRoutes(Controller). Lekin Middleware Nest'da darsida kanon topgan xato bor: forRoutes(Controller) bitta so'rovda middleware'ni ikki marta chaqiradi. Idempotentlik uchun bu 409 edi, limiter uchun esa hisoblagich ikki marta oshardi — limit amalda ikki baravar kichrayardi. Guard esa har handler uchun aniq bir marta ishlaydi. Qoidalar endi marshrutga dekorator bilan biriktiriladi:
const KIRISH = [
{ nom: "kirish-ip", limit: 30, oynaMs: D15, kalit: ip },
// kalit: email|IP
{ nom: "kirish-hisob", limit: 5, oynaMs: D15, kalit: … },
{ nom: "kirish-email", limit: 50, oynaMs: KUN, kalit: emailOl,
oshsa: "captcha" as const },
];
@Post("kirish")
@Cheklov(...KIRISH)
async kirish(…) { … }Uch qoida — 28-qismdagi sonlar aynan: IP bo'yicha 15 daqiqada 30 (credential stuffing), hisob+IP bo'yicha 5 (brute force), email bo'yicha kuniga 50 (taqsimlangan hujum). Klassga qo'yilgan @Cheklov(API_CHEKLOVI) (daqiqasiga 600) va metodnikilar qo'shiladi — getAllAndMerge.
4.2 Zanjirdagi o'rni
CheklovGuard yolg'iz ishlamaydi. So'rov Nest'ning so'rov hayot sikli bo'ylab o'tadi, guard'lar esa APP_GUARD ro'yxatidagi tartibda (Global auth guard darsida qo'ydik). POST /v1/auth/kirish uchun yo'l shunday:
| Qadam | Kim | Rad etsa | Nega shu joyda |
|---|---|---|---|
| 1 | middleware (Helmet, eski yo'llar) | — | hamma so'rovga, guard'dan oldin |
| 2 | KirishGuard |
401 / 403 | avval "kimsiz?" — anonim hech narsani bilib olmasin |
| 3 | CheklovGuard |
429 | kalit foydalanuvchi bo'yicha bo'lishi mumkin — kimlik kerak |
| 4 | CaptchaGuard |
400 | faqat @CaptchaKerak() yo'llarida |
| 5 | JsonTanaGuard (Content-Type) |
415 | tana formati — eng oxirgi tekshiruv |
| 6 | pipe → handler | 422 / natija | tana sxemasi, keyin ish |
Kirish yo'li @Ochiq() — 2-qadam uni o'tkazadi. Shuning uchun 3-qadamdagi uch qoida aynan kirish urinishlarini sanaydi. Yopiq yo'llarda esa anonim so'rov 2-qadamda 401 oladi va cheklagich bazasiga umuman tushmaydi — bu ham arzon.
Tekshirib ko'ring:
CheklovGuardniKirishGuarddan oldinga qo'ysak, yopiqGET /v1/vazifalarga anonim 1000 so'rov nima qiladi?
Javob
Har anonim so'rov avval cheklagichga — Postgres jadvaliga — yozuv qiladi, keyin baribir 401 oladi. Baza keraksiz yuk oladi. API_CHEKLOVI kaliti foydalanuvchi bo'yicha bo'lsa, anonimda kalit umuman yo'q — qoida ishlamaydi. Joriy tartibda esa anonim so'rov 2-qadamda to'xtaydi.
4.3 Guard
Kanon faylidan, asosiy metod (bo'lingan, izohlar qisqartirildi):
async canActivate(kontekst: ExecutionContext): Promise<boolean> {
// DEV rejim (auth yo'q) — 28 dagidek cheklov yo'q
if (this.auth === null) return true;
const qoidalar = this.reflector.getAllAndMerge<NestQoida[]>(
KALIT, [kontekst.getClass(), kontekst.getHandler()]);
if (qoidalar.length === 0) return true;
const req = kontekst.switchToHttp().getRequest<Request>();
const res = kontekst.switchToHttp().getResponse<Response>();
const c = this.auth.cheklagich;
let eng: (Natija & { q: NestQoida }) | undefined;
try {
for (const q of qoidalar) {
const k = q.kalit(req, this.auth);
if (k === undefined) continue;
const n = await c.ol(`${q.nom}:${k}`, q.limit, q.oynaMs);
if (!n.ruxsat && q.oshsa === "captcha"
&& this.auth.captcha !== undefined) {
// lockout DoS o'rniga CAPTCHA: token yo'q/noto'g'ri — 400
const t = await this.auth.captcha.tekshir(
req.get("x-captcha"), req.ip);
if (!t.ok) throw new CaptchaKerakXatosi();
continue;
}
if (eng === undefined || n.qoldi < eng.qoldi || !n.ruxsat) {
eng = { ...n, q };
}
if (!n.ruxsat) break;
}
} catch (x) {
if (x instanceof IlovaXatosi) throw x;
// cheklagich (baza) xatosi — so'rov O'TKAZILADI (fail open)
void this.auth.ombor.hodisa({ tur: "cheklov_xato", tafsilot: {
xato: x instanceof Error ? x.message.slice(0, 200) : "?" } })
.catch(() => undefined);
return true;
}
if (eng !== undefined) {
res.set("RateLimit-Policy", `${String(eng.q.limit)};w=${
String(Math.round(eng.q.oynaMs / 1000))}`);
res.set("RateLimit", `limit=${String(eng.q.limit)}, remaining=${
String(eng.qoldi)}, reset=${String(eng.tiklanishS)}`);
if (!eng.ruxsat) throw new CheklovXatosi(eng.tiklanishS);
}
return true;
}Mantiq 28-qismdagi cheklov() middleware'ining aynan o'zi: hamma qoida hisoblanadi, birortasi oshsa — 429, javobga eng qattiq qoidaning sarlavhalari. Yangi narsa — oshsa: "captcha" tarmog'i.
Jonli server, noto'g'ri parol bilan olti marta (biz ishga tushirdik):
HTTP/1.1 401 Unauthorized RateLimit-Policy: 5;w=900 RateLimit: limit=5, remaining=4, reset=900
HTTP/1.1 401 Unauthorized RateLimit-Policy: 5;w=900 RateLimit: limit=5, remaining=3, reset=900
HTTP/1.1 401 Unauthorized RateLimit-Policy: 5;w=900 RateLimit: limit=5, remaining=2, reset=900
HTTP/1.1 401 Unauthorized RateLimit-Policy: 5;w=900 RateLimit: limit=5, remaining=1, reset=900
HTTP/1.1 401 Unauthorized RateLimit-Policy: 5;w=900 RateLimit: limit=5, remaining=0, reset=900
HTTP/1.1 429 Too Many Requests RateLimit-Policy: 5;w=900 RateLimit: limit=5, remaining=0, reset=900 Retry-After: 900
{"type":"https://vazifalar-api.example/xatolar/tezlik-cheklovi","title":"Juda ko'p so'rov","status":429,"detail":"Juda ko'p so'rov — 900 soniyadan keyin urinib ko'ring","instance":"/v1/auth/kirish","kod":"tezlik-cheklovi","sorovId":"e8af4245-e71e-4f1e-9484-097f2caef19f"}Sarlavhada eng qattiq qoida — kirish-hisob (5 ta, 900 soniya). Oltinchi urinish — 429, standart Retry-After va Problem Details. sorovId har safar boshqa.
4.4 Ochiq xato 1: lockout DoS → CAPTCHA
28-qismda bu zaiflikni halol topilma sifatida ochiq qoldirgandik. kirish-email qoidasi kuniga 50 urinishdan keyin email egasining o'zini ham 24 soatga bloklardi. Hujumchiga parolni bilish shart emas: sizning emailingizga 50 marta noto'g'ri parol yuborsa — siz kun bo'yi kira olmaysiz. Bu lockout DoS (Rate limiting).
Tuzatish: limitdan keyin bloklash emas, CAPTCHA. To'g'ri Turnstile tokeni bilan so'rov qoidadan o'tadi. Token yo'q — 400 captcha-kerak (429 emas: frontend vidjetni ko'rsatadi). Hujumchi ham har urinishda CAPTCHA yechishi kerak — bot uchun qimmat, odam uchun bir bosish.
Kanon soxta siteverify bilan sinadi, uning javob shakli esa Cloudflare'ning rasmiy test kalitlari bilan olingan (kanon chiqishi):
secret=1x0000000000000000000000000000000AA → {"challenge_ts":"2026-10-30T20:25:27.829Z","error-codes":[],"hostname":"example.com","metadata":{"result_with_testing_key":true},"success":true}
secret=2x0000000000000000000000000000000AA → {"error-codes":["invalid-input-response"],"success":false,"messages":[],"metadata":{"result_with_testing_key":true}}1x…AA — doim o'tadigan, 2x…AA — doim rad etadigan test siri. Haqiqiy Cloudflare hisobi ishlatilmagan. Biz yurgizgan ikki test:
✓ test/cheklov.test.ts > API > BACKLOG 1: kirish-email limiti (50/kun) oshgach — bloklash (429) EMAS, CAPTCHA: tokensiz 400 captcha-kerak, to'g'ri token + parol — 200 7598ms
✓ test/cheklov.test.ts > API > BACKLOG 1: CAPTCHA sozlanmagan bo'lsa — avvalgidek 429 (fail closed: bloklash DoS'dan yomonroq — parol tanlash) 13976msIkkinchi test muhim qarorni qotiradi: CAPTCHA sozlanmagan bo'lsa — eski xulq (429) qoladi. Cheklovni butunlay o'chirib, parol tanlashga yo'l ochish bloklashdan yomonroq.
Tekshirib ko'ring: Nega CAPTCHA kerak bo'lganda 400 qaytadi, 429 emas? Ikkalasi ham "hozir o'tolmaysiz" degani-ku.
Javob
Ma'nosi boshqa. 429 — "kuting" (Retry-After): mijoz kutadi va keyin yana urinadi. 400 captcha-kerak — "kutish yordam bermaydi, avval CAPTCHA'ni yeching". Frontend kod ga qarab vidjetni ko'rsatadi. Agar 429 qaytsa, frontend foydalanuvchiga "15 daqiqadan keyin urinib ko'ring" deydi — bu esa aynan lockout DoS'ning o'zi.
4.5 Fail open: ongli qaror
Guard'ning catch qismiga yana qarang. Cheklagich — Postgres'dagi jadval. Baza bir lahzaga javob bermasa nima bo'ladi? Ikki yo'l bor: so'rovni rad etish (fail closed) yoki o'tkazish (fail open). Kanon o'tkazadi va hodisani jurnalga yozadi.
Sababi: tezlik cheklovi — qo'shimcha himoya. Asosiy himoya (parol hash'i, sessiya, RLS) cheklagichsiz ham ishlaydi. Agar cheklagich xatosida hamma so'rov rad etilsa, bazadagi kichik uzilish butun API'ni to'xtatadi — hujumchi uchun bu yangi DoS yo'li. Lekin bu qaror har joyda to'g'ri emas: to'lov yoki SMS yuborishda cheklagich buzilsa, pul ketadi. U yerda fail closed to'g'riroq. 28-qismdagi xavfsizlik tamoyillari darsidagi "ochiq yiqilish / yopiq yiqilish" savoli — har cheklovda alohida beriladi.
5. Hujumchi nigohi
- Yo'llar bo'ylab tarqatish. Throttler hisobi marshrut bo'yicha —
/kirish,/otp/kirish,/2fa/tekshiruch alohida limit. Umumiy qoida nomi bo'yicha bo'lishi kerak. - Soxta
X-Forwarded-For.trust proxyproxy'siz yoqilgan — har so'rov yangi IP. - Ko'p nusxa. Xotira storage'i bilan ikki nusxa — ikki baravar limit. Umumiy storage (Postgres, Redis).
- Lockout DoS. Begona email'ga 50 urinish — egasi kun bo'yi chiqib qoladi. Limitdan keyin CAPTCHA.
- Cheklagichni yiqitish. Baza ishlamasa, kanon so'rovni o'tkazadi (fail open) va jurnalga yozadi — cheklov qo'shimcha himoya, butun API'ni to'xtatmasin. Bu ongli qaror: asosiy himoya (parol hash, sessiya) baribir ishlaydi.
6. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
/health 429 oladi |
@SkipThrottle() faqat default ni o'chiradi |
@SkipThrottle({ qisqa: true, uzun: true }) |
| Limit 1000 marta katta | ttl soniyada yozilgan (eski maqola) |
5-versiyadan beri millisoniya |
| Hamma foydalanuvchi bir hisobda | nginx ortida trust proxy yo'q |
app.set("trust proxy", 1) |
| Ikki nusxada limit ikki baravar | xotira storage | umumiy storage |
| Mijoz qachon qayta urinishni bilmaydi | Retry-After-<nom> standart emas |
standart Retry-After |
/kirish limiti /otp/kirish orqali aylanib o'tiladi |
throttler kaliti marshrut bo'yicha | generateKey() yoki qoida nomi bo'yicha umumiy kalit |
| Oyna chegarasida limitdan ko'p so'rov o'tadi | fixed window hisobni birdan nollaydi | login uchun qabul; qat'iy kvota uchun sliding window |
| Baza uzilsa butun API to'xtaydi | cheklagich xatosida so'rov rad etiladi (fail closed) | qo'shimcha himoyada fail open + jurnal |
7. Mashqlar
Mashqlar kurs/mashqlar/30/60-throttler/ da.
1-mashq (oson): bo'shliqlarni to'ldiring
- Limitlarni beradigan modul —
.forRoot({ throttlers: [...] }) - Throttler'da
ttlbirligi — - Nomlangan limitlarda
/healthni ozod qilish —@SkipThrottle({ qisqa: , uzun: true }) - Proxy ortida haqiqiy IP uchun Express sozlamasi —
app.set("", 1) - Kanonda
kirish-emaillimitidan keyin nima so'raladi? —
Yechim
- ThrottlerModule —
@nestjs/throttlerdan; guard —ThrottlerGuard. - millisoniya —
ttl: 60_000— bir daqiqa. - true — har nom uchun aniq; qavssiz
@SkipThrottle()— faqatdefault. - trust proxy — faqat haqiqatan proxy ortida.
- CAPTCHA — lockout DoS o'rniga; sozlanmagan bo'lsa — 429.
2-mashq (o'rta): sliding window narxi
Darsdagi sliding() modeliga amallar() metodini qo'shing va 1000 so'rov uchun fixed va sliding amallarini solishtiring. Keyin limitni 100 ga, oynani 100 ga tushirib qayta o'lchang. Ishora: qaytariladigan funksiyani obyektga aylantiring ({ ol, amallar }), xuddi fixed() dagidek.
Yechim
function sliding(limit, oyna) {
const zarbalar = [];
let amal = 0;
return {
ol(t) {
const qolgan = zarbalar.filter((z) => z > t - oyna);
amal += zarbalar.length;
zarbalar.length = 0;
zarbalar.push(...qolgan, t);
return zarbalar.length <= limit;
},
amallar: () => amal,
};
}
for (const [limit, oyna] of [[1000, 60_000], [100, 100]]) {
const s = sliding(limit, oyna);
for (let t = 0; t < 1000; t++) s.ol(t);
console.log(`limit ${limit}, oyna ${oyna}: ${s.amallar()} amal`);
}Konsolda:
limit 1000, oyna 60000: 499500 amal
limit 100, oyna 100: 94950 amalFixed window ikkala holatda 1000 amal qiladi. Sliding esa oynadagi zarbalar soniga bog'liq: oyna katta bo'lsa, har so'rov oldingi hammasini ko'radi — 1000 so'rovga ~500 ming amal. Narx limit va oyna bilan o'sadi. Shu sabab sliding window odatda Redis'dagi tartiblangan to'plamlar yoki taxminiy algoritmlar bilan yoziladi (Rate limit Redis'da).
3-mashq (qiyin): "oshsa — CAPTCHA"
Kanon guard'ining qoida tarmog'ini modellashtiring. tekshir(qoidalar, hisob, captchaOk) — har qoida { nom, limit, oshsa? }, hisob — nom → shu so'rovdagi son. Qoida oshsa: oshsa === "captcha" va captchaOk === true — o'tkazadi, false — 400 captcha-kerak; oshsa yo'q — 429 <nom>. Hammasi o'tsa — "o'tdi". Ishora: for...of, continue.
Yechim
function tekshir(qoidalar, hisob, captchaOk) {
for (const q of qoidalar) {
if (hisob[q.nom] <= q.limit) continue;
if (q.oshsa === "captcha") {
if (captchaOk) continue;
return "400 captcha-kerak";
}
return `429 ${q.nom}`;
}
return "o'tdi";
}
const KIRISH = [
{ nom: "kirish-hisob", limit: 5 },
{ nom: "kirish-email", limit: 50, oshsa: "captcha" },
];
const hujum = { "kirish-hisob": 1, "kirish-email": 51 };
console.log("egasi, CAPTCHA'siz:", tekshir(KIRISH, hujum, false));
console.log("egasi, CAPTCHA bilan:", tekshir(KIRISH, hujum, true));
const brute = { "kirish-hisob": 6, "kirish-email": 51 };
console.log("brute force:", tekshir(KIRISH, brute, true));Konsolda:
egasi, CAPTCHA'siz: 400 captcha-kerak
egasi, CAPTCHA bilan: o'tdi
brute force: 429 kirish-hisobHujumchi 50 marta urinib, email limitini tugatdi. Egasi yangi qurilmadan (hisob+IP — 1) CAPTCHA bilan kira oldi. Uchinchi qator: bitta IP'dan parol tanlash — CAPTCHA ham yordam bermaydi, kirish-hisob 429 beradi. Ikki qoida birgalikda ishlaydi.
8. Loyiha qadami: CheklovGuard va CaptchaGuard
8.1 Fayllar
| Fayl | Nima |
|---|---|
src/auth/cheklov.guard.ts |
yangi: NestQoida, @Cheklov(), CheklovGuard, @CaptchaKerak(), CaptchaGuard, CaptchaKerakXatosi |
vazifalar.controller.ts, teglar.controller.ts, eksport.controller.ts |
klass ustida @Cheklov(API_CHEKLOVI) |
CaptchaGuard — 28-qismdagi captchaKerak middleware'ining guard shakli: @CaptchaKerak() bilan belgilangan yo'llarda (ro'yxat, parol tiklash so'rovi, SMS yuborish) X-Captcha sarlavhasini Turnstile bilan tekshiradi. CAPTCHA sozlanmagan bo'lsa — o'tkazadi. Rad etilsa — jurnalga bot_tutildi va 400.
Controller'larga qo'shiladigan qator (kanondan):
@Controller({ path: "vazifalar", version: ["1", VERSION_NEUTRAL] })
+@Cheklov(API_CHEKLOVI)
export class VazifalarController {8.2 Tekshiruv: to'rt kutilgan xato
Shu kesimni biz kanon fayllaridan yig'ib tekshirdik (npx tsc --noEmit):
src/ilova.ts(59,84): error TS2322: Type 'VazifaOmbori | OmborFabrikasi | undefined' is not assignable to type 'VazifaOmbori | undefined'.
test/pg-ombor.test.ts(199,36): error TS2339: Property 'yarat' does not exist on type 'VazifaOmbori | OmborFabrikasi'.
test/pg-ombor.test.ts(217,37): error TS2339: Property 'yarat' does not exist on type 'VazifaOmbori | OmborFabrikasi'.
test/prisma-ombor.test.ts(125,33): error TS2339: Property 'yarat' does not exist on type 'VazifaOmbori | OmborFabrikasi'.O'n bir xatodan yettitasi yo'qoldi: cheklov.guard.ts paydo bo'lgach, TS2307 lar ketdi, NestQoida turi kelgach — TS7006 lar ham. Qolgan to'rttasi — AppModule va eski testlar. Ular oxirgi qadamda ketadi. Hali commit qilmang.
9. Real ishda
- Har ommaviy Nest API'sida
ThrottlerModulebor — ko'pincha bitta global limit bilan. Login va SMS yo'llariga alohida qattiq limit qo'yish — yaxshi jamoalarning belgisi. - Production'da throttler storage — deyarli doim Redis: ilova bir necha nusxada ishlaydi.
- CAPTCHA — Cloudflare Turnstile, hCaptcha, reCAPTCHA. Faqat shubhali vaziyatda ko'rsatish (limitdan keyin) — foydalanuvchi uchun eng yumshoq yo'l.
- Intervyu: "Fixed va sliding window farqi?", "Proxy ortida rate limiter nimaga e'tibor beradi?", "Lockout DoS nima va undan qanday himoyalanasiz?".
Xulosa
@nestjs/throttler:ThrottlerModule.forRoot({ throttlers })+ThrottlerGuard(APP_GUARD) +@Throttle/@SkipThrottle;ttl— millisoniya.- Tuzoq: nomlangan limitlarda
@SkipThrottle()faqatdefaultni o'chiradi — biz/healthda 429 ko'rdik. - Kalit — marshrut bo'yicha; proxy ortida
trust proxy, ko'p nusxada umumiy storage. - Kanon o'lchovi: xotirada ~2× sekin, Postgres bilan teng; sarlavhalar IETF emas, CAPTCHA ilgagi yo'q —
CheklovGuardqoladi. - ochiq xato 1:
kirish-emaillimitidan keyin bloklash emas, CAPTCHA; sozlanmagan bo'lsa — 429.
Keyingi dars: Autentifikatsiya II: o'z backend JWT bilan va muqobillar — AuthModule ni ulaymiz, Next.js frontend'ni Nest backend'imiz bilan bog'laymiz, testlarni yurgizamiz va auth bosqichini commit qilamiz.
Manbalar
- NestJS hujjati — Rate limiting (
@nestjs/throttler): https://docs.nestjs.com/security/rate-limiting @nestjs/throttler— GitHub (CHANGELOG,ttlmillisoniyada): https://github.com/nestjs/throttler- IETF — RateLimit header fields for HTTP (qoralama): https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
- Cloudflare Turnstile — testing (rasmiy test kalitlari): https://developers.cloudflare.com/turnstile/troubleshooting/testing/
- OWASP — Blocking Brute Force Attacks (account lockout DoS): https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks
- Kanon o'lchovi (IlmHamroh, 2026-10-31): throttler va kanon limiteri, Turnstile test kalitlari. Demo (nomlangan limitlar,
@SkipThrottletuzog'i), jonli server, cheklov testlari va oraliqtsckesimi — biz ishga tushirdik (2026-11-02, NestJS 12.2.0,@nestjs/throttler6.7.1, PostgreSQL 17.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!