IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi11/31-dars25 daqiqa
Mundarija (34)

Session vs JWT qarori va uzoq sessiyalar: ADR, "meni eslab qol" va qurilmalar ro'yxati

Qisqacha: Brauzer va API bir saytda bo'lsa — server sessiyasi yetarli va xavfsizroq: chiqish darhol ishlaydi, har qurilmani alohida o'chirish mumkin, token kichik. JWT'ning kuchi — bazasiz tekshirish va servislar orasida ishonch — bunday ilovaga kerak emas, kamchiligi (muddatigacha bekor qilib bo'lmaydi) esa kerakli funksiyalarga zid. Qaror o'lchovlar bilan ADR hujjatiga yoziladi. Uzoq sessiya uchun ikki chegara: idle (jim turish, har so'rovda suriladi) va mutlaq muddat; "meni eslab qol" ularni uzaytiradi.

Bu darsda

  • Stateful (sessiya) va stateless (JWT) yondashuvni kanon o'lchovlari bilan solishtirasiz.
  • Arxitektura qarorini ADR (Architecture Decision Record) shaklida yozishni o'rganasiz.
  • Idle va mutlaq muddatni, sliding expiration va "meni eslab qol" ni farqlaysiz.
  • Qurilmalar ro'yxati va bitta qurilmadan chiqishni IDOR'siz yozasiz.
  • vazifalar-api ga 113-migratsiyani, docs/AUTH-QAROR.md ni va ikkita yangi testni qo'shasiz.

Oldin bilishingiz kerak: Session-based auth, JWT anatomiyasi, Access va refresh token strategiyasi, Tokenni qayerda saqlash va CSRF.

Oldingi to'rt darsda ikki yo'lni ham qurdik. Session-based auth — vazifalar-api ga kirdi. JWT va refresh rotatsiya — JWT anatomiyasi va Access va refresh token strategiyasi darslarida haqiqiy kod bilan sinaldi, lekin loyihaga qo'shilmadi. Nega? Bugun shu qarorni ochiq, o'lchovlar bilan yozamiz — va keyin uni uzoq sessiyalar bilan to'ldiramiz.

1. Nega bu kerak?

Internetda "JWT yoki sessiya?" degan bahs tugamaydi. Bir maqola "sessiya eskirdi, JWT zamonaviy" deydi, boshqasi "JWT'ni sessiya uchun ishlatmang" deydi. Ikkalasi ham o'z sharoitida to'g'ri. Xato — sharoitni ko'rmasdan, modaga qarab tanlash.

Yaxshi jamoada bunday savol bir marta, ochiq hal qilinadi va yozib qo'yiladi: qaysi variantlar ko'rildi, qaysi raqamlar o'lchandi, nima tanlandi va qachon qayta ko'rib chiqiladi. Olti oydan keyin yangi dasturchi "nega JWT emas?" deb so'rasa, javob bitta faylda turadi.

Ikkinchi muammo — foydalanuvchi. Telefonda vazifalar ni har kuni ochadigan Malika har hafta parol kiritishni xohlamaydi. Kutubxona kompyuteridan kirgan Jasur esa brauzerni yopganda sessiya o'chishini xohlaydi. Ikkalasiga bitta muddat to'g'ri kelmaydi.

2. Ikki yondashuv

2.1 Stateful va stateless

  • Stateful (holatli) — server foydalanuvchini eslab qoladi: bazada sessiya qatori bor, brauzerda faqat tasodifiy kalit. Har so'rovda server kalitni bazadan qidiradi. Xuddi kiyim ilgichidagi raqamli jeton: jetonda hech narsa yozilmagan, kiyim ilgichda.
  • Stateless (holatsiz) — server hech narsani eslamaydi: kim ekaningiz, rolingiz va muddat tokenning o'zida yozilgan va imzolangan (JWT). Server faqat imzoni tekshiradi. Xuddi suratli guvohnoma: qo'riqchi ro'yxatga qaramaydi, guvohnomaning o'ziga qaraydi.

Guvohnomaning muammosi ham shu yerda: uni qaytarib olib bo'lmaydi. Xodim ishdan bo'shatilsa ham, guvohnomadagi muddat tugaguncha u eshikdan o'taveradi — qo'riqchida "bekor qilinganlar ro'yxati" bo'lmasa. Ro'yxat paydo bo'lsa esa — biz yana holatli tizimga qaytdik.

2.2 Kanon o'lchovlari

Har ikkala yo'l bitta mashinada (i5-12500H, Node 24.21.0, 2026-10-27) o'lchangan. Avval — har so'rovda "bu kim?" savoliga javob narxi:

Har so'rovda tekshirish narxi (mediana, 1000 marta)
  • JWT (jose, EdDSA)bazasiz0,213 ms
  • connect-redisRedis0,872 ms
  • connect-pg-simplePostgres1,163 ms
  • kanon tekshir()Postgres, JOIN1,605 ms

Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, PostgreSQL 18.6, Redis 8.10.2, jose 6.2.12, i5-12500H, 2026-10-27

JWT tekshiruvi 7.5 marta tez — bazaga bormaydi. Lekin raqamlarni masshtab bilan o'qing: farq 1.4 ms. Postgres'dagi har bir vazifa so'rovi, JSON yasash va tarmoq shundan ko'p vaqt oladi. Kanon sessiyasi Redis'dagidan biroz sekinroq, chunki u bitta JOIN bilan foydalanuvchini ham olib keladi — keyingi so'rov kerak bo'lmaydi.

Endi qolgan mezonlar:

Mezon Server sessiyasi Access JWT + refresh
Chiqish, bloklash darhol refresh darhol, access 15 daqiqagacha yashaydi
O'g'irlangan token sessiya bekor reuse detection; 2 tab poygasida soxta chiqarish
Har so'rov sarlavhasi cookie 58 belgi access 298 belgi
Baza sessiyalar (hash) refresh_tokenlar + kalit aylantirish
Xato qilish imkoni kam ko'p: alg, aud, exp, kalitlar, oila

