Mundarija (25)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 JWT'ning muammosi
- 1.2 Ikki token
- 2. Rotatsiya va token oilasi
- 2.1 Qoida
- 2.2 Brauzerda sinab ko'ring
- 3. Bazada: kanon tajribasi
- 3.1 Jadval va yangilash tranzaksiyasi
- 3.2 Kanon kodi
- 4. Kanon ssenariylari
- 5. Poyga: soxta-musbat chiqarish
- 6. Bekor qilish va "barcha qurilmalardan chiqish"
- 7. Narx: kanon o'lchovi
- 8. Refresh qayerda saqlanadi
- 9. Hujumchi nigohi
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): bitta yangilash qulfi
- 3-mashq (qiyin): grace oynasi
- 12. Loyiha qadami: strategiya kanonga kirmadi
- 13. Real ishda
- Xulosa
- Manbalar
Access va refresh token strategiyasi: rotatsiya va o'g'irlikni aniqlash
Qisqacha: JWT'ni bekor qilib bo'lmaydi — shuning uchun uni qisqa qilishadi (15 daqiqa) va yonida uzoq muddatli refresh token beriladi. Refresh har ishlatilganda almashtiriladi (rotatsiya); eski refresh yana kelsa — bu o'g'irlik belgisi, butun token oilasi bekor qilinadi. Kanon buni haqiqiy kod va Postgres bilan sinadi: o'g'irlik aniqlandi, lekin ikki narsa ochildi. Birinchisi: oila bekor bo'lgandan keyin ham 15 daqiqalik access amal qilishda davom etdi. Ikkinchisi: bitta foydalanuvchining ikki tabi bir vaqtda yangilaganda, rotatsiya buni o'g'irlik deb bildi va foydalanuvchini tizimdan chiqarib yubordi.
Bu darsda
- Nega ikki xil token kerakligini va har birining muddatini tushuntirasiz.
- Refresh rotatsiyasi, token oilasi va reuse detection'ni JS va SQL'da yozasiz.
- Kanon o'lchagan to'rt ssenariyni (o'g'irlik, teskari tartib, poyga, hamma qurilmadan chiqish) tahlil qilasiz.
- Poygaga qarshi ikki yechimni — frontend qulfi va grace oynasini — yozasiz.
- Nega bu strategiya
vazifalar-apiga kirmaganini o'lchovlar bilan bilib olasiz.
Oldin bilishingiz kerak: JWT anatomiyasi, Session-based auth, Qulflar: row, table va advisory, Autentifikatsiyali so'rovlar va token yangilash.
11-qismda frontend tomonidan "access token eskirdi — refresh bilan yangisini olamiz" degan oqimni yozgansiz. U yerda server qora quti edi. Bugun o'sha qutining ichiga kiramiz: server refresh tokenni qanday saqlaydi, qanday almashtiradi va o'g'irlangan tokenni qanday sezadi.
1. Nega bu kerak?
1.1 JWT'ning muammosi
Oldingi darsda ko'rdik: JWT'ni server bazasiz tekshiradi — bu uning kuchi. Lekin xuddi shu sababli uni bekor qilib bo'lmaydi: server tokenni eslamaydi, demak "bu token endi yaroqsiz" deb yoza oladigan joy yo'q. Token o'g'irlansa, u exp gacha ishlaydi.
Ikkita yomon variant bor:
- Uzoq JWT (30 kun) — foydalanuvchi uchun qulay, o'g'irlansa 30 kunlik ochiq eshik.
- Qisqa JWT (15 daqiqa) — xavfsizroq, lekin foydalanuvchi har 15 daqiqada parol kiritadi.
1.2 Ikki token
Yechim — ikki xil token, ikki xil vazifa:
| Access token | Refresh token | |
|---|---|---|
| vazifasi | har API so'rovida "men kimman" | faqat yangi access olish |
| muddati | qisqa (kanon tajribasida 15 daqiqa) | uzun (30 kun) |
| ko'rinishi | JWT (EdDSA, iss, aud, jti) |
tasodifiy 32 bayt (opaque — ichida ma'no yo'q) |
| tekshirish | imzo, bazasiz | bazada qidirish |
| qayerga boradi | har so'rov sarlavhasida | faqat bitta endpoint'ga |
| bekor qilish | mumkin emas, muddatini kutish | darhol (bazada) |
G'oya: tez-tez yuradigan token qisqa yashaydi; uzoq yashaydigan token kam yuradi va server nazoratida.
Ikki token birga qanday ishlashi — bitta rasmda:
sequenceDiagram
participant F as Frontend
participant A as API
participant DB as Postgres
F->>A: GET /v1/vazifalar (access)
A-->>F: 200 (imzo tekshirildi, bazasiz)
Note over F,A: 15 daqiqa o'tdi
F->>A: GET /v1/vazifalar (eskirgan access)
A-->>F: 401
F->>A: POST /v1/auth/yangila (refresh)
A->>DB: refresh hash, FOR UPDATE, ishlatilgan
A-->>F: yangi access + yangi refresh
F->>A: GET /v1/vazifalar (yangi access)
A-->>F: 200Bazaga faqat yangilash so'rovi boradi — oddiy so'rovlar imzo bilan tekshiriladi.
Refresh — JWT emas, oddiy tasodifiy satr. Nega? Uni baribir bazadan tekshiramiz (bekor qilish uchun), demak ichiga ma'lumot yozishning foydasi yo'q. Bazada esa — oldingi darsdagi sessiya tokeni kabi — faqat SHA-256 hash.
2. Rotatsiya va token oilasi
2.1 Qoida
Refresh rotation (rotatsiya): har yangilashda server yangi refresh beradi va eskisini "ishlatilgan" deb belgilaydi. Bitta login'dan tug'ilgan hamma refresh'lar bitta oilaga (token family) tegishli.
Endi o'g'irlikni sezish mumkin. Refresh — bir martalik. Agar ishlatilgan refresh yana kelsa, ikki kishidan biri — o'g'ri: yoki foydalanuvchi, yoki hujumchi eski nusxani ishlatyapti. Server kim ekanini bilmaydi, shuning uchun butun oilani bekor qiladi. Bu reuse detection (qayta ishlatishni aniqlash).
stateDiagram-v2
[*] --> Faol: login yoki yangilash
Faol --> Ishlatilgan: yangilashda almashtirildi
Faol --> Bekor: chiqish yoki oila bekor
Ishlatilgan --> Bekor: yana keldi, butun oila bekor
Faol --> Eskirgan: 30 kun o'tdi
Bekor --> [*]
Eskirgan --> [*]2.2 Brauzerda sinab ko'ring
Rotatsiya va reuse detection — xotirada, Web Crypto hash bilan. Tokenlar namoyish uchun token-1, token-2 kabi o'qiladigan nomlar (haqiqiy kodda — 32 tasodifiy bayt):
const enc = new TextEncoder();
const hex = (b) => Array.from(new Uint8Array(b),
(x) => x.toString(16).padStart(2, "0")).join("");
const hash = async (t) =>
hex(await crypto.subtle.digest("SHA-256", enc.encode(t)));
let n = 0;
const yangiToken = () => `token-${++n}`; // namoyish: o'qiladigan nom
const baza = new Map(); // hash → { oila, ishlatilgan, bekor }
async function kirish(oila) {
const t = yangiToken();
baza.set(await hash(t), { oila, ishlatilgan: false, bekor: false });
return t;
}
async function yangila(t) {
const h = await hash(t);
const yangi = yangiToken();
const yh = await hash(yangi);
// shu yerdan await yo'q — bu qism bo'linmaydi (DB'da: tranzaksiya)
const q = baza.get(h);
if (!q || q.bekor) return "bekor yoki noma'lum";
if (q.ishlatilgan) {
let soni = 0;
for (const x of baza.values()) {
if (x.oila === q.oila && !x.bekor) {
x.bekor = true;
soni++;
}
}
return `QAYTA ISHLATILDI — oila bekor (${soni})`;
}
q.ishlatilgan = true;
baza.set(yh, { oila: q.oila, ishlatilgan: false, bekor: false });
return yangi;
}
let joriy = await kirish("A");
const ogirlangan = joriy; // hujumchi nusxa oldi
joriy = await yangila(joriy); // foydalanuvchi yangiladi
console.log(joriy);
console.log(await yangila(ogirlangan)); // hujumchi urinadi
console.log(await yangila(joriy)); // foydalanuvchi ham chiqdi
// poyga: BIR foydalanuvchining 2 ta tabi bir vaqtda yangilaydi
const t = await kirish("B");
const [a, b] = await Promise.all([yangila(t), yangila(t)]);
console.log("tab A:", a, "| tab B:", b);
console.log("A ning yangi tokeni:", await yangila(a));Konsolda:
token-2
QAYTA ISHLATILDI — oila bekor (2)
bekor yoki noma'lum
tab A: token-6 | tab B: QAYTA ISHLATILDI — oila bekor (2)
A ning yangi tokeni: bekor yoki noma'lumBirinchi uch qator — reuse detection ishladi. Hujumchi o'g'irlangan (allaqachon ishlatilgan) token-1 bilan keldi — oila bekor qilindi. Foydalanuvchining yangi token-2 si ham endi ishlamaydi: unga qayta login kerak. Bu narx ataylab to'lanadi — hujumchi ham chiqarib yuborildi.
Oxirgi ikki qator — poyga, u haqda pastroqda batafsil. Va bitta nozik joy: yangila dagi izohga qarang. Tekshirish va yozish orasida await bo'lsa, ikkinchi chaqiruv o'rtaga "kirib" qoladi. Biz hash'larni oldindan hisoblab, keyingi qismni bo'linmas qildik. Bazada bu vazifani tranzaksiya va FOR UPDATE bajaradi.
3. Bazada: kanon tajribasi
3.1 Jadval va yangilash tranzaksiyasi
Kanon rotatsiyani haqiqiy Postgres bilan yozdi (jose EdDSA access, 15 daqiqa; refresh — 32 bayt, bazada SHA-256, 30 kun). Tajriba jadvali:
CREATE TABLE refresh_tokenlar (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
foydalanuvchi_id bigint NOT NULL
REFERENCES foydalanuvchilar (id) ON DELETE CASCADE,
oila uuid NOT NULL,
token_hash bytea NOT NULL UNIQUE,
yaratilgan timestamptz NOT NULL DEFAULT now(),
tugash timestamptz NOT NULL,
ishlatilgan timestamptz,
bekor timestamptz
);
CREATE INDEX ON refresh_tokenlar (oila);Natija:
CREATE TABLE
CREATE INDEXBu va keyingi so'rovlarni kanon sxemasi (holat 10) qurilgan lokal bazada bajardik (biz ishga tushirdik, PostgreSQL 17.6). oila — bitta login'dan tug'ilgan zanjir, ishlatilgan — rotatsiyada almashtirilgan payt, bekor — oila bekor yoki chiqish.
Login — oilaning birinchi tokeni:
INSERT INTO refresh_tokenlar
(foydalanuvchi_id, oila, token_hash, tugash)
VALUES (1, 'a0000000-0000-4000-8000-000000000001',
sha256('r1'::bytea), now() + interval '30 days');Natija:
INSERT 0 1Yangilash — bitta tranzaksiyada: tokenni qulflab o'qish, ishlatilgan deb belgilash, yangisini yozish:
BEGIN;
SELECT id, oila, ishlatilgan IS NOT NULL AS ishlatilgan,
bekor IS NOT NULL AS bekor
FROM refresh_tokenlar
WHERE token_hash = sha256('r1'::bytea)
FOR UPDATE;
UPDATE refresh_tokenlar SET ishlatilgan = now() WHERE id = 1;
INSERT INTO refresh_tokenlar
(foydalanuvchi_id, oila, token_hash, tugash)
VALUES (1, 'a0000000-0000-4000-8000-000000000001',
sha256('r2'::bytea), now() + interval '30 days');
COMMIT;Natija:
BEGIN
id | oila | ishlatilgan | bekor
----+--------------------------------------+-------------+-------
1 | a0000000-0000-4000-8000-000000000001 | f | f
(1 row)
UPDATE 1
INSERT 0 1
COMMITFOR UPDATE (Qulflar) — shu qatorni tranzaksiya oxirigacha qulflaydi. Xuddi shu refresh bilan ikkinchi so'rov kelsa, u kutadi, birinchisi COMMIT qilgach esa qatorni allaqachon "ishlatilgan" holatda ko'radi. Qulfsiz ikkalasi ham "faol" ni ko'rib, ikkita yangi token chiqarardi — oilada ikki shox paydo bo'lardi va reuse detection buziladi.
Endi eski r1 yana keldi:
SELECT id, ishlatilgan IS NOT NULL AS ishlatilgan
FROM refresh_tokenlar WHERE token_hash = sha256('r1'::bytea);Natija:
id | ishlatilgan
----+-------------
1 | t
(1 row)Ishlatilgan — demak o'g'irlik belgisi. Butun oila:
UPDATE refresh_tokenlar SET bekor = now()
WHERE oila = 'a0000000-0000-4000-8000-000000000001'
AND bekor IS NULL;Natija:
UPDATE 2SELECT id, ishlatilgan IS NOT NULL AS ishlatilgan,
bekor IS NOT NULL AS bekor
FROM refresh_tokenlar ORDER BY id;Natija:
id | ishlatilgan | bekor
----+-------------+-------
1 | t | t
2 | f | t
(2 rows)r2 hech qachon ishlatilmagan, lekin bekor — u ham o'sha oilada.
3.2 Kanon kodi
Kanon tajribasidagi yangila() (Node, pg) — telefon uchun bo'lingan; ma'nosi aynan:
/** Rotatsiya: eski refresh "ishlatilgan", yangisi o'sha oilada.
* Ishlatilgan token yana kelsa — o'g'irlik belgisi: BUTUN oila
* bekor */
async function yangila(refresh) {
const c = await pool.connect();
try {
await c.query("BEGIN");
const { rows: [q] } = await c.query(
"SELECT id, foydalanuvchi_id AS fid, oila, ishlatilgan, " +
"bekor, tugash < now() AS eskirgan FROM refresh_tokenlar " +
"WHERE token_hash = $1 FOR UPDATE",
[h(refresh)]);
if (q === undefined) {
await c.query("ROLLBACK");
return { xato: "noma'lum" };
}
if (q.bekor !== null || q.eskirgan) {
await c.query("ROLLBACK");
return { xato: "bekor yoki eskirgan" };
}
if (q.ishlatilgan !== null) {
const r = await c.query(
"UPDATE refresh_tokenlar SET bekor = now() " +
"WHERE oila = $1 AND bekor IS NULL", [q.oila]);
await c.query("COMMIT");
return {
xato: `QAYTA ISHLATILDI — oila bekor (${r.rowCount} token)`,
};
}
await c.query(
"UPDATE refresh_tokenlar SET ishlatilgan = now() WHERE id = $1",
[q.id]);
const yangi = await refreshBer(c, q.fid, q.oila);
await c.query("COMMIT");
return { access: await accessBer(q.fid), refresh: yangi };
} catch (x) {
await c.query("ROLLBACK");
throw x;
} finally {
c.release();
}
}E'tibor bering: oila bekor qilinganda COMMIT (bekor qilish saqlanishi kerak!), noma'lum yoki eskirgan tokenda esa ROLLBACK — hech narsa o'zgarmaydi. refreshBer — 32 tasodifiy bayt, bazaga hash; accessBer — jose.SignJWT bilan EdDSA, iss, aud, jti, 15 daqiqa.
4. Kanon ssenariylari
Kanon besh ssenariyni haqiqiy bazada o'ynadi; bu yerda birinchi uchtasi. Natijada har ssenariyning "LEKIN" va "bekor" so'zli qatorlariga e'tibor bering:
=== 1) Oddiy oqim: kirish → 3 marta yangilash (har safar yangi juftlik, eskisi ishlatilgan)
bazada: { ishlatilgan: '3', faol: '1' }
=== 2) O'g'irlik: hujumchi eski refresh'ni o'g'irlagan; haqiqiy foydalanuvchi allaqachon yangilagan
hujumchi o'g'irlangan (ishlatilgan) token bilan: QAYTA ISHLATILDI — oila bekor (5 token)
endi foydalanuvchining joriy (yangi) tokeni ham: bekor yoki eskirgan → qayta login kerak (oila bekor)
LEKIN 15 daqiqalik access hali ham amal qiladi: ha (JWT bekor qilinmaydi — muddat tugashini kutadi)
=== 3) Teskari tartib: hujumchi BIRINCHI yangiladi
hujumchi yangiladi: ha — endi u ham, foydalanuvchi ham bitta oilada
foydalanuvchi (eski tokeni bilan): QAYTA ISHLATILDI — oila bekor (2 token)
hujumchining yangi tokeni: bekor yoki eskirganBirinchi ssenariyning uchta qatorini qisqartirdik. Endi tahlil:
- Oddiy oqim: uch yangilashdan keyin bazada 3 ta ishlatilgan va 1 ta faol token — zanjir kutilganidek.
- O'g'irlik: hujumchi ishlatilgan token bilan keldi — oila (5 token) bekor, foydalanuvchiga qayta login. Lekin uchinchi qator — eng muhim: hujumchining qo'lida access ham bo'lsa, u yana 15 daqiqa ishlaydi. JWT bekor qilinmaydi. Reuse detection refresh'ni to'xtatdi, lekin eshik 15 daqiqa ochiq.
- Teskari tartib: hujumchi tezroq bo'ldi va birinchi yangiladi. Endi foydalanuvchi o'z (ishlatilgan) tokeni bilan kelganda oila bekor qilinadi — hujumchi ham chiqariladi. Ya'ni kim birinchi bo'lishidan qat'i nazar, ikkinchi kelganda oila o'ladi. Bu reuse detection'ning kuchi.
5. Poyga: soxta-musbat chiqarish
Kanonning to'rtinchi ssenariysi — hech qanday hujum yo'q. Aziz brauzerda ikkita tab ochgan. Access eskirdi, ikkala tab bir vaqtda (bitta cookie'dagi bitta refresh bilan) yangilashga urindi:
=== 4) Poyga: BIR foydalanuvchining 2 ta tabi bir vaqtda bitta refresh bilan yangilaydi (xato emas, lekin rotatsiya buni o'g'irlik deb biladi)
tab A: yangi juftlik | tab B: QAYTA ISHLATILDI — oila bekor (2 token)
g'olib tabning yangi tokeni: bekor yoki eskirgan → soxta-musbat: foydalanuvchi chiqarib yuborildi (yechim: qisqa grace oynasi yoki frontend'da bitta yangilash qulfi)FOR UPDATE tufayli A birinchi o'tdi va yangi juftlik oldi. B kutdi, keyin tokenni "ishlatilgan" ko'rdi — va qoida bo'yicha butun oilani bekor qildi, A ning yangi tokenini ham. Aziz hech narsa qilmagan, lekin tizimdan chiqarib yuborildi. Bu — soxta-musbat (false positive): himoya begunohni "o'g'ri" deb topdi. Brauzerimizdagi simulyator ham xuddi shuni ko'rsatdi.
Ikki yechim bor:
| Yechim | Qanday | Narxi |
|---|---|---|
| frontend qulfi | bitta tab ichida bir vaqtda faqat bitta yangilash; boshqa so'rovlar o'sha natijani kutadi | ko'p tab uchun tablararo qulf kerak (Web Locks API, BroadcastChannel) |
| grace oynasi | refresh ishlatilganidan keyin bir necha soniya ichida yana kelsa — oilani o'ldirmasdan o'sha yangi tokenni qaytarish | shu soniyalarda hujumchi ham o'tib ketishi mumkin |
Ikkalasini mashqlarda yozasiz. Amalda ko'pincha ikkalasi birga ishlatiladi: frontend qulfi poygalarning ko'pini yo'qotadi, qisqa grace oynasi esa qolganini yumshatadi.
Tekshirib ko'ring: Nima uchun grace oynasini 10 daqiqa qilib bo'lmaydi — "foydalanuvchi hech qachon chiqarib yuborilmasin"?
Javob
Grace oynasi ichida eski refresh bilan kelgan har kim yangi tokenni oladi — hujumchi ham. Oyna qancha uzun bo'lsa, o'g'irlangan token shuncha uzoq "ishlatilmagan"dek ishlaydi va reuse detection shuncha kech ishga tushadi. Shuning uchun oyna faqat tarmoq kechikishini qoplaydigan darajada — soniyalarda bo'ladi.
6. Bekor qilish va "barcha qurilmalardan chiqish"
Kanonning beshinchi ssenariysi — foydalanuvchi "barcha qurilmalardan chiqish" tugmasini bosdi. Refresh'lar bitta UPDATE bilan bekor:
=== 5) Barcha qurilmalardan chiqish: refresh'lar bekor, access'lar muddatigacha ishlaydi
q1 refresh: bekor yoki eskirgan | q2 access hali: amal qiladiYana o'sha muammo: access'lar o'z muddatigacha ishlaydi. "Telefonim o'g'irlandi, hammasidan chiqdim" — lekin telefon yana 15 daqiqa vazifalarni o'qiydi va o'zgartiradi. Sessiyada esa bu darhol edi.
JWT'ni darhol bekor qilish yo'llari ham bor, lekin har biri JWT'ning asosiy afzalligini (bazasiz tekshirish) qisman yo'qotadi:
- Taqiq ro'yxati (denylist) — bekor qilingan
jtilar bazada yoki Redis'da; har so'rovda tekshiriladi. - Foydalanuvchi versiyasi — bazada
token_versiyaustuni, JWT ichida ham; "hammasidan chiqish" uni oshiradi. Har so'rovda baribir bitta o'qish. - Juda qisqa access (1–5 daqiqa) — oyna kichrayadi, yangilashlar ko'payadi.
7. Narx: kanon o'lchovi
Ikki yondashuvning har so'rovdagi narxi (kanon, 1000 marta, mediana):
- access JWT (jose EdDSA, bazasiz)0,213 ms
- kanon sessiyasi (Postgres, JOIN)1,605 ms
Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, PostgreSQL 18.6, jose 6.2.12, i5-12500H, 2026-10-27
Va yana ikki raqam: bitta yangilash (tranzaksiya, ikki yozuv, imzo) — 12.0 ms; access token — 298 belgi (har so'rov sarlavhasida), refresh — 43 belgi.
JWT tekshiruvi 7.5 barobar tez — bu haqiqiy afzallik. Lekin mutlaq qiymatlarga qarang: 1.4 ms farq. HTTP so'rov odatda o'nlab millisekund davom etadi. Bir saytli ilova uchun bu farq foydalanuvchiga sezilmaydi, bekor qilib bo'lmaslik va poyga esa — sezilarli.
8. Refresh qayerda saqlanadi
Qisqacha (batafsil — keyingi darsda, CSRF bilan birga):
- Refresh —
HttpOnly,Secure,SameSitecookie'da,Pathfaqat yangilash endpoint'iga (masalan,/v1/auth/yangila). JS uni o'qiy olmaydi, u boshqa so'rovlarga yopishmaydi. - Access — frontend xotirasida (o'zgaruvchida), sahifa yangilansa — refresh orqali qayta olinadi.
localStorageda emas: XSS uni bir qatorda o'g'irlaydi.
OAuth 2.0 xavfsizlik tavsiyalari (RFC 9700, 2025-yil yanvar) brauzer kabi "ochiq" mijozlar uchun refresh'ni yoki rotatsiya bilan, yoki qurilmaga bog'langan (sender-constrained) holda berishni talab qiladi. Bog'lash usullaridan biri — DPoP (RFC 9449): har so'rovni mijozning o'z kaliti bilan imzolash. Kursda sinalmagan.
9. Hujumchi nigohi
Oxirgi ustun — himoya qayerda sinalgani.
| Hujum | Nima uchun ishlaydi | Himoya | Qayerda sinaldi |
|---|---|---|---|
| o'g'irlangan refresh bilan abadiy kirish | refresh bir martalik emas | rotatsiya | kanon 1-ssenariy |
| o'g'irlangan refresh'ni foydalanuvchidan keyin ishlatish | — | reuse detection: oila bekor | kanon 2- va 3-ssenariy |
| oila bekor bo'lgach access bilan davom etish | JWT bekor qilinmaydi | qisqa access, kerak bo'lsa taqiq ro'yxati | kanon: access 15 daqiqa ishladi |
| bazadan refresh'larni o'g'irlash | ochiq saqlangan | SHA-256 hash | refresh_tokenlar jadvali |
| bir xil refresh bilan parallel so'rovlar | qulfsiz o'qish | FOR UPDATE |
«Jadval va yangilash tranzaksiyasi» |
localStorage dagi tokenni XSS bilan olish |
JS o'qiy oladi | refresh — HttpOnly cookie, access — xotirada |
keyingi dars |
10. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
| refresh'ni almashtirmaslik | o'g'irlangani abadiy ishlaydi | har yangilashda yangi refresh |
| reuse'da faqat o'sha tokenni bekor qilish | hujumchining yangi tokeni tirik | butun oila |
| refresh'ni JWT qilish | baribir bazadan tekshiriladi, katta | opaque + hash |
| yangilashni qulfsiz yozish | oila shoxlanadi | tranzaksiya + FOR UPDATE |
| uzoq access (soatlar) | bekor qilib bo'lmaydigan oyna | daqiqalar |
| har 401 da alohida yangilash | tablar poygasi, soxta chiqarish | yagona yangilash qulfi |
| "hammasidan chiqish" = refresh bekor | access'lar ishlashda davom etadi | buni foydalanuvchiga halol ayting yoki versiya |
11. Mashqlar
Mashqlar kurs/mashqlar/28/09-refresh/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
- Access token odatda refresh'dan uzunroq yashaydi.
- Rotatsiyada ishlatilgan refresh yana kelsa, server butun oilani bekor qiladi.
- Oila bekor qilingandan keyin hujumchidagi access token darhol ishlamay qoladi.
- Refresh tokenni bazada SHA-256 hash ko'rinishida saqlash yetarli.
- Ikki tab bir vaqtda yangilasa, rotatsiya buni o'g'irlik deb bilishi mumkin.
Yechim
- Noto'g'ri — access qisqa (daqiqalar), refresh uzun (kunlar).
- To'g'ri — kim o'g'ri ekanini bilmaymiz, shuning uchun hammasi.
- Noto'g'ri — kanon o'lchovi: access 15 daqiqa amal qilishda davom etdi.
- To'g'ri — 256 bitli tasodifiy token uchun tuz va sekin hash shart emas.
- To'g'ri — kanon to'rtinchi ssenariysi: soxta-musbat chiqarish.
2-mashq (o'rta): bitta yangilash qulfi
Frontend'da yangila() funksiyasini yozing: agar yangilash allaqachon ketayotgan bo'lsa, yangi so'rov yubormasdan o'sha Promise'ni qaytarsin; tugagach qulf bo'shasin. Server o'rniga serverYangila() — har chaqiruvda hisoblagichni oshiradi va 20 ms kutadi. Uchta bir vaqtdagi chaqiruv bitta server so'roviga aylanishini, keyingi chaqiruv esa yangi so'rov yuborishini ko'rsating. Ishora: ??= va finally (Promise asoslari).
Yechim
let serverSorovlari = 0;
async function serverYangila() {
serverSorovlari++;
await new Promise((r) => setTimeout(r, 20)); // tarmoq
return `access-${serverSorovlari}`;
}
let jarayonda = null;
function yangila() {
jarayonda ??= serverYangila().finally(() => {
jarayonda = null;
});
return jarayonda;
}
// uchta so'rov bir vaqtda 401 oldi va yangilashga urindi
const natija = await Promise.all([yangila(), yangila(), yangila()]);
console.log(natija.join(", "), "| serverga:", serverSorovlari);
console.log(await yangila(), "| serverga:", serverSorovlari);Konsolda:
access-1, access-1, access-1 | serverga: 1
access-2 | serverga: 2Uch chaqiruv — bitta server so'rovi, hammasi bir xil natija oldi. finally qulfni xato bo'lsa ham bo'shatadi — aks holda bitta muvaffaqiyatsiz yangilash keyingi hammasini "osib" qo'yardi. Bu qulf faqat bitta tab ichida ishlaydi. Tablar o'rtasida brauzerning Web Locks API'si (navigator.locks.request) yoki BroadcastChannel kerak — ularni 13-qismdagi brauzer API'lari bilan o'zingiz sinab ko'ring.
3-mashq (qiyin): grace oynasi
Rotatsiyani grace oynasi bilan yozing (xotirada, tokenlar — r1, r2 kabi nomlar, vaqt — parametr). Har yozuvda ishlatilgan (vaqt yoki null) va bola (shu tokendan tug'ilgan yangi token) bo'lsin. Ishlatilgan token 10 soniya ichida yana kelsa va uning bolasi hali ishlatilmagan bo'lsa — oilani o'ldirmasdan o'sha bolani qaytaring. Aks holda — oila bekor. Ssenariy: tab A 0-soniyada, tab B 2-soniyada bitta token bilan yangilaydi; hujumchi 60-soniyada o'sha eski token bilan keladi; keyin foydalanuvchi o'z tokeni bilan. Ishora: Map va for...of qiymatlar bo'yicha.
Yechim
const GRACE_MS = 10_000;
let n = 0;
const baza = new Map(); // token → { oila, ishlatilgan, bola, bekor }
function kirish(oila) {
const t = `r${++n}`;
baza.set(t, { oila, ishlatilgan: null, bola: null, bekor: false });
return t;
}
function yangila(t, hozir) {
const q = baza.get(t);
if (!q || q.bekor) return "bekor";
if (q.ishlatilgan !== null) {
const bola = baza.get(q.bola);
// grace: yaqinda yangilangan va bolasi hali ishlatilmagan —
// xuddi o'sha javobni qaytaramiz (idempotent)
if (hozir - q.ishlatilgan <= GRACE_MS &&
bola.ishlatilgan === null && !bola.bekor) {
return q.bola;
}
for (const x of baza.values()) {
if (x.oila === q.oila) x.bekor = true;
}
return "QAYTA ISHLATILDI — oila bekor";
}
const yangi = kirish(q.oila);
q.ishlatilgan = hozir;
q.bola = yangi;
return yangi;
}
const t1 = kirish("A");
console.log("tab A, 0 s:", yangila(t1, 0));
console.log("tab B, 2 s:", yangila(t1, 2_000));
console.log("hujumchi, 60 s:", yangila(t1, 60_000));
console.log("foydalanuvchi:", yangila("r2", 61_000));Konsolda:
tab A, 0 s: r2
tab B, 2 s: r2
hujumchi, 60 s: QAYTA ISHLATILDI — oila bekor
foydalanuvchi: bekorIkkala tab bir xil r2 ni oldi — poyga zararsiz o'tdi. 60-soniyada eski token oynadan tashqarida — reuse detection ishladi va butun oila, foydalanuvchining r2 si ham, bekor bo'ldi. Bu yechimning qiymati shu yerda ko'rinadi: grace — idempotentlik (Idempotency-Key darsidagi g'oya): bir xil so'rov qisqa vaqt ichida takrorlansa — bir xil javob. Haqiqiy kodda tokenlar hash bilan saqlanadi, demak bolani qaytarish uchun uni shifrlab saqlash yoki bolani qayta yaratish kerak bo'ladi — bu murakkablik grace oynasining yana bir narxi.
12. Loyiha qadami: strategiya kanonga kirmadi
Bu darsda vazifalar-api ga kod qo'shilmaydi. Kanon access va refresh strategiyasini haqiqiy kod bilan yozdi va sinadi — yuqoridagi barcha natijalar shu tajribadan — lekin loyihada server sessiyasi qoldi. Sabab endi raqamlarda ko'rinadi:
| Talab | Server sessiyasi (loyihada) | Access + refresh (tajriba) |
|---|---|---|
| har so'rovda tekshirish | 1.6 ms | 0.21 ms |
| chiqish va "hammasidan chiqish" | darhol | refresh darhol, access 15 daqiqa |
| o'g'irlangan token | sessiya bekor | reuse detection, lekin poygada soxta-musbat |
| kod va xavf yuzasi | token, hash, muddat | + kalitlar, alg, aud, oila, qulf, grace |
vazifalar frontendi va API bitta saytda ishlaydi — JWT'ning bazasiz tekshirish afzalligi bu yerda kerak emas, uning kamchiliklari esa "hammasidan chiqish" kabi kerakli funksiyaga to'g'ridan zid. Kanon bu qarorni ADR hujjati sifatida yozdi va mobil ilova yoki boshqa xizmat paydo bo'lsa, qayta ko'rishni shart qilib qo'ydi — buni Session vs JWT qarori va uzoq sessiyalar darsida ko'ramiz.
Tajriba kodi bilan o'zingiz o'ynamoqchi bo'lsangiz — alohida papkada jose va pg bilan, o'z lokal bazangizda, «Bazada: kanon tajribasi» bo'limidagi jadval bilan. vazifalar-api ga commit qilinadigan narsa yo'q.
13. Real ishda
- Auth provayderlari (Auth0, Clerk, Firebase va boshqalar) refresh rotatsiyasi va reuse detection'ni tayyor beradi — aynan shu darsdagi mexanizm, ko'pincha grace oynasi sozlamasi bilan. Bular Auth kutubxonalari va xizmatlari darsida.
- Mobil ilovalar — cookie yo'q, shuning uchun access + refresh u yerda tabiiy tanlov: refresh qurilmaning xavfsiz omborida (Keychain, Keystore) saqlanadi.
- Mikroservislar — bitta auth xizmati EdDSA bilan access chiqaradi, qolgan xizmatlar JWKS bilan bazasiz tekshiradi. JWT o'zining haqiqiy kuchini shu yerda ko'rsatadi.
- Intervyuda: "Refresh rotation nima?", "Token oilasi va reuse detection?", "Logout'dan keyin access nima bo'ladi?", "Ikki tab poygasini qanday hal qilasiz?".
Xulosa
- Access — qisqa JWT (15 daqiqa), har so'rovda, bazasiz; refresh — uzun (30 kun), opaque, bazada SHA-256, faqat yangilashda.
- Rotatsiya: har yangilashda yangi refresh; ishlatilgan refresh yana kelsa — butun oila bekor (reuse detection). Kim birinchi bo'lishidan qat'i nazar, ikkinchi kelganda oila o'ladi.
- Yangilash — tranzaksiya va
FOR UPDATE; qulfsiz oila shoxlanadi. - Kanon o'lchovi: oila bekor bo'lgach access 15 daqiqa ishladi; "hammasidan chiqish" ham access'larni to'xtatmaydi.
- Ikki tab poygasi — soxta-musbat chiqarish; yechim — frontend qulfi va qisqa grace oynasi.
- JWT tekshiruvi 0.21 ms, sessiya 1.6 ms, yangilash 12 ms —
vazifalaruchun kanon sessiyani tanladi.
Keyingi dars: Tokenni qayerda saqlash va CSRF — localStorage va cookie, CSRF hujumi va Origin tekshiruvi; kanon haqiqiy brauzerda yangi zaiflik topdi.
Manbalar
- RFC 9700 "Best Current Practice for OAuth 2.0 Security" (2025-yil yanvar) — refresh token rotation, sender-constrained tokens: https://www.rfc-editor.org/rfc/rfc9700
- RFC 9449 "OAuth 2.0 Demonstrating Proof of Possession (DPoP)": https://www.rfc-editor.org/rfc/rfc9449
- OWASP JSON Web Token Cheat Sheet (token revocation): https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
- MDN — Web Locks API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API
- PostgreSQL 18 —
SELECT … FOR UPDATE: https://www.postgresql.org/docs/18/sql-select.html#SQL-FOR-UPDATE-SHARE - Kanon o'lchovi (IlmHamroh, 2026-10-27): refresh rotatsiyasi tajribasi (jose EdDSA,
pg, PostgreSQL 18.6, Node 24.21.0) va sessiya store'lari tajribasi. Brauzer misollari va SQL so'rovlar (PostgreSQL 17.6) — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!