IlmHamroh
JavaScript Full-stack/11-qism. Asinxron JavaScript va tarmoq24/32-dars22 daqiqa
Mundarija (32)

Token bilan so'rovlar: Authorization sarlavhasi va token yangilash navbati

Qisqacha: Login'dan keyin server ikki token beradi: qisqa muddatli kirish tokeni va uzoq muddatli yangilash tokeni. Har so'rovga Authorization: Bearer <kirish tokeni> sarlavhasi qo'shiladi. Server 401 qaytarsa — yangilash tokeni bilan yangi juft olinadi va so'rov qayta yuboriladi. Bir vaqtda bir nechta 401 kelsa, yangilash bir marta bajarilishi kerak: hamma so'rov bitta umumiy Promise'ni kutadi.

Bu darsda

  • Autentifikatsiya va avtorizatsiya farqini, kirish va yangilash tokenlari nega ikkita ekanini tushuntira olasiz.
  • Login qilib, Authorization: Bearer sarlavhasi bilan himoyalangan so'rov yuborasiz va 401 sabablarini o'qiysiz.
  • Muddati tugagan tokenni foydalanuvchi sezmasdan (jim) yangilaysiz.
  • Bir vaqtda kelgan uchta 401 ni bitta yangilash bilan hal qiladigan navbat yozasiz — fetch da ham, Axios interceptor'ida ham.
  • Cookie asosidagi autentifikatsiyada fetch ning credentials sozlamasini to'g'ri qo'yasiz va token qayerda saqlanishini tanlaysiz.

Oldin bilishingiz kerak: Axios, API klient qatlami, Request, Response, Headers va fetch opsiyalari, Promise kombinatorlari, CORS mijoz tomondan.

1. Nega bu kerak?

HTTP xotirasiz: har so'rov server uchun yangi tanishuv. Siz hozirgina login qildingiz, keyingi so'rovda esa server "siz kimsiz?" deydi. Har so'rovda parol yuborish — xavfli va noqulay.

Katta bir binoni tasavvur qiling. Kirishda qo'riqchi bor. Birinchi kuni kadrlar bo'limiga pasport ko'rsatasiz va ikki narsa olasiz:

  1. Bir kunlik propusk. Uni har eshikda qo'riqchiga ko'rsatasiz. Yo'qotsangiz — zarar katta emas: ertaga u baribir yaroqsiz.
  2. Shaxsiy guvohnoma. U uzoq muddatli. Uni faqat kadrlar bo'limida, propusk muddati tugaganda yangisini olish uchun ko'rsatasiz. Har eshikda olib yurmaysiz.

Veb ilovalar ham shunday ishlaydi. Propusk — kirish tokeni (access token), guvohnoma — yangilash tokeni (refresh token). Token — server bergan, sizni tanitadigan va soxtalashtirib bo'lmaydigan qisqa matn.

Ikki atamani ajratib olaylik:

  • Autentifikatsiya (authentication) — "siz kimsiz?" savoliga javob: login va parol, token. Qo'riqchi propuskdagi ismni tekshiradi.
  • Avtorizatsiya (authorization) — "sizga bu yerga kirish mumkinmi?" Propuskingiz bor, lekin direktor xonasiga ruxsat yo'q.

Bugungi dars birinchisi haqida: brauzer so'rovlariga "men — Aziz" degan isbotni to'g'ri qo'shish va u eskirganda jim yangilash. Bu har bir login'li ilovada bor: Telegram Web, Payme, Uzum Market — ular sizni har 15 daqiqada qayta login qildirmaydi.

2. Login va tokenlar

2.1 /kirish

Mashq API'da sinov foydalanuvchisi bor: login aziz, parol bahor123. ?muddat= — kirish tokenining umri soniyalarda (5 dan 300 gacha; standart 30):

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

async function kir(login, parol) {
  const javob = await fetch(`${ASOSIY_URL}/kirish?muddat=60`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ login, parol }),
  });
  const malumot = await javob.json();
  console.log(javob.status, Object.keys(malumot));
  return malumot;
}

const tokenlar = await kir("aziz", "bahor123");
console.log("Muddat:", tokenlar.muddat, "soniya");

await kir("aziz", "noto'g'ri");

Konsolda:

text
200 [ 'kirishToken', 'yangilashToken', 'muddat' ]
Muddat: 60 soniya
401 [ 'xato' ]

Muvaffaqiyatda uch maydon: kirishToken, yangilashToken va muddat. Noto'g'ri parolda — 401 va { xato: "Login yoki parol noto'g'ri" }. Server "login yo'q" yoki "parol xato" deb farqlamaydi — bu ataylab: aks holda hujumchi qaysi loginlar borligini bilib olardi.

Mashq API'da yangilash tokeni 600 soniya (10 daqiqa) yashaydi. Real ilovalarda odatda kirish tokeni 5–15 daqiqa, yangilash tokeni — kunlar yoki haftalar.

2.2 Token ichida nima bor

Tokenni oching — u ikki qism, nuqta bilan ajratilgan: eyJzdWIi... . kD3x.... Birinchi qism — yuk (payload): Base64url bilan kodlangan oddiy JSON. Base64'ni Typed arrays va binar ma'lumot darsida ko'rgansiz: baytlarni harf va raqamlar bilan yozish usuli, shifrlash emas. Base64url — uning URL'ga mos varianti.