Jadvaldagi har qatorning orqasida tajriba bor. Access va refresh token strategiyasi darsida ko'rganimizdek, "barcha qurilmalardan chiqish" refresh tokenlarni o'ldirdi, lekin access tokenlar o'z muddatigacha ishlab turdi. Bir foydalanuvchining ikki tabi bir vaqtda yangilaganda rotatsiya buni o'g'irlik deb bildi va foydalanuvchini chiqarib yubordi. JWT anatomiyasi darsida esa alg: none va kuchsiz sir bilan bog'liq tuzoqlarni ko'rdik.

58 belgi qayerdan kelgani — tekshirib ko'ring:

js
const token = "A".repeat(43); // 32 bayt base64url — 43 belgi
console.log(`__Host-sessiya=${token}`.length);

Konsolda:

text
58

2.3 Qaror daraxti

O'lchovlar bitta savolga olib keladi: bizga JWT'ning kuchi kerakmi?

flowchart TD
  S{Kim API'ni chaqiradi?} -->|bir saytdagi brauzer| B[Server sessiyasi + HttpOnly cookie]
  S -->|mobil ilova| M[Qisqa access + refresh yoki opaque token]
  S -->|boshqa servis| J{Bazasiz tekshirish kerakmi?}
  J -->|ha| JW[Qisqa JWT, aniq aud]
  J -->|yo'q| K[API kaliti yoki HMAC]
  S -->|tashqi ilova foydalanuvchi nomidan| O[OAuth 2.0 token]

Diagrammada eng muhimi — birinchi shox. Bizning holatimiz shu: frontend va API bir saytda. Sessiya bu yerda soddaroq, tezroq bekor qilinadi va xavf yuzasi kichik. Qolgan shoxlar — API kalitlar va servislararo autentifikatsiya va OAuth 2.0 va OpenID Connect nazariyasi darslarida.

Tekshirib ko'ring: Bitta Node serveri o'rniga beshta server bo'lsa (load balancer orqasida), sessiya baribir ishlaydimi?

Javob

Ishlaydi, chunki sessiya bitta umumiy bazada (Postgres yoki Redis), serverning xotirasida emas. Har server so'rovni olib, o'sha bazadan tekshiradi. "Sessiya masshtablanmaydi" degan gap xotirada (MemoryStore) saqlangan sessiyaga tegishli. Masshtab muammosi faqat juda ko'p servislar va juda katta yuklamada boshlanadi — u yerda JWT'ning bazasiz tekshiruvi haqiqatan foyda beradi.

3. ADR: qarorni yozib qo'yish

3.1 Format

ADR (Architecture Decision Record — arxitektura qarori yozuvi) — bitta qaror uchun bitta qisqa fayl. Formatni 2011-yilda Michael Nygard taklif qilgan; uning asosiy bo'limlari: kontekst, qaror, oqibatlar. Kanon o'z variantini oldi:

Bo'lim Savol
Kontekst qanday sharoitda va qanday talablar bilan qaror qabul qilinyapti?
Variantlar qaysi yo'llar ko'rildi va ular qanday o'lchandi?
Qaror nima tanlandi va asosiy sababi?
Qachon qayta ko'riladi qaysi o'zgarish bu qarorni bekor qiladi?

Oxirgi bo'lim eng ko'p unutiladi va eng qimmati. U qarorni "abadiy haqiqat" dan "shu sharoitdagi eng yaxshi tanlov" ga aylantiradi.

3.2 docs/AUTH-QAROR.md

Kanon ADR'ini to'liq ko'rsatamiz. Jadvaldagi o'lchov havolalarini darsdagi nomlar bilan almashtirdik va satrlarni qisqartirdik, mazmun o'zgarmagan:

markdown
# ADR: brauzer uchun server sessiyasi (JWT emas)

Holat: **qabul qilingan** (2026-10-27).

## Kontekst
`vazifalar` frontendi va `vazifalar-api` bir saytda ishlaydi:
dev'da Vite proxy, deploy'da bitta domen ostida nginx. Mijoz —
brauzer (keyin Telegram bot — u API kaliti bilan). Talablar: chiqish
darhol ishlasin, "barcha qurilmalardan chiqish", qurilmalar
ro'yxati, XSS token o'g'irlay olmasin.

## Variantlar (ikkalasi haqiqiy kod bilan o'lchangan)

| Mezon | Server sessiyasi | Access JWT + refresh |
|---|---|---|
| Har so'rovda tekshirish | 1 so'rov, 1.6 ms | imzo, 0.21 ms |
| Chiqish / bloklash | darhol | access 15 daqiqagacha |
| O'g'irlangan token | sessiya bekor | reuse detection, poyga |
| Saqlash joyi | HttpOnly cookie | xotira/cookie + cookie |
| Sarlavha hajmi | 58 belgi | 298 belgi |
| Baza | sessiyalar (hash) | refresh_tokenlar + JWKS |
| Kod va xavf | kam | ko'p: alg, aud, exp, kalit |

## Qaror
Brauzer uchun — **server sessiyasi** (Postgres). Bir sayt
arxitekturasida JWT'ning asosiy afzalligi (bazasiz tekshirish,
servislar orasida tarqatish) bizga kerak emas, kamchiligi (darhol
bekor qilib bo'lmaydi) esa kerakli funksiyalarga to'g'ridan zid.

## Uzoq sessiyalar
- "Meni eslab qol": cookie 30 kun, idle 7 kun — har so'rovda
  suriladi (sliding, 60 s qadam bilan).
- Belgilanmasa: cookie Expires'siz, idle 12 soat, mutlaq 7 kun.
- Qurilmalar: GET /v1/auth/sessiyalar, bittasi —
  DELETE /v1/auth/sessiyalar/:id, hammasi —
  POST /v1/auth/chiqish-hammasi.

## Qachon qayta ko'riladi
Mobil ilova (cookie'siz) yoki boshqa servis foydalanuvchi nomidan
so'rov yuborsa — qisqa JWT (EdDSA, aniq aud) yoki OAuth 2.0 token;
dastur-dastur so'rovlari — API kaliti / HMAC.

Kanon matnida "1.4 ms farq HTTP so'rovning umumiy vaqti ichida sezilmaydi" degan gap ham bor — biz uni grafik ostidagi izohda aytdik.

4. Uzoq sessiyalar

4.1 Ikki chegara: idle va mutlaq

Sessiyaning umri ikki soat bilan o'lchanadi:

  • Idle timeout (jim turish chegarasi) — oxirgi so'rovdan beri qancha vaqt o'tdi. Foydalanuvchi faol bo'lsa, har so'rovda bu chegara suriladi. Buni sliding expiration (siljiydigan muddat) deyishadi.
  • Absolute timeout (mutlaq chegara) — sessiya yaratilgandan beri. Faol bo'lsa ham, shundan keyin qayta kirish kerak. O'g'irlangan sessiya abadiy yashamasligi uchun.

Kanonning ikki rejimi:

Rejim Cookie Idle Mutlaq
"Meni eslab qol" Expires — 30 kun 7 kun 30 kun
Belgilanmagan Expires yo'q 12 soat 7 kun

Expires siz cookie — sessiya cookie'si: brauzer yopilganda o'chadi (kutubxona kompyuteridagi Jasur uchun). Lekin "brauzer yopilganda" har doim ham amal qilmaydi — ko'p brauzerlar "yopilgan tablarni tiklash" rejimida sessiya cookie'larini ham qaytaradi. Shuning uchun serverdagi 12 soatlik idle va 7 kunlik mutlaq chegara — haqiqiy himoya, cookie'ning Expires i emas.

Kanon tekshiruvining shartlarini toza funksiya bilan sinaymiz:

js
const SOAT = 3_600_000;
const KUN = 24 * SOAT;

// Kanon tekshiruvining shartlari — bazasiz, toza funksiya
function holat(s, hozir) {
  if (s.bekor) return "bekor qilingan";
  if (hozir >= s.yaratilgan + s.muddat) return "mutlaq tugadi";
  if (hozir - s.oxirgi >= s.idle) return "idle tugadi";
  return "tirik";
}

const ESLAB = { yaratilgan: 0, muddat: 30 * KUN, idle: 7 * KUN };
const QISQA = { yaratilgan: 0, muddat: 7 * KUN, idle: 12 * SOAT };

const VAZIYATLAR = [
  ["13 soat jim", 0, 13 * SOAT],
  ["har kuni, 6-kun", 5.75 * KUN, 6 * KUN],
  ["har kuni, 10-kun", 9.75 * KUN, 10 * KUN],
  ["8 kun jim", 0, 8 * KUN],
  ["har kuni, 31-kun", 30.75 * KUN, 31 * KUN],
];

console.log("vaziyat".padEnd(17), "eslab qol".padEnd(14), "qisqa");
for (const [nom, oxirgi, hozir] of VAZIYATLAR) {
  const a = holat({ ...ESLAB, oxirgi }, hozir);
  const b = holat({ ...QISQA, oxirgi }, hozir);
  console.log(nom.padEnd(17), a.padEnd(14), b);
}

Konsolda:

text
vaziyat           eslab qol      qisqa
13 soat jim       tirik          idle tugadi
har kuni, 6-kun   tirik          tirik
har kuni, 10-kun  tirik          mutlaq tugadi
8 kun jim         idle tugadi    mutlaq tugadi
har kuni, 31-kun  mutlaq tugadi  mutlaq tugadi

"har kuni" — foydalanuvchi har kuni kiradi, oxirgi so'rovi 6 soat oldin. Ikkinchi va uchinchi qatorni solishtiring: har kuni kiradigan Malika ham "qisqa" rejimda 7-kundan keyin qayta kiradi — mutlaq chegara. "Eslab qol" rejimida u 30 kun tinch ishlaydi. Oxirgi qator: 30 kundan keyin hamma qayta kiradi.

4.2 Sliding — lekin har so'rovda yozmasdan

Idle chegarani surish uchun oxirgi_faollik yangilanishi kerak. Har so'rovda UPDATE — bazaga ortiqcha yozuv. Kanon qoidasi: faqat 60 soniyadan eski bo'lsa yozadi (Session-based auth darsidan). Narxi — bazadagi faollik 60 soniyagacha "orqada" qolishi mumkin. 7 kunlik idle uchun bu ahamiyatsiz. Uchinchi mashqda yozuvlar sonini hisoblaysiz.

4.3 "Meni eslab qol" — xavfsizlik nuqtai nazaridan

Belgi qo'yilsa, cookie 30 kun yashaydi va o'g'irlansa 30 kun ishlaydi. Shuning uchun:

  • belgi sukut bo'yicha o'chiq (eslabQol — false): foydalanuvchi uni ongli tanlaydi;
  • uzoq sessiyada ham mutlaq chegara bor — 30 kun;
  • foydalanuvchi o'z sessiyalarini ko'rib, keraksizini o'chira oladi (keyingi bo'lim);
  • parol tiklanganda hamma sessiyalar bekor bo'ladi (Email tasdiqlash va parol tiklash oqimlari).

Muhim nozik joy: kanon sxemasida migratsiyadan oldingi sessiyalar uchun sukut eslab_qol = true. Bu xulqni o'zgartirmaslik uchun: 113-migratsiyagacha hamma sessiya 30 kunlik edi. Yangi kirishlarda esa sukut — false, u sxemada (Zod) belgilangan.

Tekshirib ko'ring: Nega "eslab qolinmagan" sessiyaning mutlaq chegarasi bazada saqlanadi, cookie'ning o'zida emas?

Javob

Cookie — mijozdagi narsa. Uning Expires i yo'q, demak brauzer qachon o'chirishini server bilmaydi: brauzer tablarni tiklasa, cookie haftalab yashashi mumkin. Hujumchi esa cookie'ni o'z dasturiga ko'chirib, Expires ga umuman qaramaydi. Server faqat o'zi saqlagan narsaga ishonadi — tugash_vaqti va idle_muddat bazada.

5. Qurilmalar ro'yxati va majburan tugatish

5.1 "Qayerlarda kirganman?"

Telegram, Google va banklarda bor bo'lim: "faol sessiyalar" (ba'zi ilovalarda "seanslar"). Unda har qurilma, oxirgi faollik va "chiqish" tugmasi. Server sessiyasida buni qilish oson — sessiyalar jadvalda turibdi. JWT bilan esa buning uchun baribir jadval kerak bo'lardi.

