17-bo‘lim

Autentifikatsiya va sessiyalar

Parolni scrypt bilan xeshlash, tuz va vaqtga bardoshli taqqoslash, cookie bayroqlari, sessiya saqlash va himoya qatlami.

🕑 10 daqiqa o‘qish 📄 894 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Parolni hech qachon ochiq saqlamang
  2. scrypt bilan xeshlash
  3. timingSafeEqual nima uchun
  4. Cookie o'qish va yozish
  5. Sessiya saqlash
  6. Rollar bilan avtorizatsiya
  7. Sessiya yoki token
  8. Xulosa

Autentifikatsiya - "siz kimsiz" degan savolga javob. Avtorizatsiya - "sizga nima ruxsat" degan savolga.

Bu bo'lim birinchisi haqida, lekin oxirida ikkinchisiga ham tegamiz.

Parolni hech qachon ochiq saqlamang #

Parol bazada o'qiladigan holda turmasin

Agar baza sizib chiqsa - va bu har kuni kimdir bilan sodir bo'ladi - ochiq parollar darhol hujumchi qo'liga o'tadi.

Bundan ham yomoni: odamlar parolni takrorlaydi. Sizning saytingizdan o'g'irlangan parol ularning pochtasini ham ochadi.

Shifrlash ham yechim emas: shifrni ochadigan kalit o'sha serverda turadi.

To'g'ri yo'l - xeshlash: parolni qaytarib bo'lmaydigan qilib aylantirish. Tekshirganda kiritilgan parol qayta xeshlanadi va natijalar solishtiriladi.

Va muhimi: md5 yoki sha256 parol uchun yaramaydi. Ular ataylab tez ishlaydi, demak hujumchi sekundiga milliardlab variantni sinay oladi.

Parol uchun ataylab sekin algoritmlar kerak: scrypt, bcrypt, argon2. Node da scrypt tilning ichida bor.

scrypt bilan xeshlash #

JavaScript
import { scryptSync, randomBytes, timingSafeEqual } from "node:crypto";

function parolniXeshla(parol) {
    const tuz = randomBytes(16);
    const xesh = scryptSync(parol, tuz, 64);
    return `${tuz.toString("hex")}:${xesh.toString("hex")}`;
}

function parolTogrimi(parol, saqlangan) {
    const [tuzHex, xeshHex] = saqlangan.split(":");
    const tuz = Buffer.from(tuzHex, "hex");
    const saqlanganXesh = Buffer.from(xeshHex, "hex");
    const yangiXesh = scryptSync(parol, tuz, 64);
    return timingSafeEqual(saqlanganXesh, yangiXesh);
}

const saqlangan = parolniXeshla("MeningParolim123");

console.log("Uzunligi:", saqlangan.length);
console.log("Ikki qismdan iborat:", saqlangan.split(":").length === 2);
console.log("Parol ochiq ko'rinmaydi:", !saqlangan.includes("Parolim"));

console.log("To'g'ri parol:", parolTogrimi("MeningParolim123", saqlangan));
console.log("Noto'g'ri parol:", parolTogrimi("boshqaParol", saqlangan));

const ikkinchi = parolniXeshla("MeningParolim123");
console.log("Bir xil parol, boshqa xesh:", saqlangan !== ikkinchi);
Natija
Uzunligi: 161
Ikki qismdan iborat: true
Parol ochiq ko'rinmaydi: true
To'g'ri parol: true
Noto'g'ri parol: false
Bir xil parol, boshqa xesh: true

Oxirgi qatorga e'tibor bering: bir xil parol ikki xil xesh berdi.

Tuz nima uchun kerak

Tuz (salt) - har bir parol uchun yaratiladigan tasodifiy baytlar. U parol bilan birga xeshlanadi va xesh yonida ochiq saqlanadi.

Nega kerak: tuzsiz bir xil parollar bir xil xesh beradi. Hujumchi bazani ko'rib, "bu ikki odamda bir xil parol" deb bilib oladi. Undan ham yomoni, u oldindan hisoblangan jadval (rainbow table) bilan millionlab mashhur parolni bir zumda ochadi.

Tuz bilan esa har bir parol alohida hujum qilinishi kerak bo'ladi - va scrypt buni ataylab sekin qiladi.

Tuzni yashirish shart emas: uning vazifasi maxfiylik emas, takrorlanmaslik.

timingSafeEqual nima uchun #