Ikkinchi qism — imzo (signature): server sirli kalit bilan hisoblagan "muhr". Bu so'zni funksiya imzosi (parametrlar ro'yxati) bilan adashtirmang — bu yerda imzo hujjatdagi muhr ma'nosida.

Yukni istalgan kishi o'qiy oladi:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
const javob = await fetch(`${ASOSIY_URL}/kirish?muddat=60`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ login: "aziz", parol: "bahor123" }),
});
const { kirishToken } = await javob.json();

const [yuk, imzo] = kirishToken.split(".");
const base64 = yuk.replaceAll("-", "+").replaceAll("_", "/");
const malumot = JSON.parse(atob(base64));
console.log(malumot.sub, malumot.tur);
console.log("Umri:", malumot.exp - malumot.iat, "soniya");
console.log("Imzo uzunligi:", imzo.length);

Konsolda:

text
aziz kirish
Umri: 60 soniya
Imzo uzunligi: 43

atob — Base64 matnni oddiy matnga aylantiradigan brauzer funksiyasi (Node'da ham bor). Base64url'da + va / o'rniga - va _ ishlatiladi — URL'da xavfsiz bo'lishi uchun. Shuning uchun avval ularni qaytardik.

Yukda: sub — kim (subject), tur — qaysi token, iat — qachon berilgan (issued at), exp — qachon tugaydi (expires). Vaqtlar — 1970 yildan beri o'tgan soniyalar.

Ikki muhim xulosa:

  • Token shifrlanmagan. Uni ochish uchun hech qanday kalit kerak emas. Tokenga parol, karta raqami kabi maxfiy narsa yozilmaydi.
  • Token soxtalashtirilmaydi. Yukdagi sub ni "admin" ga o'zgartirsangiz, imzo mos kelmaydi va server "Token yaroqsiz" deydi. Imzoni faqat sirli kalitni biladigan server yasay oladi.

Siz ishda uchratadigan JWT (JSON Web Token) xuddi shunday, faqat uch qismli: sarlavha, yuk, imzo. Mashq API soddaroq, ikki qismli shaklni ishlatadi. JWT'ni to'liq JWT anatomiyasi darsida, backend qismida ko'ramiz.

Tekshirib ko'ring: Kimdir sizning kirish tokeningizni ko'chirib oldi. U tokenning yukidagi exp ni uzaytirib, abadiy ishlatishi mumkinmi?

Javob

Yo'q. Yuk o'zgarsa, imzo unga mos kelmaydi — server tokenni rad etadi. Lekin exp tugaguncha o'g'irlangan token ishlaydi. Kirish tokenining umri qisqa bo'lishining sababi shu: o'g'irlansa ham, zarar bir necha daqiqa bilan cheklanadi.

3. Authorization: Bearer

3.1 Himoyalangan so'rov

Token serverga Authorization sarlavhasida yuboriladi. Shakli: Bearer so'zi, bitta bo'sh joy, keyin token. "Bearer" — "egasi": "bu tokenni ko'rsatgan odamga ishon" degani.

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
const kirish = await fetch(`${ASOSIY_URL}/kirish`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ login: "aziz", parol: "bahor123" }),
});
const { kirishToken } = await kirish.json();

async function profilniOl(sarlavha) {
  const javob = await fetch(`${ASOSIY_URL}/profil`, {
    headers: sarlavha ? { Authorization: sarlavha } : {},
  });
  console.log(javob.status, await javob.json());
}

await profilniOl(`Bearer ${kirishToken}`);
await profilniOl();
await profilniOl(kirishToken);
await profilniOl("Bearer buzilgan.token");

Konsolda:

text
200 { login: 'aziz', ism: 'Aziz' }
401 { xato: "Token yo'q" }
401 { xato: 'Token yaroqsiz' }
401 { xato: 'Token yaroqsiz' }

To'rt holat:

  • To'g'ri sarlavha — profil keldi.
  • Sarlavha yo'q — "Token yo'q".
  • Bearer so'zisiz faqat token — "Token yaroqsiz". Server shaklni tekshiradi; so'zni unutish — eng ko'p uchraydigan xato.
  • Buzilgan token — imzo mos emas.

Muddati tugagan token esa o'zining xabarini beradi: Token muddati tugagan. Uni keyingi bo'limda jonli ko'ramiz.

401 javobida server yana bir sarlavha qo'shadi: WWW-Authenticate: Bearer — "menga Bearer token kerak". 401 Unauthorized — "siz kim ekaningizni isbotlamadingiz". Uni 403 Forbidden dan farqlang: "sizni taniyman, lekin bunga ruxsatingiz yo'q". 401 da token yangilashga urinish mantiqli, 403 da esa — yo'q: yangi token ham ruxsat bermaydi.

3.2 Muddat tugashini ko'ramiz

Natijada 5 soniyalik token olamiz. Profilni darhol so'raymiz, keyin 6 soniya kutib yana so'raymiz. Ikkinchi urinish 401 bersa — yangilash tokeni bilan yangi juft olamiz va uchinchi marta so'raymiz. Kutish paytida har soniya konsolga yoziladi:

html
<p>Natija konsolda ↓ (taxminan 7 soniya)</p>
<script>
  const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
  const kut = (ms) => new Promise((tayyor) => setTimeout(tayyor, ms));

  async function jsonYubor(yol, tana) {
    const javob = await fetch(ASOSIY_URL + yol, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(tana),
    });
    return javob.json();
  }

  async function profil(token) {
    const javob = await fetch(`${ASOSIY_URL}/profil`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const malumot = await javob.json();
    return `${javob.status} ${malumot.ism ?? malumot.xato}`;
  }

  async function sina() {
    const t = await jsonYubor("/kirish?muddat=5", {
      login: "aziz", parol: "bahor123",
    });
    console.log("Kirdik. Darhol:", await profil(t.kirishToken));
    for (let s = 1; s <= 6; s++) {
      await kut(1000);
      console.log(`${s} s...`);
    }
    console.log("Eski token:", await profil(t.kirishToken));
    const yangi = await jsonYubor("/yangila", {
      yangilashToken: t.yangilashToken,
    });
    console.log("Yangi token:", await profil(yangi.kirishToken));
  }

  sina();
</script>

Konsolda:

text
Kirdik. Darhol: 200 Aziz
1 s...
2 s...
3 s...
4 s...
5 s...
6 s...
Eski token: 401 Token muddati tugagan
Yangi token: 200 Aziz

Foydalanuvchi nuqtai nazaridan bu to'liq jarayon: token eskirdi, server 401 dedi, ilova yangilash tokeni bilan /yangila dan yangi juft oldi va ish davom etdi. Endi uni avtomatik — foydalanuvchi sezmaydigan qilib yozamiz.

Tekshirib ko'ring: Nega kirish tokenini darhol 10 daqiqa emas, 5 soniya qildik? Real ilovada 5 soniya to'g'rimi?

Javob

Darsda muddat tugashini kutib o'tirmaslik uchun. Real ilovada 5 soniya juda qisqa: har 5 soniyada yangilash so'rovi ketadi, server yuklanadi. Odatda 5–15 daqiqa tanlanadi — o'g'irlangan token uchun yetarlicha qisqa, yangilash uchun yetarlicha uzoq.

4. Jim yangilash (silent refresh)

4.1 Bitta so'rov uchun

Jim yangilash (silent refresh) — token eskirganda foydalanuvchiga hech narsa ko'rsatmay, uni yangilab, so'rovni qayta yuborish. Algoritm:

  1. So'rovni joriy kirish tokeni bilan yuboring.
  2. Javob 401 bo'lsa — /yangila ga yangilash tokenini yuboring va yangi juftni saqlang.
  3. So'rovni yangi token bilan bir marta qayta yuboring.
  4. Yangilash ham muvaffaqiyatsiz bo'lsa — foydalanuvchini tizimdan chiqaring ("Qaytadan kiring").

Sinash uchun bir hiyla ishlatamiz. Token muddati tugashini 5 soniya kutish o'rniga, login'dan keyin kirish tokenini ataylab buzamiz. Server uchun bu ham 401 — ilova uchun farqi yo'q:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
const tokenlar = { kirish: null, yangilash: null };

async function jsonYubor(yol, tana) {
  return fetch(ASOSIY_URL + yol, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(tana),
  });
}

function saqla(juft) {
  tokenlar.kirish = juft.kirishToken;
  tokenlar.yangilash = juft.yangilashToken;
}

async function tokenniYangila() {
  console.log("↻ token yangilanmoqda");
  const javob = await jsonYubor("/yangila", {
    yangilashToken: tokenlar.yangilash,
  });
  if (!javob.ok) throw new Error("Qaytadan kiring");
  saqla(await javob.json());
}

async function himoyalangan(yol) {
  const yubor = () => fetch(ASOSIY_URL + yol, {
    headers: { Authorization: `Bearer ${tokenlar.kirish}` },
  });
  let javob = await yubor();
  if (javob.status === 401) {
    await tokenniYangila();
    javob = await yubor();
  }
  return javob.json();
}

saqla(await (await jsonYubor("/kirish", {
  login: "aziz", parol: "bahor123",
})).json());

tokenlar.kirish = "eskirgan-token"; // sinov: token "eskirdi"
console.log((await himoyalangan("/profil")).ism);
console.log((await himoyalangan("/profil")).ism);

Konsolda:

text
↻ token yangilanmoqda
Aziz
Aziz

Birinchi so'rov 401 oldi, token yangilandi va so'rov qayta ketdi. Ikkinchisi esa yangi token bilan birinchi urinishdayoq o'tdi — yangilash bo'lmadi.

yubor — funksiya, chunki so'rovni ikki marta yaratamiz va ikkinchi marta tokenlar.kirish allaqachon yangi. Sarlavhani funksiyadan tashqarida bir marta yasasangiz, qayta urinish ham eski token bilan ketardi.

4.2 Muammo: bir vaqtda uchta 401