Kanon user_agent ni saqlaydi, ro'yxatda esa undan qisqa nom yasaydi:

js
function qurilma(ua) {
  if (ua === null || ua === "") return "noma'lum";
  const brauzer = /Edg\//.test(ua) ? "Edge"
    : /OPR\//.test(ua) ? "Opera"
    : /Firefox\//.test(ua) ? "Firefox"
    : /Chrome\//.test(ua) ? "Chrome"
    : /Safari\//.test(ua) ? "Safari"
    : /curl\//.test(ua) ? "curl"
    : "boshqa";
  const tizim = /Android/.test(ua) ? "Android"
    : /iPhone|iPad/.test(ua) ? "iOS"
    : /Windows/.test(ua) ? "Windows"
    : /Mac OS X/.test(ua) ? "macOS"
    : /Linux/.test(ua) ? "Linux"
    : "";
  return tizim === "" ? brauzer : `${brauzer}, ${tizim}`;
}

const UA = [
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/151.0 " +
    "Safari/537.36",
  "Mozilla/5.0 (Windows NT 10.0) Chrome/151.0 Safari/537.36 " +
    "Edg/151.0",
  "Mozilla/5.0 (Linux; Android 15) Chrome/151.0 Mobile Safari/537.36",
  "Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) " +
    "Version/19.0 Mobile/15E148 Safari/604.1",
  "curl/8.11.0",
  null,
];
for (const ua of UA) console.log(qurilma(ua));