JavaScript
import { timingSafeEqual } from "node:crypto";

const haqiqiy = Buffer.from("maxfiy-token-12345");
const togri = Buffer.from("maxfiy-token-12345");
const notogri = Buffer.from("maxfiy-token-99999");

console.log("Teng:", timingSafeEqual(haqiqiy, togri));
console.log("Teng emas:", timingSafeEqual(haqiqiy, notogri));

const boshqaUzunlik = Buffer.from("qisqa");
try {
    timingSafeEqual(haqiqiy, boshqaUzunlik);
} catch (xato) {
    console.log("Uzunlik farqi:", xato.code);
}
Natija
Teng: true
Teng emas: false
Uzunlik farqi: ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH
Oddiy === parolni sotib qo'yishi mumkin

a === b satrlarni chapdan o'ngga solishtiradi va birinchi farqda to'xtaydi.

Demak "aaaa" bilan taqqoslash "zzzz" bilan taqqoslashdan ozgina tezroq tugaydi - agar to'g'ri javob "abbb" bo'lsa.

Bu farq mikrosoniyalarda o'lchanadi, lekin hujumchi millionlab urinish qilib, o'rtachasini olsa, tokenni belgi-belgi topib olishi mumkin. Bu vaqt bo'yicha hujum deb ataladi.

timingSafeEqual esa doim butun buferni tekshiradi - natija qanday bo'lishidan qat'i nazar bir xil vaqt ketadi.

Uni parol xeshlari, tokenlar va imzolar uchun ishlating. Oddiy matn uchun kerak emas.

Diqqat: u uzunliklar teng bo'lishini talab qiladi - aks holda xato tashlaydi.

JavaScript
import express from "express";

const app = express();

function cookieOqi(sarlavha) {
    const natija = {};
    for (const bolak of (sarlavha ?? "").split(";")) {
        const [nom, ...qolgani] = bolak.trim().split("=");
        if (nom) natija[nom] = decodeURIComponent(qolgani.join("="));
    }
    return natija;
}

app.use((sorov, javob, keyingi) => {
    sorov.cookies = cookieOqi(sorov.headers.cookie);
    keyingi();
});

app.get("/qoy", (sorov, javob) => {
    javob.cookie("sessiya", "abc123", {
        httpOnly: true,
        sameSite: "lax",
        maxAge: 3600_000,
    });
    javob.json({ qoyildi: true });
});

app.get("/oqi", (sorov, javob) => {
    javob.json({ sessiya: sorov.cookies.sessiya ?? null });
});

const server = app.listen(0);
const port = server.address().port;
const asos = `http://localhost:${port}`;

const qoyildi = await fetch(asos + "/qoy");
const cookie = qoyildi.headers.getSetCookie()[0];
console.log("HttpOnly bor:", cookie.includes("HttpOnly"));
console.log("SameSite bor:", cookie.includes("SameSite=Lax"));

const oqildi = await fetch(asos + "/oqi", {
    headers: { Cookie: "sessiya=abc123" },
});
console.log(await oqildi.json());

server.close();
Natija
HttpOnly bor: true
SameSite bor: true
{ sessiya: 'abc123' }
BayroqNima qiladi
httpOnlyJavaScript o'qiy olmaydi
secureFaqat HTTPS orqali yuboriladi
sameSiteBoshqa saytdan yuborilishini cheklaydi
maxAgeQancha yashaydi
Uchta bayroq - uchta hujumdan himoya

Sessiya cookie sida bu bayroqlar majburiy:

httpOnly: true - sahifaga tushgan zararli skript document.cookie orqali sessiyani o'g'irlay olmaydi. Bu XSS hujumining eng katta zararini kamaytiradi (18-bo'lim).

secure: true - cookie faqat HTTPS orqali yuboriladi. Usiz u ochiq tarmoqda oddiy matn bo'lib uchadi.

sameSite: "lax" yoki "strict" - boshqa sayt sizning nomingizdan so'rov yuborganda cookie qo'shilmaydi. Bu CSRF hujumidan himoya.

Yuqoridagi misolda secure yo'q, chunki u localhost da HTTP bilan ishlayapti. Ishlab chiqarishda uni albatta qo'ying:

JavaScript
secure: process.env.NODE_ENV === "production"

Sessiya saqlash #

JavaScript
import express from "express";
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";

const foydalanuvchilar = new Map();
const sessiyalar = new Map();

function royxatdanOtkaz(login, parol) {
    const tuz = randomBytes(16);
    const xesh = scryptSync(parol, tuz, 64);
    foydalanuvchilar.set(login, { login, tuz, xesh, rol: "oddiy" });
}

function tekshir(login, parol) {
    const f = foydalanuvchilar.get(login);
    if (!f) return null;
    const yangi = scryptSync(parol, f.tuz, 64);
    return timingSafeEqual(f.xesh, yangi) ? f : null;
}

royxatdanOtkaz("malika", "KuchliParol1");

const app = express();
app.use(express.json());

app.use((sorov, javob, keyingi) => {
    const xom = sorov.headers.cookie ?? "";
    const mos = xom.match(/sid=([^;]+)/);
    sorov.foydalanuvchi = mos ? sessiyalar.get(mos[1]) ?? null : null;
    keyingi();
});

app.post("/kirish", (sorov, javob) => {
    const f = tekshir(sorov.body.login, sorov.body.parol);
    if (!f) {
        javob.status(401).json({ xato: "login yoki parol noto'g'ri" });
        return;
    }
    const sid = randomBytes(32).toString("hex");
    sessiyalar.set(sid, { login: f.login, rol: f.rol });
    javob.cookie("sid", sid, { httpOnly: true, sameSite: "lax" });
    javob.json({ kirildi: true });
});

function himoya(sorov, javob, keyingi) {
    if (!sorov.foydalanuvchi) {
        javob.status(401).json({ xato: "avval kiring" });
        return;
    }
    keyingi();
}

app.get("/profil", himoya, (sorov, javob) => {
    javob.json({ profil: sorov.foydalanuvchi });
});

const server = app.listen(0);
const port = server.address().port;
const asos = `http://localhost:${port}`;

const kirish = async (login, parol) => {
    const j = await fetch(asos + "/kirish", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ login, parol }),
    });
    return [j.status, j.headers.getSetCookie()[0] ?? null];
};

const [yomonHolat] = await kirish("malika", "notogri");
console.log("Noto'g'ri parol:", yomonHolat);

const [holat, cookie] = await kirish("malika", "KuchliParol1");
console.log("To'g'ri parol:", holat);

const himoyasiz = await fetch(asos + "/profil");
console.log("Cookie siz:", himoyasiz.status);

const sid = cookie.split(";")[0];
const bilan = await fetch(asos + "/profil", { headers: { Cookie: sid } });
console.log("Cookie bilan:", bilan.status, await bilan.json());

server.close();
Natija
Noto'g'ri parol: 401
To'g'ri parol: 200
Cookie siz: 401
Cookie bilan: 200 { profil: { login: 'malika', rol: 'oddiy' } }

Uch qismdan iborat tizim:

  1. Kirish - parol tekshiriladi, tasodifiy sessiya kaliti yaratiladi.
  2. Qatlam - har so'rovda cookie o'qilib, foydalanuvchi topiladi.
  3. Himoya - sorov.foydalanuvchi bo'lmasa, 401.
"Login yoki parol noto'g'ri" - ataylab noaniq

E'tibor bering, xato xabari qaysi biri noto'g'ri ekanini aytmaydi.

Agar "bunday login yo'q" desangiz, hujumchi qaysi loginlar mavjudligini bemalol aniqlab oladi. Keyin u faqat parolga hujum qiladi.

Xuddi shu sabab bilan ro'yxatdan o'tishda va parolni tiklashda ham "bunday email topilmadi" deb yozmang.

Va yana: noto'g'ri login uchun ham parolni xeshlab ko'ring - aks holda javob tezligi loginning mavjudligini oshkor qiladi.

Rollar bilan avtorizatsiya #

JavaScript
import express from "express";

const app = express();

const sessiyalar = new Map([
    ["oddiy-sid", { login: "malika", rol: "oddiy" }],
    ["admin-sid", { login: "husanboy", rol: "admin" }],
]);

app.use((sorov, javob, keyingi) => {
    const mos = (sorov.headers.cookie ?? "").match(/sid=([^;]+)/);
    sorov.foydalanuvchi = mos ? sessiyalar.get(mos[1]) ?? null : null;
    keyingi();
});