Sahifa ochilganda ilova odatda bir nechta so'rovni parallel yuboradi: profil, bildirishnomalar, buyurtmalar. Token eskirgan bo'lsa — uchalasi ham 401 oladi. Yuqoridagi himoyalangan bilan nima bo'ladi?

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
const tokenlar = { kirish: null, yangilash: null };
let yangilashlar = 0;

async function jsonYubor(yol, tana) {
  return fetch(ASOSIY_URL + yol, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(tana),
  });
}

function saqla(juft) {
  tokenlar.kirish = juft.kirishToken;
  tokenlar.yangilash = juft.yangilashToken;
}

async function tokenniYangila() {
  yangilashlar++;
  const javob = await jsonYubor("/yangila", {
    yangilashToken: tokenlar.yangilash,
  });
  if (!javob.ok) throw new Error("Qaytadan kiring");
  saqla(await javob.json());
}

async function himoyalangan(yol) {
  const yubor = () => fetch(ASOSIY_URL + yol, {
    headers: { Authorization: `Bearer ${tokenlar.kirish}` },
  });
  let javob = await yubor();
  if (javob.status === 401) {
    await tokenniYangila();
    javob = await yubor();
  }
  return javob.json();
}

saqla(await (await jsonYubor("/kirish", {
  login: "aziz", parol: "bahor123",
})).json());
tokenlar.kirish = "eskirgan-token";

await Promise.all([
  himoyalangan("/profil"),
  himoyalangan("/profil"),
  himoyalangan("/profil"),
]);
console.log("Yangilashlar:", yangilashlar); // Yangilashlar: 3

Uchta 401 — uchta yangilash (konsoldagi 3). Mashq API'da bu baribir ishladi, lekin real serverlarda ikki jiddiy muammo bor:

  • Bekorga yuk. 20 ta parallel so'rov — 20 ta yangilash.
  • Rotatsiya (rotation). Xavfsiz serverlar har yangilashda yangi yangilash tokeni beradi va eskisini o'chiradi. Birinchi yangilash eski tokenni "yoqib yuboradi". Ikkinchi va uchinchisi esa shu eski token bilan keladi — server ularni o'g'irlik deb biladi va foydalanuvchini butunlay tizimdan chiqaradi. Aziz sahifani ochdi va... login sahifasiga tushdi.

4.3 Yechim: yangilash navbati

G'oya: yangilash bitta Promise. Birinchi 401 uni boshlaydi va o'zgaruvchiga saqlaydi. Qolgan 401 lar yangi yangilash boshlamaydi — o'sha Promise'ni kutadi. Promise tugagach, o'zgaruvchi tozalanadi.

sequenceDiagram
  participant A as Profil
  participant B as Buyurtmalar
  participant Y as Yangilash
  participant S as Server
  A->>S: GET (eski token)
  B->>S: GET (eski token)
  S-->>A: 401
  A->>Y: yangilanish yo'q — boshlayman
  S-->>B: 401
  B->>Y: yangilanish bor — kutaman
  Y->>S: POST /yangila
  S-->>Y: yangi juft
  Y-->>A: tayyor
  Y-->>B: tayyor
  A->>S: GET (yangi token)
  B->>S: GET (yangi token)

Diagrammada "Buyurtmalar" ikkinchi yangilashni boshlamaydi — u allaqachon ketayotganini ko'radi va kutadi. Kod:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
const tokenlar = { kirish: null, yangilash: null };
let yangilashlar = 0;
let yangilanish = null;

async function jsonYubor(yol, tana) {
  return fetch(ASOSIY_URL + yol, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(tana),
  });
}

function saqla(juft) {
  tokenlar.kirish = juft.kirishToken;
  tokenlar.yangilash = juft.yangilashToken;
}

async function tokenniYangila() {
  yangilashlar++;
  const javob = await jsonYubor("/yangila", {
    yangilashToken: tokenlar.yangilash,
  });
  if (!javob.ok) throw new Error("Qaytadan kiring");
  saqla(await javob.json());
}

function bittaYangila() {
  if (!yangilanish) {
    yangilanish = tokenniYangila().finally(() => {
      yangilanish = null;
    });
  }
  return yangilanish;
}

async function himoyalangan(yol) {
  const yubor = (token) => fetch(ASOSIY_URL + yol, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const ishlatilgan = tokenlar.kirish;
  let javob = await yubor(ishlatilgan);
  if (javob.status === 401) {
    if (tokenlar.kirish === ishlatilgan) await bittaYangila();
    javob = await yubor(tokenlar.kirish);
  }
  return javob.json();
}

saqla(await (await jsonYubor("/kirish", {
  login: "aziz", parol: "bahor123",
})).json());
tokenlar.kirish = "eskirgan-token";

const profillar = await Promise.all([
  himoyalangan("/profil"),
  himoyalangan("/profil"),
  himoyalangan("/profil"),
]);
console.log(profillar.map((p) => p.ism));
console.log("Yangilashlar soni:", yangilashlar);

Konsolda:

text
[ 'Aziz', 'Aziz', 'Aziz' ]
Yangilashlar soni: 1

Uchta so'rov, bitta yangilash. Uch qism muhim:

  • bittaYangila — yangilash ketayotgan bo'lsa (yangilanish bor), o'shani qaytaradi. Yo'q bo'lsa — boshlaydi. Bitta Promise'ni istalgancha await qilish mumkin — hammasi bir xil natijani oladi (Promise asoslari: Promise bir marta hal bo'ladi).
  • .finally(...) — yangilash tugagach (muvaffaqiyatli yoki xato bilan), o'zgaruvchi tozalanadi. Aks holda keyingi safar token yana eskirganda, eski — allaqachon tugagan — Promise qaytarilardi va yangilash bo'lmasdi.
  • ishlatilgan tokenni solishtirish. Sekin so'rov 401 ni yangilash tugagandan keyin olishi mumkin. Unda token allaqachon yangi: tokenlar.kirish !== ishlatilgan. Yana yangilash shart emas — faqat yangi token bilan qayta yuborish kifoya.