Konsolda:

text
Chrome, Windows
Edge, Windows
Chrome, Android
Safari, iOS
curl
noma'lum

Tekshiruvlar tartibiga qarang — u tasodifiy emas. Edge'ning User-Agent satrida Chrome/ ham bor, shuning uchun Edg/ birinchi tekshiriladi. Android satrida Linux bor, iPhone satrida Mac OS X — ularning tartibi ham shuning uchun. UA satrlari namunaviy, qisqartirilgan.

Ikki muhim gap. Birinchisi — User-Agent'ni mijoz o'zi yozadi, uni soxtalashtirish oson. Shuning uchun u faqat ko'rsatish uchun, hech qanday xavfsizlik qarori unga tayanmaydi. Ikkinchisi — ro'yxatda token ham, uning hash'i ham yo'q: javob o'g'irlansa ham undan sessiya yasab bo'lmaydi.

5.2 Bitta sessiyani o'chirish va IDOR

DELETE /v1/auth/sessiyalar/:id — "telefonimdan chiqish". Bu yerda klassik xato kutib turadi: sessiya id si — oddiy son. Agar server faqat WHERE id = $1 qilsa, Dilnoza id larni ketma-ket sinab, boshqalarning sessiyalarini o'chirib chiqadi. Bu IDOR (Insecure Direct Object Reference — obyektga himoyasiz to'g'ridan murojaat), API dunyosida BOLA deb ataladi (Session-based auth darsida vazifalar uchun ko'rgandik).

Kanon himoyasi — egasi sharti so'rovning o'zida:

sql
UPDATE sessiyalar SET bekor_vaqti = now()
WHERE id = $1 AND foydalanuvchi_id = $2 AND bekor_vaqti IS NULL;

Begona sessiya id si uchun bitta ham qator o'zgarmaydi va server 404 qaytaradi — 403 emas. 403 "bunday sessiya bor, lekin sizniki emas" degan ma'lumotni oshkor qilardi.

5.3 Majburan tugatishning uch darajasi

Kim Nima Qanday
foydalanuvchi bitta qurilma DELETE /v1/auth/sessiyalar/:id
foydalanuvchi hamma qurilmalar POST /v1/auth/chiqish-hammasi
tizim hamma qurilmalar parol tiklanganda — avtomatik

Admin tomonidan bloklash ham xuddi shu UPDATE — rollarni RBAC va resurs egaligi darsida qo'shamiz. Server sessiyasida har uchala holat keyingi so'rovdayoq kuchga kiradi.

6. Mobil ilova tokenlari

