IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi9/31-dars20 daqiqa
Mundarija (25)

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-api ga 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: 200

Bazaga 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):

js
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:

text
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'lum

Birinchi 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:

sql
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:

text
CREATE TABLE
CREATE INDEX

Bu 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:

sql
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:

text
INSERT 0 1

Yangilash — bitta tranzaksiyada: tokenni qulflab o'qish, ishlatilgan deb belgilash, yangisini yozish:

sql
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:

text
BEGIN
 id |                 oila                 | ishlatilgan | bekor
----+--------------------------------------+-------------+-------
  1 | a0000000-0000-4000-8000-000000000001 | f           | f
(1 row)

UPDATE 1
INSERT 0 1
COMMIT

FOR 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:

sql
SELECT id, ishlatilgan IS NOT NULL AS ishlatilgan
FROM refresh_tokenlar WHERE token_hash = sha256('r1'::bytea);

Natija:

text
 id | ishlatilgan
----+-------------
  1 | t
(1 row)

Ishlatilgan — demak o'g'irlik belgisi. Butun oila:

sql
UPDATE refresh_tokenlar SET bekor = now()
WHERE oila = 'a0000000-0000-4000-8000-000000000001'
  AND bekor IS NULL;

Natija:

text
UPDATE 2
sql
SELECT id, ishlatilgan IS NOT NULL AS ishlatilgan,
       bekor IS NOT NULL AS bekor
FROM refresh_tokenlar ORDER BY id;

Natija:

text
 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:

js
/** 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:

text
=== 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 eskirgan

Birinchi ssenariyning uchta qatorini qisqartirdik. Endi tahlil:

  1. Oddiy oqim: uch yangilashdan keyin bazada 3 ta ishlatilgan va 1 ta faol token — zanjir kutilganidek.
  2. 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.
  3. 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:

text
=== 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:

text
=== 5) Barcha qurilmalardan chiqish: refresh'lar bekor, access'lar muddatigacha ishlaydi
  q1 refresh: bekor yoki eskirgan | q2 access hali: amal qiladi

Yana 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 jti lar bazada yoki Redis'da; har so'rovda tekshiriladi.
  • Foydalanuvchi versiyasi — bazada token_versiya ustuni, 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):

Har so'rovda kimlikni tekshirish (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, SameSite cookie'da, Path faqat 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. localStorage da 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

  1. Access token odatda refresh'dan uzunroq yashaydi.
  2. Rotatsiyada ishlatilgan refresh yana kelsa, server butun oilani bekor qiladi.
  3. Oila bekor qilingandan keyin hujumchidagi access token darhol ishlamay qoladi.
  4. Refresh tokenni bazada SHA-256 hash ko'rinishida saqlash yetarli.
  5. Ikki tab bir vaqtda yangilasa, rotatsiya buni o'g'irlik deb bilishi mumkin.
Yechim
  1. Noto'g'ri — access qisqa (daqiqalar), refresh uzun (kunlar).
  2. To'g'ri — kim o'g'ri ekanini bilmaymiz, shuning uchun hammasi.
  3. Noto'g'ri — kanon o'lchovi: access 15 daqiqa amal qilishda davom etdi.
  4. To'g'ri — 256 bitli tasodifiy token uchun tuz va sekin hash shart emas.
  5. 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
js
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:

text
access-1, access-1, access-1 | serverga: 1
access-2 | serverga: 2

Uch 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
js
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:

text
tab A, 0 s: r2
tab B, 2 s: r2
hujumchi, 60 s: QAYTA ISHLATILDI — oila bekor
foydalanuvchi: bekor

Ikkala 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 — vazifalar uchun 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Access va refresh token strategiyasi: rotatsiya va o'g'irlikni aniqlash — IlmHamroh