Bu naqsh — Asinxron naqshlar dagi "bir xil ishni bir marta bajarish" (request deduplication) ning amaliy ko'rinishi.

5. Axios interceptor bilan

Axios darsida interceptor'larni "nazorat punkti" dedik. Token uchun ular ayniqsa qulay: so'rov interceptor'i har so'rovga token qo'shadi, javob interceptor'i 401 ni ushlab, yangilab, qayta yuboradi. Interfeys kodi esa tokenlar haqida umuman bilmaydi:

html
<p>Natija konsolda ↓</p>
<script type="module">
  import axios from "https://cdn.jsdelivr.net/npm/axios@1.20.0/+esm";

  const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
  const tokenlar = { kirish: null, yangilash: null };
  let yangilanish = null;
  let yangilashlar = 0;

  const api = axios.create({ baseURL: ASOSIY_URL, timeout: 8000 });

  async function tokenniYangila() {
    yangilashlar++;
    const { data } = await axios.post(`${ASOSIY_URL}/yangila`, {
      yangilashToken: tokenlar.yangilash,
    });
    tokenlar.kirish = data.kirishToken;
    tokenlar.yangilash = data.yangilashToken;
  }

  function bittaYangila() {
    yangilanish ??= tokenniYangila().finally(() => {
      yangilanish = null;
    });
    return yangilanish;
  }

  api.interceptors.request.use((config) => {
    config.headers.Authorization = `Bearer ${tokenlar.kirish}`;
    return config;
  });

  api.interceptors.response.use(
    (javob) => javob,
    async (xato) => {
      const config = xato.config;
      if (xato.response?.status !== 401 || config.qaytaYuborildi) {
        return Promise.reject(xato);
      }
      config.qaytaYuborildi = true;
      await bittaYangila();
      return api(config);
    },
  );

  const { data } = await axios.post(`${ASOSIY_URL}/kirish`, {
    login: "aziz", parol: "bahor123",
  });
  tokenlar.kirish = "eskirgan-token";
  tokenlar.yangilash = data.yangilashToken;

  const javoblar = await Promise.all([
    api.get("/profil"), api.get("/profil"), api.get("/profil"),
  ]);
  console.log(javoblar.map((j) => j.data.ism));
  console.log("Yangilashlar soni:", yangilashlar);
</script>

Konsolda:

text
(3) ['Aziz', 'Aziz', 'Aziz']
Yangilashlar soni: 1

Uch tafsilot:

  • return api(config) — Axios instance'ni funksiya sifatida chaqirsa bo'ladi: u shu sozlama bilan so'rovni qayta yuboradi. So'rov yana so'rov interceptor'idan o'tadi va yangi tokenni oladi. Javob interceptor'ining natijasi — qayta yuborilgan so'rovning javobi. await api.get(...) uni oddiy javob kabi oladi.
  • config.qaytaYuborildi — o'zimiz qo'ygan belgi. Yangi token bilan ham 401 kelsa (masalan, foydalanuvchi bloklangan), cheksiz "401 → yangila → 401" aylanishi bo'lmaydi: ikkinchi 401 to'g'ridan-to'g'ri catch ga ketadi.
  • Yangilash api orqali emas, oddiy axios.post bilan. /yangila ham 401 qaytarishi mumkin (yangilash tokeni eskirgan). U api orqali ketsa, interceptor uni ham ushlab, yana yangilashga urinardi — o'zini o'zi kutib qotib qoladigan holat.

DevTools konsolida esa uchta qizil GET .../profil 401 qatorini ko'rasiz. Bu — brauzerning tarmoq yozuvi, kodingizdagi xato emas: 401 lar ushlandi va so'rovlar qayta muvaffaqiyatli ketdi (Network panelida uchta 401 va uchta 200).

Operator ??= — "chap tomon null yoki undefined bo'lsa, o'ngdagini yoz" (??, ?. va mantiqiy tayinlash). if (!yangilanish) yangilanish = ... ning qisqasi.

Tekshirib ko'ring: Javob interceptor'idan config.qaytaYuborildi tekshiruvini olib tashlasak va server foydalanuvchini bloklagan bo'lsa (har doim 401), nima bo'ladi?

Javob

Cheksiz sikl: 401 → yangilash → qayta yuborish → yana 401 → yana yangilash ... Brauzer serverga to'xtovsiz so'rov yog'diradi, mashq API esa daqiqasiga 120 tadan keyin 429 bilan to'xtatadi. Belgi har so'rovga faqat bitta qayta urinish beradi.