Mobil ilovada brauzer yo'q: cookie'ni ilova o'zi boshqaradi va CSRF muammosi ham yo'q. Odatdagi yo'llar:

  • Opaque token (ma'nosiz tasodifiy satr) — xuddi bizning sessiya tokeni, lekin cookie o'rniga Authorization: Bearer sarlavhasida. Ilova uni telefonning xavfsiz omborida saqlaydi (iOS Keychain, Android Keystore).
  • Qisqa access + refresh — refresh rotatsiya bilan; ayniqsa bir nechta servis bo'lsa.
  • Uchinchi tomon ilovalari uchun — OAuth 2.0 (mobil ilovalar uchun qoidalar RFC 8252 da).

Kanon bularni sinamadi — mobil ilova kursda keyinroq. ADR'dagi "Qachon qayta ko'riladi" bo'limi aynan shu holatni yozib qo'ygan: mobil ilova paydo bo'lsa — qaror qayta ko'riladi.

7. Hujumchi nigohi

Oxirgi ustun — himoya qayerda sinalgani.

Hujum Nima uchun ishlaydi Himoya Qayerda sinaldi
o'g'irlangan cookie bilan oylab yurish muddat cheksiz yoki juda uzun mutlaq chegara (7 / 30 kun) holat(): "mutlaq tugadi"
ochiq qolgan kutubxona kompyuteri sessiya brauzer yopilgandan keyin ham tirik eslab qolinmasa — 12 soat idle test: 13 soatdan keyin 401
DELETE /sessiyalar/<begona id> egasi tekshirilmaydi AND foydalanuvchi_id = $2, 404 test: Dilnoza sessiyasi — 404
ro'yxat javobidan token olish javobga hash/token tushadi ro'yxatda faqat id, vaqt, qurilma test: javobda token yo'q
soxta User-Agent bilan "ishonchli qurilma" UA'ga qaror bog'langan UA — faqat ko'rsatish uchun — (kod qoidasi)
JWT chiqqandan keyin ham ishlaydi stateless token bekor bo'lmaydi brauzer uchun server sessiyasi ADR, oldingi darslar
idle'ni idle bilan "uzaytirish" faqat sliding, mutlaq yo'q ikkala chegara birga holat(): "har kuni, 31-kun"

8. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
"JWT zamonaviy" deb tanlash bekor qilish va xavf yuzasi unutiladi o'lchab, ADR'ga yozish
faqat idle chegara faol hujumchi abadiy qoladi idle + mutlaq
"eslab qol" sukut bo'yicha yoqiq umumiy kompyuterda sessiya qoladi sukut — o'chiq
har so'rovda oxirgi_faollik yozish ortiqcha yozuv yuklamasi 60 s qadam
sessiya id bo'yicha o'chirish IDOR egasi sharti SQL'da
begona sessiyaga 403 borligini oshkor qiladi 404
ADR'siz qaror "nega?" savoliga javob yo'q bir fayl, "qachon qayta ko'riladi" bilan

9. Mashqlar

Mashqlar kurs/mashqlar/28/11-sessiya-qarori/ da.

1-mashq (oson): qaysi biri?

Har vaziyat uchun tanlang: server sessiyasi, qisqa JWT yoki API kaliti.

  1. vazifalar frontendi va API bitta domen ostida.
  2. Hisobot servisi vazifalar-api ga har kecha ma'lumot so'raydi (foydalanuvchisiz).
  3. Yuzlab mikroservis bitta foydalanuvchi so'rovini bir-biriga uzatadi va har biri bazaga bormasdan "kim?" ni bilishi kerak.
  4. Bank ilovasi: operator foydalanuvchini darhol bloklay olishi shart.
Yechim
  1. Server sessiyasi — bir sayt, darhol chiqish, kanon qarori.
  2. API kaliti (yoki HMAC imzo) — bu foydalanuvchi emas, dastur (API kalitlar va servislararo autentifikatsiya).
  3. Qisqa JWT — aynan JWT'ning kuchi: bazasiz tekshirish va servislar orasida ishonch. Muddat — daqiqalar.
  4. Server sessiyasi — darhol bloklash talab qilingan; JWT bilan bu taqiq ro'yxatini (denylist), ya'ni yana holatni talab qiladi.

2-mashq (o'rta): qachon tugaydi?

qachonTugaydi(s) funksiyasini yozing: sessiya { yaratilgan, muddat, idle, oxirgi } (ms) bo'yicha qaysi chegara birinchi kelishini va qachonligini qaytarsin: { vaqt, sabab }, sabab — "mutlaq" yoki "idle". To'rt holatni soatlarda chiqaring: qisqa rejim, hozirgina kirgan (oxirgi: 0); qisqa, 6.9-kunda faol; eslab qolingan, 2-kunda faol; eslab qolingan, 29-kunda faol. Ishora: toFixed(1) (Matn ↔ son va sonni tekshirish) va padEnd.

Yechim
js
const SOAT = 3_600_000;
const KUN = 24 * SOAT;

function qachonTugaydi(s) {
  const mutlaq = s.yaratilgan + s.muddat;
  const idle = s.oxirgi + s.idle;
  return mutlaq <= idle
    ? { vaqt: mutlaq, sabab: "mutlaq" }
    : { vaqt: idle, sabab: "idle" };
}

const QISQA = { yaratilgan: 0, muddat: 7 * KUN, idle: 12 * SOAT };
const ESLAB = { yaratilgan: 0, muddat: 30 * KUN, idle: 7 * KUN };

const SINOV = [
  ["qisqa, hozirgina kirdi", { ...QISQA, oxirgi: 0 }],
  ["qisqa, 6.9-kun faol", { ...QISQA, oxirgi: 6.9 * KUN }],
  ["eslab, 2-kun faol", { ...ESLAB, oxirgi: 2 * KUN }],
  ["eslab, 29-kun faol", { ...ESLAB, oxirgi: 29 * KUN }],
];
for (const [nom, s] of SINOV) {
  const { vaqt, sabab } = qachonTugaydi(s);
  const soat = (vaqt / SOAT).toFixed(1);
  console.log(nom.padEnd(23), soat, "soat,", sabab);
}

Konsolda:

text
qisqa, hozirgina kirdi  12.0 soat, idle
qisqa, 6.9-kun faol     168.0 soat, mutlaq
eslab, 2-kun faol       216.0 soat, idle
eslab, 29-kun faol      720.0 soat, mutlaq

Sliding faqat idle chegarani suradi: 6.9-kunda faol bo'lsa ham qisqa sessiya 168-soatda (7 kun) tugaydi. Eslab qolingan sessiyada 2-kundagi faollik idle'ni 9-kungacha (216 soat) surdi.

3-mashq (qiyin): 60 soniyalik qadam

yozuvlarSoni(sorovlar, qadam) ni yozing: so'rov vaqtlari (ms, o'sib boradi) bo'yicha oxirgi_faollik necha marta bazaga yozilishini sanasin. Qoida kanondagidek: t - oxirgi > qadam bo'lsa yoziladi va oxirgi = t; boshida oxirgi = 0 (sessiya yaratilgan payt). 10 daqiqa davomida har 5 soniyada bitta so'rov uchun qadam = 0 (har so'rovda) va qadam = 60000 ni solishtiring, oxirida bazadagi faollik oxirgi so'rovdan necha soniya orqada qolganini chiqaring. Ishora: Array.from({ length: 120 }, (_, i) => …) (flat, flatMap, fill va Array.from) va at(-1).

