Mundarija (39)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 300 so'rov — 300 javob
- 1.2 Kim hujum qiladi
- 2. Algoritmlar: qisqa takror
- 2.1 Bitta ketma-ketlik, uchta javob
- 2.2 Nega kanon baribir fixed window'ni tanladi
- 3. Kalit: kimni sanaymiz
- 3.1 Lockout DoS
- 3.2 Kanonning login qoidalari
- 3.3 Halol topilma: kunlik qoida lockout'ni qaytaradi
- 3.4 IP qayerdan olinadi: trust proxy
- 4. Qayerda saqlash
- 4.1 Uch variant
- 4.2 UNLOGGED jadval
- 4.3 Bitta atomik so'rov
- 5. Javob: 429 va sarlavhalar
- 5.1 Status va Retry-After
- 5.2 RateLimit va RateLimit-Policy
- 6. Cheklagich buzilsa: fail open yoki fail closed
- 6.1 Ikki yo'l
- 6.2 Kanon qarori va uning narxi
- 7. Kutubxonalar
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): sliding window counter
- 3-mashq (qiyin): bir nechta qoida va eng qattiq sarlavha
- 11. Loyiha qadami: tezlik cheklovi
- 11.1 Migratsiya va Prisma
- 11.2 src/umumiy/cheklov.ts
- 11.3 Qoidalar: src/auth/marshrut.ts
- 11.4 Ma'lumot yo'llari va ulash
- 11.5 Testlar
- 11.6 Tekshiruv va commit
- 12. Real ishda
- Xulosa
- Manbalar
Rate limiting: tezlik cheklovi, kalit tanlash, RateLimit sarlavhalari va fail open
Qisqacha: Tezlik cheklovi (rate limiting) — bitta mijoz ma'lum vaqtda nechta so'rov yuborishi mumkinligini cheklaydi. U parol terib ko'rishni (brute force) va credential stuffing'ni qimmatlashtiradi, SMS va xat yuboruvchi yo'llarni pul o'g'irlashdan saqlaydi. Eng muhim qaror — algoritm emas, kalit: nima bo'yicha sanaymiz (IP, hisob, hisob+IP). Bu darsda
vazifalar-apiga Postgres'dagi hisoblagich bilan cheklov qo'yamiz,429vaRateLimitsarlavhalarini qaytaramiz va cheklagich buzilsa nima bo'lishini ongli tanlaymiz.
Bu darsda
- Fixed window, sliding window va token bucket'ni bitta so'rovlar ketma-ketligida solishtirasiz.
- Kalit tanlash (IP, hisob, hisob+IP, kunlik email) va lockout DoS tuzog'ini tushuntirasiz.
429,Retry-AftervaRateLimitsarlavhalarini va ularning standart holatini bilasiz.- Cheklagich buzilganda "fail open" va "fail closed" farqini va narxini tushuntirasiz.
vazifalar-apiga UNLOGGED jadval,cheklov.tsva oltita testni qo'shib, commit qilasiz.
Oldin bilishingiz kerak: Distributed lock va rate limit Redis'da (algoritmlar), Mashhur middleware'lar (express-rate-limit), Reverse proxy orqasida Node (trust proxy), Parolni xavfsiz saqlash, Ikki faktorli autentifikatsiya va passkeys.
27-qismda fixed window, sliding window va token bucket'ni Redis'da yozgansiz. 25-qismda express-rate-limit ni sinab, uni loyihaga kiritmagan edik — o'shanda himoya qiladigan narsa yo'q edi. Endi bor: login, 2FA kodi, SMS. Bugun algoritmni emas, qarorlarni o'rganamiz: kimni sanaymiz, qayerda saqlaymiz, nima qaytaramiz va hisoblagich ishlamay qolsa nima qilamiz.
1. Nega bu kerak?
1.1 300 so'rov — 300 javob
28-qism boshida kanon 27-qism yakunidagi API'ga ketma-ket 300 ta so'rov yubordi (kanon tajribasi):
=== 6) Tezlik cheklovi yo'q: 300 ta ketma-ket so'rov holat kodlari
300 200300 ta — hammasi 200. Login hali yo'q edi, lekin u paydo bo'lgach, xuddi shu tarzda parol ham terib ko'riladi. Har urinish bizga argon2 hisobi (~33 ms, 19 MiB) turadi (Parolni xavfsiz saqlash) — demak cheklovsiz login nafaqat parolni, balki serverni ham xavf ostiga qo'yadi.
1.2 Kim hujum qiladi
| Hujum | Qanday ko'rinadi | Qaysi kalit ushlaydi |
|---|---|---|
| brute force | bitta hisobga ko'p parol | hisob (+IP) |
| credential stuffing | bitta IP'dan ko'p hisob, sizgan parollar bilan | IP |
| taqsimlangan hujum | ko'p IP, bitta hisob | hisob bo'yicha kunlik |
| 2FA kodini terish | 6 raqam — 1 000 000 variant | hisob |
| SMS pumping | ko'p raqamga SMS — sizning pulingizga | IP, raqam, kunlik byudjet |
| toshqin (flood) | oddiy yo'llarga juda ko'p so'rov | foydalanuvchi |
Oxirgi qator haqida halol: haqiqiy DDoS'ni ilova darajasidagi cheklov to'xtatmaydi — so'rovlar baribir serverga keladi. Bu ish tarmoq, CDN va proxy darajasida qilinadi. Bizning cheklov — qimmat amallarni himoya qilish uchun.
2. Algoritmlar: qisqa takror
2.1 Bitta ketma-ketlik, uchta javob
27-qismdagi uchta algoritmni xotirada, soxta soat bilan solishtiramiz. Limit — 60 soniyada 5 ta. Hujumchi oyna chegarasini biladi: 55–59-soniyalarda 5 ta, 60–64-soniyalarda yana 5 ta yuboradi:
// limit: 5 ta so'rov / 60 s. Vaqt — soniyalarda (soxta soat)
const LIMIT = 5;
const OYNA = 60;
function fixedWindow() {
let boshi = 0;
let son = 0;
return (t) => {
if (t >= boshi + OYNA) {
boshi = t - (t % OYNA); // oyna daqiqa boshidan
son = 0;
}
son += 1;
return son <= LIMIT;
};
}
function slidingLog() {
const vaqtlar = [];
return (t) => {
while (vaqtlar.length > 0 && vaqtlar[0] <= t - OYNA) {
vaqtlar.shift();
}
if (vaqtlar.length >= LIMIT) return false;
vaqtlar.push(t);
return true;
};
}
function tokenBucket() {
let tokenlar = LIMIT;
let oxirgi = 0;
return (t) => {
const qoshildi = ((t - oxirgi) * LIMIT) / OYNA;
tokenlar = Math.min(LIMIT, tokenlar + qoshildi);
oxirgi = t;
if (tokenlar < 1) return false;
tokenlar -= 1;
return true;
};
}
// hujumchi: 55–59 s da 5 ta, 60–64 s da yana 5 ta
const SOROVLAR = [55, 56, 57, 58, 59, 60, 61, 62, 63, 64];
const ALGORITMLAR = { fixedWindow, slidingLog, tokenBucket };
for (const [nom, yasa] of Object.entries(ALGORITMLAR)) {
const ol = yasa();
const javob = SOROVLAR.map((t) => (ol(t) ? "+" : "-")).join("");
const otdi = javob.split("+").length - 1;
console.log(nom.padEnd(12), javob, `${otdi} ta / 10 s`);
}Konsolda:
fixedWindow ++++++++++ 10 ta / 10 s
slidingLog +++++----- 5 ta / 10 s
tokenBucket +++++----- 5 ta / 10 sFixed window 10 soniyada 10 ta o'tkazdi — limitning ikki barobari. 60-soniyada oyna yangilandi va hisob nolga tushdi. Bu oyna chegarasidagi portlash. Sliding log va token bucket — 5 ta.
2.2 Nega kanon baribir fixed window'ni tanladi
| Algoritm | Saqlaydi | Aniqlik | Narx |
|---|---|---|---|
| fixed window | bitta son + tugash vaqti | chegarada 2× | eng arzon — bitta qator |
| sliding log | har so'rov vaqti | aniq | ko'p xotira |
| sliding window counter | ikki son | taxminiy | arzon |
| token bucket | tokenlar + vaqt | portlashga ruxsat beradi | arzon, lekin hisob murakkab |
Login uchun limit kichik (15 daqiqada 5 ta) va oyna uzun. Eng yomon holat — 15 daqiqada 10 ta urinish. Parol terish uchun bu baribir yo'q darajada kam. Narxi esa — Postgres'da bitta qator va bitta so'rov. Kanon kodining izohida ham shunday yozilgan: chegarada 2× — login uchun maqbul; kerak bo'lsa sliding yoki token bucket. Ommaviy API'da, qat'iy kvota sotiladigan joyda — boshqa tanlov.
3. Kalit: kimni sanaymiz
3.1 Lockout DoS
Eng sodda g'oya — "hisob bo'yicha 5 ta noto'g'ri urinish, keyin blok". Hujumchi buni teskarisiga ishlatadi: qurbonning emailini biladi, ataylab 5 marta noto'g'ri parol kiritadi — va haqiqiy egasi kira olmay qoladi. Bu lockout DoS (bloklash orqali xizmatdan mahrum qilish).
// hisoblagich: kalit → urinishlar soni (oyna hisobga olinmagan)
function cheklagich(limit) {
const m = new Map();
return (kalit) => {
const son = (m.get(kalit) ?? 0) + 1;
m.set(kalit, son);
return son <= limit;
};
}
const EMAIL = "aziz@example.com";
const HUJUMCHI_IP = "203.0.113.9";
const AZIZ_IP = "198.51.100.4";
for (const [nom, kalitYasa] of [
["faqat email", (email) => email],
["email + IP", (email, ip) => `${email}|${ip}`],
]) {
const ol = cheklagich(5);
// hujumchi Azizning hisobiga 5 marta terdi
for (let i = 0; i < 5; i++) ol(kalitYasa(EMAIL, HUJUMCHI_IP));
// Aziz o'z uyidan to'g'ri parol bilan kiradi
const aziz = ol(kalitYasa(EMAIL, AZIZ_IP));
const hujumchi = ol(kalitYasa(EMAIL, HUJUMCHI_IP));
console.log(nom.padEnd(12), "Aziz:", aziz ? "kirdi" : "429",
"| hujumchi:", hujumchi ? "davom" : "429");
}Konsolda:
faqat email Aziz: 429 | hujumchi: 429
email + IP Aziz: kirdi | hujumchi: 429"Faqat email" kaliti hujumchini to'xtatdi — lekin Azizni ham. "Email + IP" kaliti faqat hujumchini to'xtatdi. IP'lar — hujjatlar uchun ajratilgan namunaviy manzillar (203.0.113.0/24, 198.51.100.0/24).
3.2 Kanonning login qoidalari
Bitta kalit hamma hujumni ushlamaydi, shuning uchun login'da uchta qoida birga ishlaydi:
| Qoida | Kalit | Limit | Nimaga qarshi |
|---|---|---|---|
kirish-ip |
IP | 30 / 15 daqiqa | credential stuffing |
kirish-hisob |
email + IP | 5 / 15 daqiqa | brute force, lockout'siz |
kirish-email |
50 / kun | taqsimlangan hujum |
Muvaffaqiyatli kirish kirish-hisob hisobini tozalaydi: Aziz parolni ikki marta adashtirib, uchinchisida kirsa — keyingi safar yana 5 ta urinishi bor.
IP limiti nega 30, 5 emas? Bitta IP orqasida ko'p odam bo'lishi mumkin: ofis, universitet yotoqxonasi, mobil operator (CGNAT — bir manzil ostida minglab abonent). Juda qattiq IP limiti ularni birgalikda bloklaydi.
3.3 Halol topilma: kunlik qoida lockout'ni qaytaradi
Uchinchi qoida taqsimlangan hujumni ushlaydi — lekin u faqat email bo'yicha. Biz sinab ko'rdik (biz ishga tushirdik): kanon kodi, trust proxy yoqilgan test ilovasi, har 5 so'rovda boshqa X-Forwarded-For. Hujumchi 10 xil IP'dan Azizning hisobiga jami 50 ta noto'g'ri urinish qildi, keyin Aziz yangi IP'dan to'g'ri parol bilan kirdi:
hujum: 401 | Aziz: 429 50;w=86400 86396Hujumchining 50 ta urinishi hammasi 401 oldi (har IP — 5 tadan, kirish-hisob oshmadi). Lekin Aziz 429 oldi va keyingi urinish uchun ~24 soat kutishi kerak (Retry-After: 86396). Kanonning izohidagi "lockout DoS yo'q" faqat kirish-hisob qoidasi uchun to'g'ri. Kunlik qoida esa uni boshqa ko'rinishda qaytaradi — endi buning uchun 10 ta IP kerak.
Bu xato emas, murosa (trade-off). Taqsimlangan hujumga qarshi ham, lockout'ga qarshi ham bir vaqtda kurashish qiyin. Real tizimlarda limit oshganda bloklash o'rniga CAPTCHA so'raladi (CAPTCHA tokenini serverda tekshirish, honeypot va SMS pumping). Tanish qurilmadan (avval kirgan sessiya cookie'si bilan) kelgan urinish alohida sanaladi, foydalanuvchiga email orqali xabar yuboriladi. Kanon kodni o'zgartirmadi — siz loyihangizda shu qoidaning narxini bilib qoldiring.
3.4 IP qayerdan olinadi: trust proxy
req.ip — Express aniqlagan mijoz manzili. Proxy orqasida u proxy'ning manzili bo'ladi, trust proxy yoqilsa — X-Forwarded-For dan olinadi (Reverse proxy orqasida Node). Xavf: proxy yo'q joyda trust proxy yoqilsa, hujumchi har so'rovga boshqa X-Forwarded-For yozadi — va har safar "yangi IP" bo'lib ko'rinadi.
Kanon testi ikkalasini tekshiradi: trust proxy o'chiq — soxta sarlavha yordam bermaydi (6-urinish 429); yoqiq — 6 ta urinish 6 xil "IP" dan, hammasi 401. Biz jonli serverda ham sinadik (biz ishga tushirdik, TRUST_PROXY=false) — soxta X-Forwarded-For: 198.51.100.7 bilan ham javob o'zgarmadi:
HTTP/1.1 429 Too Many Requests
RateLimit-Policy: 5;w=900
RateLimit: limit=5, remaining=0, reset=899
Retry-After: 8994. Qayerda saqlash
4.1 Uch variant
| Ombor | Afzalligi | Kamchiligi |
|---|---|---|
| jarayon xotirasi | eng tez, bog'liqliksiz | har nusxa o'z hisobini yuritadi; qayta ishga tushsa — nol |
| Redis | tez, TTL, ko'p nusxa uchun umumiy | yana bitta xizmat |
| Postgres | allaqachon bor, tranzaksiya | har so'rovda bazaga yozish |
vazifalar-api da Redis majburiy emas (27-qismda u ixtiyoriy kesh edi). Login esa baribir bazaga boradi. Shuning uchun kanon Postgres'ni tanladi — bog'liqlik qo'shilmadi, Redis varianti esa 27-qismdagi kodingizda bor.
4.2 UNLOGGED jadval
Migratsiya sql/122-tezlik-chegaralari.sql:
CREATE UNLOGGED TABLE tezlik_chegaralari (
kalit text NOT NULL,
son int NOT NULL,
tugash timestamptz NOT NULL,
CONSTRAINT tezlik_chegaralari_pkey PRIMARY KEY (kalit),
CONSTRAINT tezlik_chegaralari_kalit_check
CHECK (char_length(kalit) <= 300)
);
CREATE INDEX tezlik_chegaralari_tugash_idx
ON tezlik_chegaralari (tugash);
GRANT SELECT, INSERT, UPDATE, DELETE ON tezlik_chegaralari
TO vazifalar_auth;
REVOKE ALL ON tezlik_chegaralari FROM vazifalar_ilova;Telefon uchun qatorlarni bo'ldik, izohlarni olib tashladik. UNLOGGED — Postgres bu jadvalni WAL'ga (o'zgarishlar jurnaliga) yozmaydi: yozish tezroq, lekin server qulasa, jadval bo'shatiladi. Hisoblagich uchun bu maqbul yo'qotish: eng yomoni — hujumchi 15 daqiqalik oynani boshidan boshlaydi. Huquq faqat vazifalar_auth rolida (RBAC va resurs egaligi).
O'z bazangizda tekshiring (biz ishga tushirdik, PostgreSQL 17.6, holat sxemasi bilan):
SELECT relname, relpersistence
FROM pg_class
WHERE relname = 'tezlik_chegaralari';Natija:
relname | relpersistence
--------------------+----------------
tezlik_chegaralari | u
(1 row)u — unlogged (p — oddiy, doimiy jadval).
4.3 Bitta atomik so'rov
Hisoblagichning yuragi — bitta INSERT … ON CONFLICT DO UPDATE (UPSERT va MERGE). Qator yo'q bo'lsa — son = 1; bor va oyna tugagan bo'lsa — yana 1; aks holda +1. So'rovni PREPARE bilan bir marta e'lon qilamiz va uch marta chaqiramiz. Ikkinchi va uchinchi chaqiruv orasida oynani "tugatamiz":
PREPARE ol(text) AS
INSERT INTO tezlik_chegaralari AS t (kalit, son, tugash)
VALUES ($1, 1, now() + interval '15 minutes')
ON CONFLICT (kalit) DO UPDATE SET
son = CASE WHEN t.tugash <= now() THEN 1 ELSE t.son + 1 END,
tugash = CASE WHEN t.tugash <= now()
THEN now() + interval '15 minutes'
ELSE t.tugash END
RETURNING son;
EXECUTE ol('kirish-hisob:dilnoza@example.com|127.0.0.1');
EXECUTE ol('kirish-hisob:dilnoza@example.com|127.0.0.1');
-- oynani "tugatamiz": tugash vaqtini o'tmishga suramiz
UPDATE tezlik_chegaralari SET tugash = now() - interval '1 second'
WHERE kalit = 'kirish-hisob:dilnoza@example.com|127.0.0.1';
EXECUTE ol('kirish-hisob:dilnoza@example.com|127.0.0.1');Natija:
PREPARE
son
-----
1
(1 row)
INSERT 0 1
son
-----
2
(1 row)
INSERT 0 1
UPDATE 1
son
-----
1
(1 row)
INSERT 0 1Birinchi chaqiruv — 1, ikkinchi — 2, oyna tugagach — yana 1. Kanon kodida oyna uzunligi parametr ($2 * interval '1 millisecond'), bu yerda soddalik uchun 15 daqiqa yozildi.
Nega "o'qib, keyin yozish" emas? Ikki parallel so'rov ikkalasi ham son = 4 ni o'qib, ikkalasi ham "5-chi, mumkin" deb qaror qilardi. ON CONFLICT qatorni qulflaydi — so'rovlar navbat bilan o'tadi (Qulflar: row, table va advisory). Kanon testi buni isbotlaydi: 50 ta parallel so'rov, limit 10 — aynan 10 tasi ruxsat oldi va 50 ta har xil son qiymati qaytdi.
5. Javob: 429 va sarlavhalar
5.1 Status va Retry-After
429 Too Many Requests — "juda ko'p so'rov". U bilan birga Retry-After: <soniya> — qachon qayta urinish mumkin. Yaxshi mijoz (masalan, bizning frontend) shu raqamni ko'rsatadi: "15 daqiqadan keyin urinib ko'ring".
5.2 RateLimit va RateLimit-Policy
Bu ikki sarlavha mijozga limit tugashidan oldin ma'lumot beradi. Holati muhim: ular hali standart emas. IETF qoralamasi (draft-ietf-httpapi-ratelimit-headers) ko'p marta o'zgargan. Kanon 07-versiya shaklini ishlatadi (2023-yil iyun):
RateLimit-Policy: 5;w=900
RateLimit: limit=5, remaining=4, reset=900Eng so'nggi 11-versiyada (2026-yil may, RFC emas) sintaksis boshqacha: RateLimit-Policy: "default";q=100;w=60 va RateLimit: "default";r=50;t=30. express-rate-limit da ham bunga standardHeaders parametri bilan qoralama versiyasi tanlanadi. Amaliy xulosa: bu sarlavhalarga qat'iy bog'lanmang — asosiy shartnoma 429 va Retry-After.
Jonli server (biz ishga tushirdik): Dilnozaning hisobiga 6 marta noto'g'ri parol, keyin to'g'ri parol:
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=899 Retry-After: 899To'g'ri parol bilan urinish (sarlavhalar qisqartirilgan):
HTTP/1.1 429 Too Many Requests
Cache-Control: no-store
RateLimit-Policy: 5;w=900
RateLimit: limit=5, remaining=0, reset=899
Retry-After: 899
Content-Type: application/problem+json; charset=utf-8
{"type":"https://vazifalar-api.example/xatolar/tezlik-cheklovi","title":"Juda ko'p so'rov","status":429,"detail":"Juda ko'p so'rov — 899 soniyadan keyin urinib ko'ring","instance":"/v1/auth/kirish","kod":"tezlik-cheklovi","sorovId":"e27ee1a2-72ee-4df2-9b9e-a2f3b1d6f9b9"}Ikki narsaga qarang:
- To'g'ri parol ham 429. Oyna ichida cheklov parolni tekshirmaydi. Aks holda hujumchi 6-urinishdan keyin javob kodlariga qarab ("429 — noto'g'ri, 200 — topdim") terishni davom ettirardi.
- Javobdagi sarlavha — eng qattiq qoidaniki: login'da uchta qoida bor, lekin
5;w=900ko'rinadi, chunki unda eng kam urinish qolgan.
6. Cheklagich buzilsa: fail open yoki fail closed
6.1 Ikki yo'l
Hisoblagich — Postgres'dagi jadval. Agar unga so'rov xato bersa (timeout — kutish muddati tugadi, qulf, jadval yo'q) — nima qilamiz?
- Fail open (ochiq qolish) — so'rovni o'tkazamiz, xatoni jurnalga yozamiz.
- Fail closed (yopiq qolish) — so'rovni rad etamiz (
503).
flowchart TD
S[So'rov] --> Q{Har qoida: UPSERT}
Q -->|baza xatosi| O[fail open: jurnal, o'tkazish]
Q -->|baza xatosi| C[fail closed: 503]
Q -->|limit oshdi| R[429 + Retry-After]
Q -->|limit ichida| H[RateLimit sarlavhasi, davom]const ISHLAYDI = { ol: async () => ({ ruxsat: true }) };
const BUZUQ = {
ol: async () => {
throw new Error("cheklov jadvali javob bermadi");
},
};
async function cheklov(ombor, rejim) {
try {
const n = await ombor.ol();
return n.ruxsat ? "o'tdi" : "429";
} catch (x) {
if (rejim === "open") return `o'tdi (jurnal: ${x.message})`;
return "503 — keyinroq urinib ko'ring";
}
}
for (const rejim of ["open", "closed"]) {
const a = await cheklov(ISHLAYDI, rejim);
const b = await cheklov(BUZUQ, rejim);
console.log(rejim.padEnd(6), "ishlaydi:", a);
console.log(rejim.padEnd(6), "buzuq: ", b);
}Konsolda:
open ishlaydi: o'tdi
open buzuq: o'tdi (jurnal: cheklov jadvali javob bermadi)
closed ishlaydi: o'tdi
closed buzuq: 503 — keyinroq urinib ko'ringNima bo'ldi? Cheklagich ishlaganda ikki rejim farq qilmaydi. Farq faqat ombor buzilganda chiqadi: "open" so'rovni o'tkazib, xatoni jurnalga yozdi, "closed" esa 503 qaytardi.
6.2 Kanon qarori va uning narxi
vazifalar-api — fail open. Kanon kodidagi izoh: "cheklov — qo'shimcha himoya, butun API'ni to'xtatmasin". Muqobil va ularning narxi:
| Fail open (kanon) | Fail closed | |
|---|---|---|
| cheklagich buzilganda | hamma kiradi, hujumchi ham | hech kim kirmaydi |
| xavfsizlik | himoya vaqtincha yo'q | himoya saqlanadi |
| mavjudlik (availability) | API ishlaydi | cheklagich xatosi = butun login to'xtadi |
| xatoni kim sezadi | faqat jurnal (cheklov_xato) — kuzatilmasa, hech kim |
hamma foydalanuvchi darhol |
| qachon to'g'ri | cheklov — qo'shimcha qatlam, asosiy himoya boshqa joyda | cheklov — asosiy himoya yoki pul bilan bog'liq |
Kanon tanlovi nega maqbul:
- Cheklagich va login bitta Postgres'da. Baza butunlay ishlamasa, login ham ishlamaydi — "fail open" faqat cheklov jadvali alohida buzilgan holatda (timeout, qulf) ahamiyatga ega.
- Cheklov — yagona himoya emas. Parol argon2id bilan, umumiy javob vaqti cheklangan, 2FA bor.
- Cheklagichdagi xato (kod yoki migratsiya) hamma foydalanuvchini kirgizmay qo'yishi — o'zimizga o'zimiz DoS qilish.
Lekin narxi ham real: buzilish paytida hujumchi cheklovsiz parol teradi. Shuning uchun cheklov_xato jurnali kuzatilishi shart (ogohlantirish, Logging asoslari) — jim fail open xavfli.
Login uchun fail closed muqobili. Agar sizning tizimingizda login eng qimmatli eshik bo'lsa (bank, to'lov), uni alohida yopish mumkin: faqat /kirish va /2fa/tekshir uchun cheklagich xatosida 503 va Retry-After qaytarish, qolgan yo'llarda — fail open. Ya'ni rejim yo'l bo'yicha tanlanadi. Pul bilan bog'liq tekshiruvlarda kanonning o'zi ham yopiq yo'lni tanlagan: CAPTCHA xizmati javob bermasa, ro'yxat rad etiladi (CAPTCHA tokenini serverda tekshirish, honeypot va SMS pumping).
Tekshirib ko'ring: Nega CAPTCHA tekshiruvida kanon "fail closed", tezlik cheklovida esa "fail open" tanladi? Bu izchilsizlik emasmi?
Javob
Yo'q — har joyda xavf va narx boshqacha. CAPTCHA ro'yxat va SMS yuborishni himoya qiladi: u o'tkazib yuborilsa, botlar hisob ochadi va SMS uchun pul ketadi — zarar to'g'ridan-to'g'ri. Uni yopish narxi kichik: ro'yxat bir necha daqiqa ishlamaydi, mavjud foydalanuvchilar kiraveradi. Login cheklovi esa qo'shimcha qatlam (argon2, 2FA qoladi), uni yopish esa hamma mavjud foydalanuvchini tashqarida qoldiradi. Qaror har eshik uchun alohida qilinadi.
7. Kutubxonalar
Kanon express-rate-limit 8.7.1 (25-qismda sinagansiz) va rate-limiter-flexible 11.2.3 ni ko'rib chiqdi va kiritmadi — o'z 60 qatorlik kodi yetdi. Ikkinchisi ko'p omborlar bilan ishlaydi (hujjat bo'yicha, kanonda sinalmagan). Katta loyihada tayyor paket afzal bo'lishi mumkin: chekka holatlar hal qilingan. Kichik loyihada esa bitta UPSERT'ni o'zingiz o'qib tushunasiz.
8. Hujumchi nigohi
| Hujumchi nima qiladi | Natija vazifalar-api da |
|---|---|
| bitta hisobga 6 marta parol | 6-chisi 429, to'g'ri parol ham 429 |
| bitta IP'dan 31 xil hisobga | 31-chisi 429 (kirish-ip) |
| qurbon hisobini bloklashga urinadi | kirish-hisob — yo'q; kirish-email — 50 ta urinish va 10 ta IP bilan ha («Halol topilma» bo'limi) |
X-Forwarded-For ni soxtalaydi |
trust proxy o'chiq — foydasi yo'q |
| 2FA kodini teradi | 5 urinish / 15 daqiqa, to'g'ri kod ham 429 |
| cheklov jadvalini buzadi | fail open — himoya yo'q, lekin jurnalda cheklov_xato |
9. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
| faqat hisob bo'yicha blok | lockout DoS | hisob + IP |
| faqat IP bo'yicha | ko'p IP'li hujum o'tadi, NAT ortidagilar jabrlanadi | bir nechta qoida |
| "o'qib, keyin yozish" hisoblagich | parallel so'rovlar limitdan oshadi | atomik UPSERT yoki INCR |
trust proxy proxy'siz yoqilgan |
X-Forwarded-For bilan aylanib o'tish |
faqat haqiqiy proxy orqasida |
| 429 da to'g'ri parolni o'tkazish | javob kodi parolni oshkor qiladi | oyna ichida hammasi 429 |
| jim fail open | buzilganini hech kim bilmaydi | jurnal + ogohlantirish |
10. Mashqlar
Mashqlar kurs/mashqlar/28/26-tezlik-cheklovi/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
- Fixed window oyna chegarasida limitning ikki barobarigacha o'tkazishi mumkin.
- Faqat email bo'yicha blok — eng xavfsiz variant.
- UNLOGGED jadval server qulaganda bo'shatiladi.
RateLimitsarlavhasi — RFC standarti.- Fail open bo'lsa, cheklagich buzilganda hujumchi ham cheklovsiz ishlaydi.
Yechim
- To'g'ri — «Bitta ketma-ketlik, uchta javob» bo'limidagi
++++++++++. - Noto'g'ri — lockout DoS: hujumchi qurbonni bloklaydi.
- To'g'ri — WAL'ga yozilmagani uchun crash recovery'da tozalanadi.
- Noto'g'ri — IETF qoralamasi (11-versiya, 2026-yil may), sintaksisi o'zgarib turadi.
- To'g'ri — shuning uchun jurnal kuzatiladi.
2-mashq (o'rta): sliding window counter
Yana bir algoritm — sliding window counter. Joriy oyna hisobiga oldingi oyna hisobining "erimagan" qismi qo'shiladi: taxmin = oldingi × (1 − o'tgan_ulush) + joriy. slidingCounter(limit, oyna) ni yozing va limit 5 / 60 s bilan «Bitta ketma-ketlik, uchta javob» bo'limidagi ketma-ketlikda, keyin 90, 91, 92-soniyalarda sinang.
Yechim
// sliding window counter: oldingi oyna hisobi "eriydi"
function slidingCounter(limit, oyna) {
let joriyBoshi = 0;
let joriy = 0;
let oldingi = 0;
return (t) => {
const boshi = t - (t % oyna);
if (boshi !== joriyBoshi) {
oldingi = boshi - joriyBoshi === oyna ? joriy : 0;
joriy = 0;
joriyBoshi = boshi;
}
const otgan = (t - boshi) / oyna;
const taxmin = oldingi * (1 - otgan) + joriy;
if (taxmin >= limit) return false;
joriy += 1;
return true;
};
}
const ol = slidingCounter(5, 60);
const SOROVLAR = [55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 90, 91, 92];
for (const t of SOROVLAR) {
console.log(String(t).padStart(3), ol(t) ? "ruxsat" : "429");
}Konsolda:
55 ruxsat
56 ruxsat
57 ruxsat
58 ruxsat
59 ruxsat
60 429
61 ruxsat
62 429
63 429
64 429
90 ruxsat
91 ruxsat
92 42960-soniyada taxmin 5 × 1 + 0 = 5 — rad. 61-da 5 × 59/60 ≈ 4.92 — limitdan kichik, o'tdi. Bu algoritmning tabiati: u taxminiy, chunki oldingi oynadagi so'rovlar tekis taqsimlangan deb hisoblaydi. Shunga qaramay, 10 soniyada 6 ta — fixed window'ning 10 tasidan ancha yaxshi, xotira esa ikki son. 90-soniyada oldingi oynaning yarmi "erigan" — yangi so'rovlarga joy ochildi.
3-mashq (qiyin): bir nechta qoida va eng qattiq sarlavha
Kanonning cheklov() g'oyasini toza JS'da yozing. Birinchi funksiya — xotiraCheklagich(soat). U (kalit, limit, oynaS) => { ruxsat, qoldi, reset } qaytaradi, soat soxta. Ikkinchisi — tekshir(ol, qoidalar, sorov). U har qoidani hisoblaydi, eng kam qoldi li (yoki rad etgan) qoidani tanlaydi va "<status> <limit>;w=<oyna> remaining=<qoldi> reset=<reset>" qaytaradi: ruxsat bo'lsa status 401 (parol noto'g'ri deb hisoblaymiz), aks holda 429. Qoidalar: IP — 30 / 900 s, hisob+IP — 3 / 900 s. Har 10 soniyada bitta so'rov — 4 ta, keyin 900 soniya kutib yana bitta.
Yechim
function xotiraCheklagich(soat) {
const m = new Map();
return (kalit, limit, oynaS) => {
const hozir = soat();
const e = m.get(kalit);
const y = e === undefined || e.tugash <= hozir
? { son: 1, tugash: hozir + oynaS }
: { son: e.son + 1, tugash: e.tugash };
m.set(kalit, y);
return {
ruxsat: y.son <= limit,
qoldi: Math.max(0, limit - y.son),
reset: y.tugash - hozir,
};
};
}
function tekshir(ol, qoidalar, sorov) {
let eng;
for (const q of qoidalar) {
const kalit = `${q.nom}:${q.kalit(sorov)}`;
const n = { ...ol(kalit, q.limit, q.oyna), q };
const qattiqroq = eng === undefined || n.qoldi < eng.qoldi;
if (qattiqroq || !n.ruxsat) eng = n;
if (!n.ruxsat) break;
}
const status = eng.ruxsat ? 401 : 429;
return `${status} ${eng.q.limit};w=${eng.q.oyna} ` +
`remaining=${eng.qoldi} reset=${eng.reset}`;
}
let hozir = 0;
const ol = xotiraCheklagich(() => hozir);
const QOIDALAR = [
{ nom: "ip", limit: 30, oyna: 900, kalit: (s) => s.ip },
{
nom: "hisob", limit: 3, oyna: 900,
kalit: (s) => `${s.email}|${s.ip}`,
},
];
const SOROV = { ip: "203.0.113.9", email: "aziz@example.com" };
for (let i = 1; i <= 4; i++) {
hozir += 10;
console.log(i, tekshir(ol, QOIDALAR, SOROV));
}
hozir += 900;
console.log(5, tekshir(ol, QOIDALAR, SOROV));Konsolda:
1 401 3;w=900 remaining=2 reset=900
2 401 3;w=900 remaining=1 reset=890
3 401 3;w=900 remaining=0 reset=880
4 429 3;w=900 remaining=0 reset=870
5 401 3;w=900 remaining=2 reset=900Sarlavhada doim hisob+IP qoidasi ko'rinadi: unda qoldi IP qoidasidagidan (29, 28 …) kam. reset kamayib boradi — fixed window'da oyna birinchi so'rovdan boshlanadi va siljimaydi. 900 soniyadan keyin oyna tugadi va hisob noldan boshlandi. Kanon kodi xuddi shunday ishlaydi, faqat ol asinxron va Postgres'ga boradi.
11. Loyiha qadami: tezlik cheklovi
11.1 Migratsiya va Prisma
sql/122-tezlik-chegaralari.sql — «UNLOGGED jadval» bo'limidagi jadval; xuddi shu SQL — prisma/migrations/20261028130000_tezlik_chegaralari/migration.sql. schema.prisma ga model:
/// tezlik cheklovi hisoblagichlari — UNLOGGED jadval (Prisma sxemada
/// ifodalanmaydi, SQL migratsiyada)
model TezlikChegarasi {
kalit String @id(map: "tezlik_chegaralari_pkey")
son Int
tugash DateTime @db.Timestamptz(6)
@@index([tugash], map: "tezlik_chegaralari_tugash_idx")
@@map("tezlik_chegaralari")
}Prisma UNLOGGED ni bilmaydi — shuning uchun jadval SQL migratsiyada yaratiladi, model esa faqat sxema farqi bo'lmasligi uchun.
11.2 src/umumiy/cheklov.ts
Modulning asosiy qismi. Telefon uchun qatorlarni bo'ldik va izohlarni qisqartirdik, kod ma'nosi kanondagidek (bo'lingan nusxa tip, lint va testlardan o'tdi — biz tekshirdik). Ikki yordamchi klass matnda tasvirlangan:
// cheklov.ts — tezlik cheklovi (rate limiting). Algoritm — fixed
// window (oyna boshida hisob 0); kalit — kim/nima bo'yicha (IP,
// hisob+IP, foydalanuvchi). Ombor: Postgres (UNLOGGED jadval, bitta
// atomik UPSERT) yoki xotira (dev/test, bitta jarayon). Sarlavhalar —
// IETF draft-ietf-httpapi-ratelimit-headers: RateLimit-Policy va
// RateLimit; 429 da Retry-After (soniya). Fixed window kamchiligi:
// oyna chegarasida 2× portlash — login uchun maqbul (limit kichik,
// oyna uzun).
import type { Pool } from "pg";
import type { Request, RequestHandler } from "express";
import { IlovaXatosi } from "./xatolar.ts";
export type Natija = {
ruxsat: boolean;
son: number;
qoldi: number;
tiklanishS: number;
};
export interface Cheklagich {
/** hisoblagichni 1 ga oshiradi (limitdan oshsa ruxsat: false) */
ol(kalit: string, limit: number, oynaMs: number): Promise<Natija>;
/** muvaffaqiyatdan keyin (to'g'ri login) — hisobni tozalash */
tozala(kalit: string): Promise<void>;
}
const natija = (
son: number,
limit: number,
tugashMs: number,
hozirMs: number,
): Natija => ({
ruxsat: son <= limit,
son,
qoldi: Math.max(0, limit - son),
tiklanishS: Math.max(1, Math.ceil((tugashMs - hozirMs) / 1000)),
});
export class PgCheklagich implements Cheklagich {
readonly #pool: Pool;
constructor(pool: Pool) {
this.#pool = pool;
}
async ol(
kalit: string,
limit: number,
oynaMs: number,
): Promise<Natija> {
// Bitta so'rov, atomik (qator qulfi ON CONFLICT ichida): oyna
// tugagan bo'lsa — 1 dan, aks holda +1
const { rows: [q] } = await this.#pool.query<{
son: number; tugash: Date; hozir: Date;
}>(
`INSERT INTO tezlik_chegaralari AS t (kalit, son, tugash)
VALUES ($1, 1, now() + $2 * interval '1 millisecond')
ON CONFLICT (kalit) DO UPDATE SET
son = CASE WHEN t.tugash <= now() THEN 1 ELSE t.son + 1 END,
tugash = CASE WHEN t.tugash <= now()
THEN now() + $2 * interval '1 millisecond'
ELSE t.tugash END
RETURNING son, tugash, now() AS hozir`,
[kalit.slice(0, 300), oynaMs],
);
if (q === undefined) {
throw new Error("cheklov: UPSERT qator qaytarmadi");
}
if (Math.random() < 0.01) {
void this.#pool.query(
"DELETE FROM tezlik_chegaralari " +
"WHERE tugash < now() - interval '1 hour'",
).catch(() => undefined);
}
const tugash = q.tugash.getTime();
return natija(q.son, limit, tugash, q.hozir.getTime());
}
async tozala(kalit: string): Promise<void> {
await this.#pool.query(
"DELETE FROM tezlik_chegaralari WHERE kalit = $1",
[kalit.slice(0, 300)],
);
}
}
// XotiraCheklagich va Cheklovsiz — pastdagi matnda
export class CheklovXatosi extends IlovaXatosi {
constructor(tiklanishS: number) {
super(
429,
`Juda ko'p so'rov — ${String(tiklanishS)} soniyadan keyin ` +
"urinib ko'ring",
{
kod: "tezlik-cheklovi",
sarlavha: "Juda ko'p so'rov",
httpSarlavhalar: { "Retry-After": String(tiklanishS) },
},
);
}
}
export type Qoida = {
nom: string;
limit: number;
oynaMs: number;
kalit: (req: Request) => string | undefined;
};
/**
* Bir nechta qoida: hammasi hisoblanadi, birortasi oshsa — 429.
* Javobga eng qattiq qoida sarlavhasi. Kalit undefined — qoida
* o'tkaziladi.
* Cheklagich (baza) ishlamasa — so'rov O'TKAZILADI (fail open) va
* log'ga: cheklov — qo'shimcha himoya, butun API'ni to'xtatmasin.
*/
export function cheklov(
c: Cheklagich,
qoidalar: readonly Qoida[],
xatoLog?: (x: unknown) => void,
): RequestHandler {
return async (req, res, next) => {
let eng: (Natija & { q: Qoida }) | undefined;
try {
for (const q of qoidalar) {
const k = q.kalit(req);
if (k === undefined) continue;
const n = await c.ol(`${q.nom}:${k}`, q.limit, q.oynaMs);
if (eng === undefined || n.qoldi < eng.qoldi || !n.ruxsat) {
eng = { ...n, q };
}
if (!n.ruxsat) break;
}
} catch (x) {
xatoLog?.(x);
next();
return;
}
if (eng !== undefined) {
const oynaS = Math.round(eng.q.oynaMs / 1000);
res.set(
"RateLimit-Policy",
`${String(eng.q.limit)};w=${String(oynaS)}`,
);
res.set(
"RateLimit",
`limit=${String(eng.q.limit)}, ` +
`remaining=${String(eng.qoldi)}, ` +
`reset=${String(eng.tiklanishS)}`,
);
if (!eng.ruxsat) throw new CheklovXatosi(eng.tiklanishS);
}
next();
};
}Ko'rsatilmagan ikki klass: XotiraCheklagich — 3-mashqdagi xotiraCheklagich bilan bir xil mantiq (Map va soxta soat, dev va testlar uchun); Cheklovsiz — doim ruxsat: true qaytaradi (cheklovga urilmasligi kerak bo'lgan testlar uchun).
Uch narsaga e'tibor bering:
kalit.slice(0, 300)— kalit foydalanuvchi kiritgan emaildan yasaladi. Uzunlik cheklovi bo'lmasa, hujumchi 1 MB lik "email" bilan jadvalni to'ldirardi (bazada hamCHECKbor — chuqur himoya).Math.random() < 0.01— har yuzinchi so'rov eski qatorlarni tozalaydi. Alohida fon vazifasi kerak emas; natijasini kutmaymiz (void).catchfaqat cheklagich xatosini ushlaydi.CheklovXatosi(429)trydan tashqarida tashlanadi — aks holda limit oshganda ham fail open ishlab ketardi.
11.3 Qoidalar: src/auth/marshrut.ts
Auth router boshida (telefon uchun bo'lingan; ikki kalit funksiyasi — hisobIp, mfaHisob — kanonda qator ichida, nom berib ajratdik):
// Tezlik cheklovlari. Login: IP bo'yicha (bitta IP — ko'p hisob:
// credential stuffing), hisob+IP (bitta hisobga terish — brute
// force; IP qo'shilgani uchun hujumchi boshqa joydan qurbon
// hisobini "qulflab" qo'ya olmaydi) va email bo'yicha kunlik
// (taqsimlangan hujum). Xat/SMS yuboruvchi yo'llar — IP bo'yicha
// soatiga.
const D15 = 15 * 60_000, SOAT = 3_600_000, KUN = 86_400_000;
const ip = (req: Request) => req.ip ?? "noma'lum";
const email = (req: Request): string | undefined => {
const e: unknown =
(req.body as { email?: unknown } | undefined)?.email;
return typeof e === "string"
? e.trim().toLowerCase().slice(0, 254)
: undefined;
};
// Cheklagich xatosi (baza) — so'rov o'tadi (fail open), jurnalga
const xatoLog = (x: unknown): void => {
const xato = x instanceof Error ? x.message.slice(0, 200) : "?";
void xizmat.hodisa({ tur: "cheklov_xato", tafsilot: { xato } })
.catch(() => undefined);
};
const hisobIp = (req: Request) => {
const e = email(req);
return e === undefined ? undefined : `${e}|${ip(req)}`;
};
r.use("/kirish", cheklov(d.cheklagich, [
{ nom: "kirish-ip", limit: 30, oynaMs: D15, kalit: ip },
{ nom: "kirish-hisob", limit: 5, oynaMs: D15, kalit: hisobIp },
{ nom: "kirish-email", limit: 50, oynaMs: KUN, kalit: email },
], xatoLog));
const yuborish = [
"/royxat", "/parol-tiklash/sorov", "/email-tasdiq/qayta",
"/otp/yubor", "/telefon",
];
for (const y of yuborish) {
r.use(y, cheklov(d.cheklagich, [
{ nom: "yuborish-ip", limit: 10, oynaMs: SOAT, kalit: ip },
], xatoLog));
}
const tekshiruv = [
"/otp/kirish", "/email-tasdiq", "/parol-tiklash", "/telegram",
"/passkey/kirish",
];
for (const y of tekshiruv) {
r.use(y, cheklov(d.cheklagich, [
{ nom: "tekshiruv-ip", limit: 30, oynaMs: D15, kalit: ip },
], xatoLog));
}
// 2FA: bitta hisob (mfa cookie'dagi id) — 15 daqiqada 5 urinish
const mfaHisob = (req: Request) => {
const k = d.mfa.kutish.ol(req);
return k === undefined ? undefined : String(k.f);
};
r.use("/2fa/tekshir", cheklov(d.cheklagich, [
{ nom: "2fa", limit: 5, oynaMs: D15, kalit: mfaHisob },
], xatoLog));email() emailni xuddi login kabi normallaydi (trim, kichik harf) — aks holda Aziz@Example.com va aziz@example.com ikki xil hisoblagich bo'lardi va hujumchi limitni registr bilan aylanib o'tardi. 2FA qoidasi bilan Ikki faktorli autentifikatsiya va passkeys darsida ochiq qolgan cheklov yopildi: 6 raqamli kodni endi cheklovsiz terib bo'lmaydi.
Muvaffaqiyatli login'dan keyin, xuddi shu faylda:
// muvaffaqiyat — shu hisob+IP hisobi noldan
await d.cheklagich.tozala(
`kirish-hisob:${body.email}|${req.ip ?? "noma'lum"}`,
);11.4 Ma'lumot yo'llari va ulash
src/ilova.ts da ma'lumot yo'llari (/v1/vazifalar, /v1/teglar) foydalanuvchi bo'yicha daqiqasiga 600 so'rov oladi — kirish tekshiruvidan keyin, chunki kalit — req.egasi:
const egasiniAniqla: express.RequestHandler[] =
auth === undefined
? [ozgarmasEgasi(egasiId)]
: [kirishKerak, cheklov(auth.cheklagich, [
{ nom: "api", limit: 600, oynaMs: 60_000,
kalit: (req) => String(req.egasi) },
])];src/server.ts da auth bog'liqliklariga cheklagich: new PgCheklagich(pool), AuthBogliqliklari tipiga cheklagich: Cheklagich maydoni, jurnal turlariga "cheklov_xato" qo'shiladi. Testlardagi umumiy yordamchi Cheklovsiz ni beradi — boshqa testlar ko'p login qiladi va cheklovga urilmasligi kerak.
11.5 Testlar
test/cheklov.test.ts — oltita test. Ulardan biri — brute force (telefon uchun bo'lingan):
test("brute force: bitta hisob+IP — 5 ta noto'g'ri, 6-chisi " +
"429 + Retry-After + RateLimit; TO'G'RI parol ham 429 (oyna " +
"ichida); boshqa hisob — ochiq", async () => {
const { d, app } = ilova(pg);
await hisobOch(app, d, "aziz@example.com");
await hisobOch(app, d, "dilnoza@example.com");
for (let i = 0; i < 5; i++) {
const r = await xato(app).expect(401);
assert.match(String(r.headers["ratelimit"]),
/^limit=\d+, remaining=\d+, reset=\d+$/);
}
const r = await xato(app).expect(429);
const kod = (r.body as { kod: string }).kod;
assert.equal(kod, "tezlik-cheklovi");
assert.ok(Number(r.headers["retry-after"]) > 800);
assert.equal(r.headers["ratelimit-policy"], "5;w=900");
const kirish = (email: string) => request(app)
.post("/v1/auth/kirish").send({ email, parol: PAROL });
await kirish("aziz@example.com").expect(429);
await kirish("dilnoza@example.com").expect(200);
});Oxirgi test fail open'ni hujjatlaydi: ol doim Promise.reject qaytaradigan buzuq cheklagich bilan login 200 va jurnalda cheklov_xato yozuvi bo'lishi kerak. Kimdir keyinroq fail closed'ga o'tkazsa, test yiqiladi va u ongli qaror qilishga majbur bo'ladi.
Faqat shu faylni ishga tushiramiz (biz ishga tushirdik: kanon nusxasi va yuqoridagi bo'lingan fayllar, Node 24.21.0, lokal PostgreSQL 17.6):
node --test test/cheklov.test.ts✔ XotiraCheklagich: oyna ichida limit, oyna tugagach — noldan; tozala (4.4595ms)
✔ PgCheklagich: 50 parallel so'rov, limit 10 — AYNAN 10 ta ruxsat (UPSERT atomik) (446.7172ms)
▶ API
✔ brute force: bitta hisob+IP — 5 ta noto'g'ri, 6-chisi 429 + Retry-After + RateLimit; TO'G'RI parol ham 429 (oyna ichida); boshqa hisob — ochiq (892.4203ms)
✔ muvaffaqiyatli kirish hisob+IP hisobini tozalaydi; X-Forwarded-For soxtasi trust proxy'siz yordam bermaydi, trust proxy bilan — bypass (faqat proxy orqasida yoqing!) (1241.0743ms)
✔ 2FA: bitta hisobga 5 urinish/15 daqiqa — 6-chi to'g'ri kod ham 429 (501.5792ms)
✔ ma'lumot yo'llari: foydalanuvchi bo'yicha (600/daqiqa — testda kichik limit bilan emas, sarlavha tekshiruvi); cheklagich xatosi — fail open + jurnal (491.2978ms)
✔ API (3127.6494ms)
ℹ tests 6
ℹ suites 1
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6082.948Testlar uchun lokal Postgres'da auth test bazasi kerak (TEST_AUTH_DATABASE_URL); u bo'lmasa, Postgres'li to'plam "o'tkazildi" deb chiqadi.
11.6 Tekshiruv va commit
Holat tekshiruvi (kanon, 2026-10-28, PostgreSQL 18.6): tip va lint xatosiz, testlar 216 (215 + 1 o'tkazilgan) — oltita yangi. Testcontainers 10/10, brauzer testlari 10/10, migratsiyalar va schema.prisma orasida farq yo'q.
M="feat(api): tezlik cheklovi — Postgres hisoblagichlari (UNLOGGED,"
M+=" atomik UPSERT), login (IP, hisob+IP, email), 2FA, xat/SMS"
M+=" yo'llari, foydalanuvchi bo'yicha; RateLimit sarlavhalari;"
M+=" db: 122"
git add src test sql prisma
git commit -m "$M"12. Real ishda
- Qatlamlar: CDN yoki WAF (Cloudflare va boshqalar) — IP bo'yicha qo'pol cheklov; nginx
limit_req— proxy darajasida; ilova — hisob va biznes mantiq bo'yicha (faqat ilova "bu Azizning hisobi" ekanini biladi). - Monitoring:
429lar soni vacheklov_xato— alohida grafik. - Intervyuda: "rate limit algoritmlarini solishtiring", "login uchun qanday kalit?", "lockout DoS nima?", "limiter buzilsa nima qilasiz?".
Xulosa
- Tezlik cheklovi qimmat amallarni (login, 2FA, SMS) himoya qiladi; DDoS'ni tarmoq darajasi to'xtatadi.
- Fixed window chegarada 2× o'tkazadi — login uchun maqbul, chunki bitta qator va bitta so'rov.
- Asosiy qaror — kalit: hisob + IP lockout'siz brute force'ni to'xtatadi; kunlik email qoidasi taqsimlangan hujumni ushlaydi, lekin lockout'ni 10 ta IP evaziga qaytaradi.
- Hisoblagich — atomik:
INSERT … ON CONFLICT DO UPDATE; UNLOGGED jadval — tez, qulaganda nolga. - Javob:
429+Retry-After;RateLimitsarlavhalari — hali qoralama. - Cheklagich buzilsa
vazifalar-api— fail open (mavjudlik), lekin jurnal bilan; login uchun fail closed — yo'l bo'yicha muqobil.
Keyingi dars: Backend'dagi injection va mass assignment — kiruvchi ma'lumot kod yoki so'rovga aylanib qoladigan barcha yo'llar va vazifalar-api ning qaysi qatlami nimani ushlab qolishi.
Manbalar
- IETF draft-ietf-httpapi-ratelimit-headers: 07-versiya (2023-06-24, kanon formati) https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-07 va 11-versiya (2026-05-23, faol qoralama) https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/ (2026-10-28 da o'qildi)
- RFC 6585 — 429 Too Many Requests: https://www.rfc-editor.org/rfc/rfc6585
- RFC 9110 —
Retry-After: https://www.rfc-editor.org/rfc/rfc9110#name-retry-after - OWASP Authentication Cheat Sheet (hisobni bloklash va lockout): https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Credential Stuffing Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html
- PostgreSQL 18:
CREATE TABLE … UNLOGGEDhttps://www.postgresql.org/docs/18/sql-createtable.html,INSERT … ON CONFLICThttps://www.postgresql.org/docs/18/sql-insert.html - RFC 5737 — hujjatlar uchun IPv4 manzillari: https://www.rfc-editor.org/rfc/rfc5737
- Kanon (IlmHamroh, 2026-10-27/28): hujum yuzasi tajribasi (300 × 200), holat 26 (
cheklov.ts,122,cheklov.test.ts, tekshiruv 216). Darsdagi brauzer misollari, SQL (PostgreSQL 17.6), jonlicurl,cheklov.test.tsva kunlik qoida tajribasi — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!