6.1 Brauzer o'zi yuboradi

Tokenni JavaScript o'zgaruvchisida saqlab, sarlavhaga qo'lda qo'yish — bir yo'l. Ikkinchi, ko'p saytlarda uchraydigan yo'l — cookie. Server login javobida Set-Cookie sarlavhasi bilan kichik matn yuboradi, brauzer uni saqlaydi va shu saytga har so'rovga o'zi qo'shadi. JavaScript hech narsa qilmaydi.

Cookie'ning eng muhim belgisi — HttpOnly: bunday cookie'ni JavaScript umuman ko'rmaydi va o'qiy olmaydi. Sahifaga begona skript tushib qolsa ham, tokenni o'g'irlay olmaydi. Cookie'larni to'liq Cookie'lar JS'dan darsida ko'ramiz.

6.2 credentials

fetch cookie'ni qachon yuborishini credentials sozlamasi belgilaydi (Request, Response, Headers):

credentials Cookie qachon ketadi
"same-origin" (standart) faqat o'z saytingizga
"include" boshqa saytga ham
"omit" hech qachon

Frontend app.example.com da, API esa api.example.com da bo'lsa — bu boshqa origin. Cookie ketishi uchun:

js
const javob = await fetch("https://api.example.com/profil", {
  credentials: "include",
});

Lekin bu yetarli emas. CORS mijoz tomondan darsidagi qoidani eslang: server ham ruxsat berishi kerak. Cookie'li so'rovda server javobida ikki sarlavha bo'lishi shart:

  • Access-Control-Allow-Credentials: true;
  • Access-Control-Allow-Origin: https://app.example.com — aniq manzil, * emas.

* bilan cookie'li so'rov ishlamaydi — brauzer javobni bloklaydi. Mashq API shuning uchun cookie ishlatmaydi: u hamma saytga ochiq (*) bo'lishi kerak. Axios'da xuddi shu sozlama — withCredentials: true.

6.3 Token qayerda saqlanadi?

Jadvalda bitta yangi so'z bor — CSRF: begona sayt sizning brauzeringiz nomidan, cookie bilan so'rov yuborishi. Cookie har so'rovga o'zi qo'shilgani uchun shunday hujum mumkin bo'ladi.

Joy XSS (begona skript) Sahifa yangilansa Izoh
JS o'zgaruvchisi (xotira) o'g'irlash qiyin yo'qoladi yangilash tokeni bilan qayta olinadi
localStorage o'g'irlanadi saqlanadi eng oddiy, eng xavfli
HttpOnly cookie o'qib bo'lmaydi saqlanadi CSRF'dan himoya kerak

Ko'p zamonaviy ilovalar aralash yo'l tanlaydi: kirish tokeni — xotirada (o'zgaruvchida), yangilash tokeni — HttpOnly cookie'da. Sahifa yangilanganda ilova /yangila ga murojaat qiladi, brauzer cookie'ni o'zi qo'shadi va yangi kirish tokeni xotiraga tushadi.

localStorage ni localStorage va sessionStorage darsida, ikki darsdan keyin o'rganamiz. CSRF himoyasi — SameSite belgisi va boshqalar — ni Tokenni qayerda saqlash va CSRF darsida, backend qismida ko'ramiz.

7. Hujumchi nigohi

Token — eshik kaliti. Hujumchi uni olishning to'rt yo'lini sinaydi:

  • XSS orqali o'g'irlash. Sahifaga begona skript tushsa (masalan, sharh innerHTML bilan chiqarilsa), u localStorage.getItem("token") ni o'qib, o'z serveriga yuboradi. Himoya: foydalanuvchi matni — faqat textContent (Kontentni o'zgartirish va XSS xavfi); uzoq muddatli token — HttpOnly cookie'da; kirish tokeni — qisqa muddatli.
  • Token URL'da. ?token=eyJ... — URL server loglarida, brauzer tarixida, Referer sarlavhasida qoladi va boshqa saytlarga sizib chiqadi. Himoya: token faqat Authorization sarlavhasida yoki cookie'da. Hech qachon URL'da emas.
  • Konsol va loglar. console.log(tokenlar) — ishlab chiqarishda qolib ketsa, xatolarni yig'adigan xizmatlarga (Sentry kabi) token ham tushadi. Himoya: tokenni hech qachon loglamang; xato xabariga sarlavhalarni qo'shmang.
  • Yangilash tokenini ko'p joyga yuborish. Yangilash tokeni har so'rovda ketsa, o'g'irlanish ehtimoli o'sha so'rovlar soniga ko'payadi. Himoya: yangilash tokeni faqat /yangila ga ketadi — bizning koddagi kabi. Rotatsiya bilan esa o'g'irlangan eski token ishlatilsa, server buni sezadi (Access va refresh token strategiyasi).

Va bitta umumiy qoida: yangilash muvaffaqiyatsiz bo'lsa — tokenlarni o'chiring va login sahifasiga yuboring. Eskirgan tokenlarni "balki ishlab qolar" deb saqlab qolmang.

8. Ko'p uchraydigan xatolar

8.1 Bearer siz yoki bo'sh joysiz

Authorization: token yoki Bearer${token} — server "Token yaroqsiz" deydi. Tuzatish: aynan `Bearer ${token}` — so'z, bitta bo'sh joy, token.

8.2 Sarlavhani bir marta yasash

const sarlavha = { Authorization: ... } ni funksiya boshida bir marta yasab, qayta urinishda ham ishlatish — qayta urinish eski token bilan ketadi va yana 401. Tuzatish: sarlavhani har yuborishda joriy tokendan yasang (yubor() funksiyasi kabi).

8.3 Har 401 da alohida yangilash

Parallel so'rovlar — bir nechta yangilash, rotatsiyali serverda esa majburiy logout. Tuzatish: bittaYangila navbati.

8.4 Cheksiz yangilash sikli

/yangila ning o'zi 401 qaytaradi, interceptor uni ham ushlaydi va yana yangilaydi. Yoki yangi token bilan ham 401 kelaveradi. Tuzatish: yangilash so'rovi interceptor'li instance'dan o'tmasin; har so'rovga faqat bitta qayta urinish (qaytaYuborildi).

8.5 finally siz navbat

yangilanish o'zgaruvchisi tozalanmasa, keyingi safar u allaqachon bajarilgan Promise'ni qaytaradi — token yangilanmaydi, hamma so'rov 401 bilan qaytadi. Tuzatish: .finally(() => { yangilanish = null; }).

8.6 403 da yangilash

403 — "ruxsat yo'q", token esa yaroqli. Yangilash befoyda va yuk. Tuzatish: faqat 401 da yangilang.

9. Mashqlar

1-mashq (oson): Kirish xatosi

kir(login, parol) funksiyasini yozing: muvaffaqiyatda tokenlar juftini qaytarsin, 401 da Error tashlasin — matni serverning xato maydonidan. aziz/bahor123 va aziz/12345 bilan sinang; birinchisida muddat ni, ikkinchisida xato matnini chiqaring.

Yechim
js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

async function kir(login, parol) {
  const javob = await fetch(`${ASOSIY_URL}/kirish`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ login, parol }),
  });
  const malumot = await javob.json();
  if (!javob.ok) {
    throw new Error(malumot.xato);
  }
  return malumot;
}