Yechim
js
const QADAM = 60_000;

// so'rovlar vaqtlari (ms) bo'yicha bazaga necha marta yozilishini
// sanaydi; oxirgi — sessiya yaratilgan payt
function yozuvlarSoni(sorovlar, qadam) {
  let oxirgi = 0;
  let yozuv = 0;
  for (const t of sorovlar) {
    if (t - oxirgi > qadam) {
      yozuv++;
      oxirgi = t;
    }
  }
  return { yozuv, oxirgi };
}

// 10 daqiqa davomida har 5 soniyada bitta so'rov
const sorovlar = Array.from(
  { length: 120 }, (_, i) => (i + 1) * 5000);
const har = yozuvlarSoni(sorovlar, 0);
const qadamli = yozuvlarSoni(sorovlar, QADAM);
console.log("so'rovlar:", sorovlar.length);
console.log("har so'rovda yozish:", har.yozuv);
console.log("60 s qadam bilan:", qadamli.yozuv);
const ortda = sorovlar.at(-1) - qadamli.oxirgi;
console.log("bazadagi faollik ortda qolgan:", ortda / 1000, "s");

Konsolda:

text
so'rovlar: 120
har so'rovda yozish: 120
60 s qadam bilan: 9
bazadagi faollik ortda qolgan: 15 s

120 o'rniga 9 ta yozuv — 13 marta kam. Narxi: bazadagi oxirgi_faollik 15 soniya orqada (umuman — 60 soniyagacha). 12 soatlik yoki 7 kunlik idle uchun bu sezilmaydi. Agar idle 5 daqiqa bo'lsa (bank), qadamni ham kichraytirish kerak bo'lardi.

10. Loyiha qadami: eslab qolish, qurilmalar va ADR

10.1 Migratsiya sql/113-sessiya-eslab-qol.sql

Izohlarni qisqartirdik, SQL kanondagidek:

sql
-- "Meni eslab qol" va sessiyaga xos bo'sh turish chegarasi.
-- Mavjud qatorlar — eslab qolingan (oldingi xulq o'zgarmaydi).
-- DEFAULT bilan ADD COLUMN — PG 11+ da jadvalni qayta yozmaydi.
ALTER TABLE sessiyalar
  ADD COLUMN eslab_qol boolean NOT NULL DEFAULT true;
ALTER TABLE sessiyalar
  ADD COLUMN idle_muddat interval NOT NULL
  DEFAULT interval '7 days';
ALTER TABLE sessiyalar ADD CONSTRAINT sessiyalar_idle_muddat_check
  CHECK (idle_muddat BETWEEN interval '1 minute'
                         AND interval '400 days');

Prisma tomoni: prisma/migrations/20261027130000_sessiya_eslab_qol/migration.sql (xuddi shu SQL) va schema.prisma dagi Sessiya modeliga ikki maydon. Prisma interval turini bilmaydi — u Unsupported bilan e'lon qilinadi (Prisma Migrate chuqur):

prisma
  eslabQol   Boolean                 @default(true) @map("eslab_qol")
  idleMuddat Unsupported("interval")
    @default(dbgenerated("'7 days'::interval")) @map("idle_muddat")

Sxemani lokal bazada (110–113 migratsiyalari bilan) tekshirdik — biz ishga tushirdik, PostgreSQL 17.6:

sql
SELECT column_name, column_default
FROM information_schema.columns
WHERE table_name = 'sessiyalar'
  AND column_name IN ('eslab_qol', 'idle_muddat')
ORDER BY column_name;

Natija:

text
 column_name |   column_default
-------------+--------------------
 eslab_qol   | true
 idle_muddat | '7 days'::interval
(2 rows)

Chegaradan tashqari qiymat:

sql
\set VERBOSITY terse
INSERT INTO sessiyalar (foydalanuvchi_id, token_hash, tugash_vaqti,
                        idle_muddat)
VALUES (1, sha256('namuna'), now() + interval '1 day',
        interval '2 years');

Natija:

text
ERROR:  new row for relation "sessiyalar" violates check constraint "sessiyalar_idle_muddat_check"

"Yangi qator sessiyalar_idle_muddat_check cheklovini buzadi". Kod xato qilib 2 yillik idle yozmoqchi bo'lsa, baza rad etadi.

10.2 Kanon topilmasi: 168:00:00

Kod idle'ni millisekundda beradi: $7 * interval '1 millisecond'. Testda kutilgan 7 days o'rniga boshqa narsa chiqdi:

sql
SELECT (604800000 * interval '1 millisecond')::text AS ms_dan,
       (interval '7 days')::text AS kunlik,
       604800000 * interval '1 millisecond' = interval '7 days'
         AS tengmi;

Natija:

text
  ms_dan   | kunlik | tengmi
-----------+--------+--------
 168:00:00 | 7 days | t
(1 row)

Postgres interval ni uch qismda saqlaydi: oylar, kunlar va mikrosekundlar (SQL'da sana va vaqt zonalari). Millisekunddan yasalgan interval "kunlar" qismiga emas, vaqt qismiga tushadi — 168:00:00. Vaqtni hisoblashda ular teng (t), faqat matn ko'rinishi boshqa. Kanon testi shunga moslandi va izoh qo'shildi. Bitta tuzoq: yozgi vaqtga o'tish bor zonalarda 1 day va 24 hours sanaga qo'shilganda farq qilishi mumkin; bizning server UTC'da ishlaydi.

10.3 Kod

src/auth/sessiya.ts da o'zgargan joylar (to'liq fayl telefon uchun bo'lingan; uzun tiplarni alohida nom bilan chiqardik, ma'no kanondagidek — tip, lint va 18 test o'tdi, biz tekshirdik). Sozlama — ikki rejim:

ts
const SOAT = 3600_000;
export const SUKUT: SessiyaSozlamasi = Object.freeze({
  idleMs: 7 * 24 * SOAT,
  muddatMs: 30 * 24 * SOAT,
  qisqaIdleMs: 12 * SOAT,
  qisqaMuddatMs: 7 * 24 * SOAT,
  secure: true,
});

yarat() — rejimga qarab muddat va idle tanlanadi va har sessiyaning o'z idle'i bazaga yoziladi:

ts
  async yarat(
    foydalanuvchiId: number,
    mijoz: Mijoz,
    eslabQol = true,
  ): Promise<{ token: string; tugash: Date; eslabQol: boolean }> {
    const token = randomBytes(32).toString("base64url");
    const [muddat, idle] = eslabQol
      ? [this.s.muddatMs, this.s.idleMs]
      : [this.s.qisqaMuddatMs, this.s.qisqaIdleMs];
    const [q] = await this.#sorov<{ tugash: Date }>(
      `INSERT INTO sessiyalar (foydalanuvchi_id, token_hash,
         tugash_vaqti, ip, user_agent, eslab_qol, idle_muddat)
       VALUES ($1, $2, now() + $3 * interval '1 millisecond', $4, $5,
         $6, $7 * interval '1 millisecond')
       RETURNING tugash_vaqti AS tugash`,
      [
        foydalanuvchiId,
        tokenHash(token),
        muddat,
        mijoz.ip ?? null,
        mijoz.userAgent?.slice(0, 300) ?? null,
        eslabQol,
        idle,
      ],
    );
    if (q === undefined) throw new Error("sessiya yaratilmadi");
    return { token, tugash: q.tugash, eslabQol };
  }

tekshir() dagi WHERE endi umumiy sozlama emas, qatorning o'z chegarasini oladi: AND s.oxirgi_faollik > now() - s.idle_muddat. Keyin sozlamani o'zgartirsangiz, mavjud sessiyalar o'z qoidasi bilan yashashda davom etadi.

Cookie — eslab qolinmasa Expires siz:

ts
  cookieQoy(
    res: Response,
    token: string,
    tugash: Date,
    eslabQol = true,
  ): void {
    const a = this.#atributlar();
    res.cookie(
      cookieNomi(this.s),
      token,
      eslabQol ? { ...a, expires: tugash } : a,
    );
  }

Qo'shimcha metodlar: royxat(foydalanuvchiId, joriyId) — faol sessiyalar (id, joriy, eslabQol, yaratilgan, oxirgiFaollik, tugash, ip, qurilma), egasiBekorQilsin(foydalanuvchiId, sessiyaId) — yuqoridagi IDOR'siz UPDATE, qurilma(ua) — «Qurilmalar ro'yxati» bo'limidagi funksiya.

10.4 Yo'llar, sxema va config

src/auth/sxema.ts — ro'yxat va kirish sxemalariga bitta maydon: eslabQol: z.boolean().default(false). src/auth/marshrut.ts — sessiyaBer(req, res, f, body.eslabQol) va ikki yangi yo'l (bo'lingan):

ts
  // qurilmalar ro'yxati va bittasini bekor qilish
  r.route("/sessiyalar")
    .get(async (req, res) => {
      if (req.sessiya === undefined) throw new KirilmaganXatosi();
      const { foydalanuvchi, id } = req.sessiya;
      res.json({
        sessiyalar: await sessiyalar.royxat(foydalanuvchi.id, id),
      });
    })
    .all(metodYoq("GET, HEAD"));
  const IdSxemasi = z.strictObject({
    id: z.coerce.number().int().positive()
      .max(Number.MAX_SAFE_INTEGER),
  });
  r.route("/sessiyalar/:id")
    .delete(tekshirib({ params: IdSxemasi },
      async ({ params }, req, res) => {
        if (req.sessiya === undefined) throw new KirilmaganXatosi();
        const fid = req.sessiya.foydalanuvchi.id;
        // Begona sessiya id — 404 (borligini oshkor qilmaymiz)
        if (!(await sessiyalar.egasiBekorQilsin(fid, params.id))) {
          throw new TopilmadiXatosi("Sessiya topilmadi",
            "sessiya-topilmadi", "Sessiya topilmadi");
        }
        await xizmat.hodisa({ tur: "sessiya_bekor",
          foydalanuvchiId: fid, ...mijoz(req),
          tafsilot: { sessiyaId: params.id } });
        if (params.id === req.sessiya.id) sessiyalar.cookieOchir(res);
        res.sendStatus(204);
      }))
    .all(metodYoq("DELETE"));

O'z sessiyasini o'chirsa — cookie ham o'chiriladi (cookieOchir). Jurnalga yangi hodisa turi — sessiya_bekor. GET /v1/auth/men endi sessiya.id va eslabQol ni ham qaytaradi. Config'ga ikki o'zgaruvchi: SESSIYA_QISQA_IDLE_DAQIQA (sukut 720) va SESSIYA_QISQA_MUDDAT_KUN (sukut 7).

Kanon tekshiruvining jonli qismida login endi eslab qolinmagan cookie beradi — Expires yo'q:

text
Set-Cookie: __Host-sessiya=<token>; Path=/; HttpOnly; Secure; SameSite=Lax
text
{"foydalanuvchi":{"id":1,"email":"aziz@example.com","toliqIsm":"Aziz Karimov"},"sessiya":{"id":1,"eslabQol":false,"yaratilgan":"2026-10-27T15:00:38.239Z","tugash":"2026-11-03T15:00:38.239Z"}}

Ikkinchi qator — /men javobi: tugash yaratilgandan aniq 7 kun keyin.

10.5 Testlar

test/sessiya.test.ts ga ikkita test qo'shildi. Birinchisining yadrosi (bo'lingan):

ts
const q = await kir();      // eslabQol berilmagan
assert.match(setCookie(q),
  /^__Host-sessiya=[^;]+; Path=\/; HttpOnly; Secure; SameSite=Lax$/);
const u = await kir(true);  // eslab qol
assert.match(setCookie(u), /Expires=/);
// 13 soat jimlik: qisqasi tugaydi, eslab qolingani tirik
await pool.query("UPDATE sessiyalar SET oxirgi_faollik = " +
  "now() - interval '13 hours'");
await request(app).get("/v1/auth/men")
  .set("Cookie", cookieniOl(q.headers["set-cookie"])).expect(401);
await request(app).get("/v1/auth/men")
  .set("Cookie", cookieniOl(u.headers["set-cookie"])).expect(200);

setCookie(r) — o'qish uchun qisqartirildi (kanonda Set-Cookie sarlavhasining birinchi qiymati to'g'ridan olinadi). Ikkinchi test: ikki qurilma, ro'yxatda 2 ta sessiya va joriysi "Chrome, Windows", javob matnida token yo'q. Dilnozaning sessiyasini o'chirish — 404 (Dilnoza tirik qoladi). O'z telefonini o'chirish — 204 (telefon 401, kompyuter 200); /sessiyalar/abc — 400.

Biz ishga tushirdik — lokal PostgreSQL 17.6 da (TEST_AUTH_DATABASE_URL — sxemasi qurilgan bo'sh baza), butun fayl, yangi ikki test qatori:

text
  ✔ meni eslab qol: belgilanmasa — Expires'siz cookie, idle 12 soat, mutlaq 7 kun; belgilansa — 30 kun / 7 kun (392.9703ms)
  ✔ qurilmalar: ro'yxat (joriy belgilangan, token yo'q), bittasini bekor qilish; begona sessiya — 404 (576.1921ms)
ℹ tests 18
ℹ pass 18
ℹ fail 0

Test nomlari boshidagi kanon ichki raqamini olib tashladik. Chiqishdan boshqa 16 test qatori va qolgan xulosa qatorlarini tushirib qoldirdik.

10.6 Tekshiruv va commit

Kanon tekshiruvi (2026-10-27, PostgreSQL 18.6): tip va lint xatosiz, testlar 149 (148 + 1 o'tkazilgan) — ikkita yangi; e2e 4/4; Prisma migratsiyalari va schema.prisma orasida farq yo'q.

bash
M="feat(auth): \"meni eslab qol\", sessiyaga xos idle, qurilmalar"
M+=" ro'yxati va bitta sessiyani bekor qilish;"
M+=" docs: ADR — brauzer uchun server sessiyasi; db: 113"
git add src test sql prisma docs/AUTH-QAROR.md
git commit -m "$M"

11. Real ishda

  • ADR ko'p jamoada docs/adr/0001-….md ko'rinishida raqamlanadi va kod bilan birga review qilinadi. Qaror o'zgarsa, eski ADR o'chirilmaydi — "o'rniga 0007 keldi" deb belgilanadi.
  • Faol sessiyalar sahifasi (Telegram, Google, banklar) — aynan bugungi royxat va egasiBekorQilsin. Ko'p xizmat yangi qurilmadan kirilganda xat ham yuboradi.
  • Express dunyosida express-session ning rolling: true sozlamasi — sliding expiration'ning o'zi. Biz uni o'z kodimizda, 60 s qadam bilan yozdik.
  • Intervyuda: "Session yoki JWT — qachon qaysi?", "JWT'ni qanday bekor qilasiz?", "Idle va absolute timeout farqi?", "Remember me qanday xavfsiz qilinadi?".

Xulosa

  • Bir saytdagi brauzer uchun server sessiyasi soddaroq, darhol bekor qilinadi va kichik; JWT 1.4 ms tez, lekin bekor qilinmaydi.
  • Qaror o'lchovlar bilan ADR'ga yoziladi; eng qimmat bo'lim — "qachon qayta ko'riladi".
  • Uzoq sessiya — ikki chegara: idle (sliding) va mutlaq; ikkalasi ham serverda.
  • "Meni eslab qol" sukut bo'yicha o'chiq; eslab qolinmagan cookie Expires siz, lekin chegara baribir bazada.
  • Qurilmalar ro'yxatida token yo'q, User-Agent — faqat ko'rsatish uchun; bitta sessiyani o'chirishda egasi sharti SQL'da, begonaga — 404.
  • Postgres interval millisekunddan 168:00:00 bo'ladi — 7 days ga teng, faqat yozilishi boshqa.

Keyingi dars: Email tasdiqlash va parol tiklash oqimlari — bir martalik tokenlar, email borligini oshkor qilmaydigan javob va parol tiklangandan keyin hamma sessiyalarni bekor qilish.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Session vs JWT qarori va uzoq sessiyalar: ADR, "meni eslab qol" va qurilmalar ro'yxati — IlmHamroh