function rolTalab(...ruxsat) {
    return (sorov, javob, keyingi) => {
        if (!sorov.foydalanuvchi) {
            javob.status(401).json({ xato: "avval kiring" });
            return;
        }
        if (!ruxsat.includes(sorov.foydalanuvchi.rol)) {
            javob.status(403).json({ xato: "ruxsat yo'q" });
            return;
        }
        keyingi();
    };
}

app.get("/profil", rolTalab("oddiy", "admin"), (s, j) => {
    j.json({ sahifa: "profil" });
});

app.get("/admin", rolTalab("admin"), (s, j) => {
    j.json({ sahifa: "boshqaruv" });
});

const server = app.listen(0);
const port = server.address().port;
const asos = `http://localhost:${port}`;

const sora = async (yol, sid) => {
    const j = await fetch(asos + yol, {
        headers: sid ? { Cookie: `sid=${sid}` } : {},
    });
    return j.status;
};

console.log("mehmon /admin:", await sora("/admin", null));
console.log("oddiy /profil:", await sora("/profil", "oddiy-sid"));
console.log("oddiy /admin: ", await sora("/admin", "oddiy-sid"));
console.log("admin /admin: ", await sora("/admin", "admin-sid"));

server.close();
Natija
mehmon /admin: 401
oddiy /profil: 200
oddiy /admin:  403
admin /admin:  200
401 va 403 farqi

Bu ikkisi doim chalkashtiriladi:

  • 401 Unauthorized - "kim ekaningizni bilmayman". Kiring.
  • 403 Forbidden - "kim ekaningizni bilaman, lekin ruxsat yo'q".

Yuqoridagi natijada farq aniq ko'rinadi: mehmon 401 oldi, oddiy foydalanuvchi esa 403.

Nomlari chalkash: 401 aslida "autentifikatsiyadan o'tmagan" degani, "avtorizatsiyasiz" emas. Bu standartdagi eski xato.

Sessiya yoki token #

Qaysi birini tanlash

Ikki keng tarqalgan yondashuv bor:

Sessiya (yuqorida ko'rgan) - serverda saqlanadi, mijozda esa faqat kalit turadi.

  • Chiqishda sessiyani darhol o'chirish mumkin.
  • Server xotira yoki Redis talab qiladi.
  • Brauzerlar uchun eng oddiy va xavfsiz yo'l.

JWT token - hamma ma'lumot tokenning ichida, imzolangan.

  • Server hech narsa saqlamaydi.
  • Bekor qilish qiyin: token muddati tugaguncha amal qiladi.
  • Bir nechta mustaqil xizmat uchun qulay.

Amaliy maslahat: brauzer uchun oddiy sayt yozayotgan bo'lsangiz, sessiya ishlating - u soddaroq va bekor qilish oson. JWT ni faqat unga haqiqiy ehtiyoj bo'lganda oling.

Ikkalasida ham amal qiladigan qoida bitta: kalitni httpOnly cookie da saqlang, localStorage da emas - u yerdan uni har qanday skript o'qiy oladi.

Amaliy topshiriq
  1. scryptSync bilan parolni xeshlang.
  2. Bir xil parol ikki xil xesh berishini isbotlang.
  3. timingSafeEqual bilan taqqoslang.
  4. Turli uzunlikdagi buferlar bilan xatoni ko'ring.
  5. httpOnly va sameSite bayroqlarini qo'ying.
  6. Cookie ni o'qiydigan qatlam yozing.
  7. Kirish marshrutini yozib, sessiya yarating.
  8. Himoya qatlamini qo'shib, 401 ni oling.
  9. Rol tekshiruvini qo'shib, 403 ni oling.
  10. 401 va 403 farqini o'z so'zingiz bilan tushuntiring.

Xulosa #

  • Parolni hech qachon ochiq saqlamang; md5 va sha256 ham yaramaydi.
  • scrypt - Node ichidagi, ataylab sekin algoritm.
  • Tuz har bir parolga alohida; u maxfiy emas, lekin takrorlanmasligi shart.
  • Xesh va tokenlarni timingSafeEqual bilan solishtiring.
  • Sessiya cookie sida httpOnly, secure va sameSite bo'lsin.
  • Xato xabari qaysi biri noto'g'ri ekanini aytmasin.
  • 401 - kim ekaningiz noma'lum; 403 - ruxsat yo'q.
  • Brauzer uchun sessiya soddaroq; kalitni localStorage da saqlamang.

Keyingi bo'limda xavfsizlikni kengroq ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.