const juft = await kir("aziz", "bahor123");
console.log("Muddat:", juft.muddat); // Muddat: 30

try {
  await kir("aziz", "12345");
} catch (xato) {
  console.log(xato.message); // Login yoki parol noto'g'ri
}

?muddat= berilmadi — server standart 30 soniyani berdi.

2-mashq (o'rta): Token eskirdimi?

Ba'zi ilovalar 401 ni kutmay, token tugashiga oz qolganda oldindan yangilaydi. tokenEskirdimi(token, hozir, zaxira) funksiyasini yozing: tokenning yukidagi exp ni o'qisin va exp - zaxira <= hozir bo'lsa true qaytarsin (hozir va zaxira — soniyalarda). Tarmoqsiz sinash uchun tokenni o'zingiz yasang: yuk — {"sub":"aziz","exp":1000} ning Base64url ko'rinishi, imzo — istalgan matn.

Ishora: btoa — atob ning teskarisi (matn → Base64).

Yechim
js
function yukniOqi(token) {
  const yuk = token.split(".")[0];
  const base64 = yuk.replaceAll("-", "+").replaceAll("_", "/");
  return JSON.parse(atob(base64));
}

function tokenEskirdimi(token, hozir, zaxira = 0) {
  const { exp } = yukniOqi(token);
  return exp - zaxira <= hozir;
}

const yuk = btoa(JSON.stringify({ sub: "aziz", exp: 1000 }))
  .replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
const token = `${yuk}.soxta-imzo`;

console.log(tokenEskirdimi(token, 900)); // false
console.log(tokenEskirdimi(token, 1000)); // true
console.log(tokenEskirdimi(token, 990, 30)); // true

Uchinchi qator: 990 da tokenning 10 soniyasi qoldi, zaxira esa 30 — yangilash vaqti keldi. replaceAll("=", "") — Base64url odatda oxiridagi = to'ldiruvchilarsiz yoziladi.

Muhim: bu tekshiruv faqat qulaylik uchun. Foydalanuvchi kompyuterining soati noto'g'ri bo'lishi mumkin, shuning uchun 401 da yangilash baribir kerak. Va imzoni brauzerda tekshirib bo'lmaydi — buni faqat server qiladi.

3-mashq (qiyin): Yangilash muvaffaqiyatsiz bo'lsa — chiqish

«Yangilash navbati» bo'limidagi kodni o'zgartiring: /yangila muvaffaqiyatsiz bo'lsa, chiqish() funksiyasi chaqirilsin — u tokenlarni null qilsin va "Qaytadan kiring" deb yozsin. Bir vaqtda kelgan uchta so'rov uchun chiqish() bir marta chaqirilsin. Sinash uchun login'dan keyin ikkala tokenni ham buzing. Promise.allSettled (Promise kombinatorlari) bilan uchala natija holatini chiqaring. Faylni kurs/mashqlar/11/24-token/chiqish.mjs ga saqlang.

Yechim
js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
const tokenlar = { kirish: null, yangilash: null };
let yangilanish = null;

async function jsonYubor(yol, tana) {
  return fetch(ASOSIY_URL + yol, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(tana),
  });
}

