17-bo‘lim
Autentifikatsiya va sessiyalar
Parolni scrypt bilan xeshlash, tuz va vaqtga bardoshli taqqoslash, cookie bayroqlari, sessiya saqlash va himoya qatlami.
Ushbu bo‘lim mundarijasi
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 #
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 #
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);
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 (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 #
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);
}
Teng: true
Teng emas: false
Uzunlik farqi: ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH
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.
Cookie o'qish va yozish #
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();
HttpOnly bor: true
SameSite bor: true
{ sessiya: 'abc123' }
| Bayroq | Nima qiladi |
|---|---|
httpOnly | JavaScript o'qiy olmaydi |
secure | Faqat HTTPS orqali yuboriladi |
sameSite | Boshqa saytdan yuborilishini cheklaydi |
maxAge | Qancha yashaydi |
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:
secure: process.env.NODE_ENV === "production"
Sessiya saqlash #
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();
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:
- Kirish - parol tekshiriladi, tasodifiy sessiya kaliti yaratiladi.
- Qatlam - har so'rovda cookie o'qilib, foydalanuvchi topiladi.
- Himoya -
sorov.foydalanuvchibo'lmasa, 401.
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 #
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();
mehmon /admin: 401
oddiy /profil: 200
oddiy /admin: 403
admin /admin: 200
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 #
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
Redistalab 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.
scryptSyncbilan parolni xeshlang.- Bir xil parol ikki xil xesh berishini isbotlang.
timingSafeEqualbilan taqqoslang.- Turli uzunlikdagi buferlar bilan xatoni ko'ring.
httpOnlyvasameSitebayroqlarini qo'ying.- Cookie ni o'qiydigan qatlam yozing.
- Kirish marshrutini yozib, sessiya yarating.
- Himoya qatlamini qo'shib, 401 ni oling.
- Rol tekshiruvini qo'shib, 403 ni oling.
- 401 va 403 farqini o'z so'zingiz bilan tushuntiring.
Xulosa #
- Parolni hech qachon ochiq saqlamang;
md5vasha256ham yaramaydi. scrypt- Node ichidagi, ataylab sekin algoritm.- Tuz har bir parolga alohida; u maxfiy emas, lekin takrorlanmasligi shart.
- Xesh va tokenlarni
timingSafeEqualbilan solishtiring. - Sessiya cookie sida
httpOnly,securevasameSitebo'lsin. - Xato xabari qaysi biri noto'g'ri ekanini aytmasin.
- 401 - kim ekaningiz noma'lum; 403 - ruxsat yo'q.
- Brauzer uchun sessiya soddaroq; kalitni
localStorageda saqlamang.
Keyingi bo'limda xavfsizlikni kengroq ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
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.