function saqla(juft) {
  tokenlar.kirish = juft.kirishToken;
  tokenlar.yangilash = juft.yangilashToken;
}

function chiqish() {
  tokenlar.kirish = null;
  tokenlar.yangilash = null;
  console.log("Qaytadan kiring");
}

async function tokenniYangila() {
  const javob = await jsonYubor("/yangila", {
    yangilashToken: tokenlar.yangilash,
  });
  if (!javob.ok) {
    chiqish();
    throw new Error("Sessiya tugadi");
  }
  saqla(await javob.json());
}

function bittaYangila() {
  yangilanish ??= tokenniYangila().finally(() => {
    yangilanish = null;
  });
  return yangilanish;
}

async function himoyalangan(yol) {
  const yubor = (token) => fetch(ASOSIY_URL + yol, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const ishlatilgan = tokenlar.kirish;
  let javob = await yubor(ishlatilgan);
  if (javob.status === 401) {
    if (tokenlar.kirish === ishlatilgan) await bittaYangila();
    javob = await yubor(tokenlar.kirish);
  }
  return javob.json();
}

saqla(await (await jsonYubor("/kirish", {
  login: "aziz", parol: "bahor123",
})).json());
tokenlar.kirish = "eskirgan";
tokenlar.yangilash = "eskirgan";

const natijalar = await Promise.allSettled([
  himoyalangan("/profil"),
  himoyalangan("/profil"),
  himoyalangan("/profil"),
]);
for (const n of natijalar) {
  console.log(n.status, n.reason?.message);
}

Konsolda:

text
Qaytadan kiring
rejected Sessiya tugadi
rejected Sessiya tugadi
rejected Sessiya tugadi

chiqish() bir marta chaqirildi, chunki u tokenniYangila ichida, tokenniYangila esa navbat tufayli bir marta ishladi. Uchala so'rov ham bir xil rad etilgan Promise'ni kutgan edi — hammasi bir xil xatoni oldi. Promise.allSettled hech birini yo'qotmay, uchala natijani ham ko'rsatdi.

bash
git add 11/24-token/chiqish.mjs
git commit -m "11/24: yangilash muvaffaqiyatsizda bir marta chiqish"

10. Real ishda

  • SPA'lar (bitta sahifali ilovalar — oddiy SPA router darsida o'zimiz yasaymiz). React, Vue ilovalarida aynan shu Axios interceptor yoki fetch o'rami + yangilash navbati yoziladi. React'da auth holatini butun ilovaga tarqatish — SPA'da autentifikatsiya oqimi.
  • Mobil ilovalar. Payme, Click, Uzum ilovalari ham kirish va yangilash tokenlari bilan ishlaydi — shuning uchun ularda har kuni qayta login qilmaysiz. Mexanizm bir xil, faqat tokenlar telefonning xavfsiz xotirasida.
  • Backend tomoni. Tokenlarni qanday yaratish, imzolash, rotatsiya va o'g'irlikni aniqlash — Access va refresh token strategiyasi. "Sizga token umuman kerakmi yoki sessiya yetadimi?" — Session vs JWT qarori.
  • Intervyu. "Access va refresh token farqi?", "Token qayerda saqlanadi?", "Bir nechta 401 kelsa nima qilasiz?" — o'rta darajali (middle) frontend dasturchi intervyusining klassik savollari.

Xulosa

  • Autentifikatsiya — kim ekanini isbotlash, avtorizatsiya — nimaga ruxsat borligi. 401 — "kimligingiz isbotlanmadi", 403 — "ruxsat yo'q".
  • Kirish tokeni — qisqa muddatli, har so'rovda Authorization: Bearer <token>. Yangilash tokeni — uzoq muddatli, faqat /yangila ga.
  • Token yuki shifrlanmagan (Base64url), lekin imzo uni soxtalashtirishdan saqlaydi.
  • Jim yangilash: 401 → yangilash → yangi token bilan bir marta qayta yuborish → muvaffaqiyatsiz bo'lsa chiqish.
  • Parallel 401 lar — bitta umumiy yangilash Promise'i (bittaYangila), finally bilan tozalanadi.
  • Cookie'li auth: credentials: "include" + server tomonda aniq origin va Allow-Credentials; HttpOnly cookie'ni JS o'qiy olmaydi.

Keyingi dars: WebSocket va Server-Sent Events — so'rov-javobdan doimiy ulanishga: server o'zi xabar yuborganda brauzer uni qanday oladi.

Manbalar

  • RFC 6750: "The OAuth 2.0 Authorization Framework: Bearer Token Usage" — rfc-editor.org
  • MDN: "Authorization", "WWW-Authenticate", "401 Unauthorized", "403 Forbidden", "Request: credentials" — developer.mozilla.org
  • OWASP: "JSON Web Token Cheat Sheet", "HTML5 Security Cheat Sheet" (localStorage va tokenlar) — cheatsheetseries.owasp.org
  • Axios hujjatlari: "Interceptors" — axios-http.com/docs/interceptors
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Token bilan so'rovlar: Authorization sarlavhasi va token yangilash navbati — IlmHamroh