4-bo‘lim
Autentifikatsiya va parollar
Parol siyosati, ikki bosqichli autentifikatsiya, sessiyalar, tokenlar va parolsiz kirish.
Ushbu bo‘lim mundarijasi
Autentifikatsiya - "siz kimsiz?" degan savolga javob. U buzilsa, qolgan barcha himoya ma'nosini yo'qotadi.
Uch omil #
Barmoq izini almashtirib bo'lmaydi. Agar u oshkor bo'lsa, umrbod oshkor qolaveradi.
Shuning uchun biometriya:
- Qurilmada lokal saqlanishi kerak (serverga yuborilmasin)
- Yagona omil sifatida ishlatilmasin
- Zaxira usuli bo'lsin (PIN kod)
Parol siyosati #
| Eski tavsiya | Yangi tavsiya |
|---|---|
| 8 belgi, murakkab | 12+ belgi, uzunlik muhimroq |
| Har 90 kunda almashtirish | Faqat buzilish shubhasida |
| Maxsus belgi majburiy | Majburiy emas |
| Maxfiy savol | Ishlatmang |
| Parol maslahati | Ishlatmang |
| — | Tarqalgan parollar ro'yxatini tekshiring |
| — | Nusxa-joylashtirishga ruxsat bering |
Har 90 kunda parol almashtirish talab qilinsa, foydalanuvchilar:
Parol2026! → Parol2026!! → Parol2026!!!
Yanvar2026 → Fevral2026 → Mart2026
Bu parollarni zaiflashtiradi, kuchaytirmaydi.
To'g'ri yondashuv: uzun parol + 2FA + buzilish monitoringi.
final class ParolTekshiruvchi
{
private const MIN_UZUNLIK = 12;
private const MAKS_UZUNLIK = 128;
public function __construct(
private TarqalganParollarRoyxati $tarqalganlar,
) {}
/** @return string[] Xatolar ro'yxati */
public function tekshiring(string $parol, Foydalanuvchi $foydalanuvchi): array
{
$xatolar = [];
$uzunlik = mb_strlen($parol);
if ($uzunlik < self::MIN_UZUNLIK) {
$xatolar[] = sprintf('Parol kamida %d belgi bo\'lishi kerak', self::MIN_UZUNLIK);
}
if ($uzunlik > self::MAKS_UZUNLIK) {
$xatolar[] = 'Parol juda uzun';
}
if ($this->tarqalganlar->bormi($parol)) {
$xatolar[] = 'Bu parol juda keng tarqalgan, boshqasini tanlang';
}
if ($this->shaxsiyMalumotBormi($parol, $foydalanuvchi)) {
$xatolar[] = 'Parolda ism yoki email bo\'lmasligi kerak';
}
if ($this->ketmaKetlikmi($parol)) {
$xatolar[] = 'Ketma-ket belgilar ishlatmang (12345, qwerty)';
}
return $xatolar;
}
private function shaxsiyMalumotBormi(string $parol, Foydalanuvchi $f): bool
{
$past = mb_strtolower($parol);
foreach ([$f->ism, $f->familiya, explode('@', $f->email)[0]] as $qism) {
if (mb_strlen($qism) >= 3 && str_contains($past, mb_strtolower($qism))) {
return true;
}
}
return false;
}
private function ketmaKetlikmi(string $parol): bool
{
$naqshlar = ['12345', 'qwerty', 'abcdef', '11111'];
foreach ($naqshlar as $naqsh) {
if (str_contains(mb_strtolower($parol), $naqsh)) {
return true;
}
}
return false;
}
}
Buzilgan parollarni tekshirish #
// Have I Been Pwned - k-anonimlik bilan
final class TarqalganParollarRoyxati
{
public function bormi(string $parol): bool
{
$xesh = strtoupper(sha1($parol));
$prefiks = substr($xesh, 0, 5);
$qolgan = substr($xesh, 5);
// Faqat 5 belgi yuboriladi - to'liq parol emas
$javob = $this->http->get("https://api.pwnedpasswords.com/range/{$prefiks}");
foreach (explode("\n", $javob) as $qator) {
[$suffiks, $soni] = explode(':', trim($qator));
if (hash_equals($qolgan, $suffiks)) {
return true;
}
}
return false;
}
}
Xizmatga to'liq parol yuborilmaydi - faqat xeshning birinchi 5 belgisi.
Server shu prefiks bilan boshlanadigan barcha xeshlarni qaytaradi (taxminan 500 ta), tekshiruv esa mahalliy bajariladi.
Shunday qilib xizmat sizning parolingizni bilmaydi.
Parolni saqlash #
// Ro'yxatdan o'tish
$xesh = password_hash($parol, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 2,
]);
// Kirish
if (password_verify($parol, $xesh)) {
if (password_needs_rehash($xesh, PASSWORD_ARGON2ID, $sozlamalar)) {
$yangiXesh = password_hash($parol, PASSWORD_ARGON2ID, $sozlamalar);
$this->repozitoriy->xeshYangilang($id, $yangiXesh);
}
}
password_needs_rehash - avtomatik yangilashVaqt o'tishi bilan hisoblash quvvati oshadi va eski sozlamalar zaiflashadi.
Bu funksiya foydalanuvchi keyingi kirishida xeshni jimgina yangilaydi - hech kim parolini almashtirishi shart emas.
Kirish urinishlarini cheklash #
final class KirishNazorati
{
private const IP_CHEGARA = 20;
private const HISOB_CHEGARA = 5;
private const OYNA = 900; // 15 daqiqa
public function __construct(private KeshInterfeysi $kesh) {}
public function ruxsatBormi(string $ip, string $login): bool
{
$ipUrinish = (int) $this->kesh->oling("kirish:ip:{$ip}");
$hisobUrinish = (int) $this->kesh->oling('kirish:hisob:' . sha1($login));
return $ipUrinish < self::IP_CHEGARA
&& $hisobUrinish < self::HISOB_CHEGARA;
}
public function muvaffaqiyatsizYozing(string $ip, string $login): void
{
$this->kesh->oshiring("kirish:ip:{$ip}", self::OYNA);
$this->kesh->oshiring('kirish:hisob:' . sha1($login), self::OYNA);
}
public function tozalang(string $ip, string $login): void
{
$this->kesh->ochiring("kirish:ip:{$ip}");
$this->kesh->ochiring('kirish:hisob:' . sha1($login));
}
public function kutishVaqti(string $login): int
{
return $this->kesh->qolganVaqt('kirish:hisob:' . sha1($login)) ?? 0;
}
}
| Faqat IP | Faqat hisob |
|---|---|
| Bir ofisdan hamma bloklanadi | Hujumchi ko'p hisobni sinaydi |
| Botnet turli IP dan hujum qiladi | Bitta hisobga hujum to'sildi |
Ikkalasini birga ishlating.
Shuningdek "parol purkash" hujumini hisobga oling: hujumchi bitta mashhur parolni minglab hisobga sinaydi. Bunda har bir hisobga faqat 1-2 urinish to'g'ri keladi va hisob chegarasi ishlamaydi.
Yechim: umumiy muvaffaqiyatsiz urinishlar sonini ham kuzating.
// Progressiv kechikish
$kechikish = min(2 ** $urinishlar, 300); // 2, 4, 8, 16 ... 300 soniya
Ikki bosqichli autentifikatsiya #
// TOTP - vaqtga asoslangan bir martalik parol
final class TotpXizmati
{
private const OYNA = 1; // oldingi va keyingi 30 soniya
public function sirYarating(): string
{
return Base32::kodlang(random_bytes(20));
}
public function qrHavolasi(string $sir, string $email, string $ilova): string
{
return sprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30',
rawurlencode($ilova),
rawurlencode($email),
$sir,
rawurlencode($ilova),
);
}
public function tekshiring(string $sir, string $kod): bool
{
$joriyVaqt = (int) floor(time() / 30);
for ($i = -self::OYNA; $i <= self::OYNA; $i++) {
if (hash_equals($this->kodYarating($sir, $joriyVaqt + $i), $kod)) {
return true;
}
}
return false;
}
private function kodYarating(string $sir, int $vaqt): string
{
$kalit = Base32::ochiring($sir);
$malumot = pack('N*', 0, $vaqt);
$xesh = hash_hmac('sha1', $malumot, $kalit, true);
$ofset = ord($xesh[19]) & 0xf;
$son = (
((ord($xesh[$ofset]) & 0x7f) << 24) |
((ord($xesh[$ofset + 1]) & 0xff) << 16) |
((ord($xesh[$ofset + 2]) & 0xff) << 8) |
(ord($xesh[$ofset + 3]) & 0xff)
) % 1000000;
return str_pad((string) $son, 6, '0', STR_PAD_LEFT);
}
}
// Ishlatilgan kodni belgilang
$kalit = "totp:ishlatilgan:{$foydalanuvchiId}:{$kod}";
if ($this->kesh->bormi($kalit)) {
throw new KodAllaqachonIshlatilgan();
}
$this->kesh->saqlang($kalit, true, 90);
Aks holda hujumchi kodni ushlab qolib, 30 soniya ichida qayta ishlatishi mumkin.
Zaxira kodlar #
final class ZaxiraKodlar
{
private const SONI = 10;
/** @return string[] */
public function yarating(int $foydalanuvchiId): array
{
$kodlar = [];
$xeshlar = [];
for ($i = 0; $i < self::SONI; $i++) {
$kod = sprintf(
'%s-%s',
bin2hex(random_bytes(3)),
bin2hex(random_bytes(3)),
);
$kodlar[] = $kod;
$xeshlar[] = password_hash($kod, PASSWORD_DEFAULT);
}
$this->repozitoriy->saqlang($foydalanuvchiId, $xeshlar);
// Kodlar faqat bir marta ko'rsatiladi
return $kodlar;
}
public function ishlating(int $foydalanuvchiId, string $kod): bool
{
foreach ($this->repozitoriy->oling($foydalanuvchiId) as $id => $xesh) {
if (password_verify($kod, $xesh)) {
$this->repozitoriy->ochiring($id); // bir martalik
return true;
}
}
return false;
}
}
Foydalanuvchi telefonini yo'qotsa, hisobiga kira olmaydi.
Qo'llab-quvvatlash orqali tiklash esa ijtimoiy muhandislik uchun eshik ochadi.
Zaxira kodlar - eng xavfsiz yechim. Ular:
- Faqat bir marta ko'rsatiladi
- Xeshlangan holda saqlanadi
- Har biri bir martalik
- Tugab qolsa, yangisini yaratish mumkin
WebAuthn #
// Ro'yxatdan o'tkazish
const kalit = await navigator.credentials.create({
publicKey: {
challenge: yangiChaqiruv,
rp: { name: "Dokon", id: "dokon.uz" },
user: {
id: foydalanuvchiId,
name: "[email protected]",
displayName: "Husanboy Qodirov",
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" }, // RS256
],
authenticatorSelection: {
userVerification: "required",
residentKey: "preferred",
},
timeout: 60000,
attestation: "none",
},
});
Kalit domenga bog'langan.
Foydalanuvchi dokon-uz.support soxta saytiga kirsa, kalit
umuman ishlamaydi - u faqat dokon.uz uchun ishlaydi.
Bu TOTP dan katta afzallik: TOTP kodini foydalanuvchi soxta saytga kiritishi mumkin.
Sessiyalar #
// Xavfsiz sessiya sozlamalari
session_set_cookie_params([
'lifetime' => 0, // brauzer yopilganda tugaydi
'path' => '/',
'domain' => '',
'secure' => true, // faqat HTTPS
'httponly' => true, // JavaScript o'qiy olmaydi
'samesite' => 'Lax', // CSRF himoyasi
]);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.gc_maxlifetime', '1800');
session_start();
// Kirishdan keyin sessiya identifikatorini yangilash
session_regenerate_id(true);
Hujumchi foydalanuvchiga ma'lum sessiya identifikatori bilan havola yuboradi. Foydalanuvchi kirgach, hujumchi shu identifikator bilan uning hisobiga kiradi.
Himoya: kirishdan keyin session_regenerate_id(true).
use_strict_mode = 1 esa serverga noma'lum identifikatorni qabul
qilmaslikni buyuradi.
final class SessiyaBoshqaruvchi
{
private const FAOLSIZLIK_MUDDATI = 1800; // 30 daqiqa
private const MUTLAQ_MUDDAT = 43200; // 12 soat
public function tekshiring(): void
{
if (!isset($_SESSION['foydalanuvchi_id'])) {
throw new SessiyaYoq();
}
if (time() - $_SESSION['oxirgi_faollik'] > self::FAOLSIZLIK_MUDDATI) {
$this->tugating();
throw new SessiyaMuddatiTugadi();
}
if (time() - $_SESSION['boshlangan'] > self::MUTLAQ_MUDDAT) {
$this->tugating();
throw new SessiyaMuddatiTugadi();
}
// Sessiyani o'g'irlashni qiyinlashtirish
if ($_SESSION['agent_xeshi'] !== $this->agentXeshi()) {
$this->tugating();
throw new ShubhaliSessiya();
}
$_SESSION['oxirgi_faollik'] = time();
}
private function agentXeshi(): string
{
return hash('sha256', ($_SERVER['HTTP_USER_AGENT'] ?? '') . $this->sir);
}
public function tugating(): void
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$p['path'], $p['domain'], $p['secure'], $p['httponly']);
}
session_destroy();
}
}
Mobil foydalanuvchilarda IP tez-tez o'zgaradi (uyali tarmoq, Wi-Fi ga o'tish).
Natijada foydalanuvchi doimiy tizimdan chiqib ketadi.
User-Agent esa barqarorroq - lekin u ham 100% ishonchli emas.
JWT tokenlari #
// Kirish tokeni - qisqa muddatli
$kirishToken = JWT::yarating([
'sub' => (string) $foydalanuvchi->id,
'rol' => $foydalanuvchi->rol->value,
'iat' => time(),
'exp' => time() + 900, // 15 daqiqa
'jti' => bin2hex(random_bytes(16)),
], $sir);
// Yangilash tokeni - uzoq muddatli, bazada saqlanadi
$yangilashToken = bin2hex(random_bytes(32));
$this->repozitoriy->saqlang(
$foydalanuvchi->id,
hash('sha256', $yangilashToken),
muddat: '+30 days',
);
// 1. Algoritmni tekshirmaslik
$malumot = JWT::decode($token, $sir); // "alg": "none" hujumi
// TO'G'RI
$malumot = JWT::decode($token, $sir, ['HS256']);
// 2. Muddatni tekshirmaslik
if (!isset($malumot['exp']) || $malumot['exp'] < time()) {
throw new TokenMuddatiTugadi();
}
// 3. Maxfiy ma'lumotni JWT ga qo'yish
// JWT SHIFRLANMAGAN - u faqat imzolangan!
// base64 dekodlash bilan hamma o'qiy oladi
$token = JWT::yarating(['parol' => $parol]); // XATO
Parolni tiklash #
final class ParolTiklashXizmati
{
private const MUDDAT = 3600; // 1 soat
public function soraing(string $email): void
{
$foydalanuvchi = $this->repozitoriy->emailBoyicha($email);
// Foydalanuvchi topilmasa ham bir xil javob
if ($foydalanuvchi !== null) {
$token = bin2hex(random_bytes(32));
$this->tokenlar->saqlang(
$foydalanuvchi->id,
hash('sha256', $token),
time() + self::MUDDAT,
);
$this->pochta->yuboring($email, new ParolTiklashXati($token));
}
// Har doim bir xil xabar
$this->javob = 'Agar bu email ro\'yxatdan o\'tgan bo\'lsa, xat yuborildi';
}
public function tiklang(string $token, string $yangiParol): void
{
$yozuv = $this->tokenlar->xeshBoyicha(hash('sha256', $token))
?? throw new NotogriToken();
if ($yozuv->muddat < time()) {
throw new TokenMuddatiTugadi();
}
$this->repozitoriy->parolYangilang(
$yozuv->foydalanuvchiId,
password_hash($yangiParol, PASSWORD_ARGON2ID),
);
$this->tokenlar->ochiring($yozuv->id);
// Barcha sessiyalarni tugatish
$this->sessiyalar->hammasiniTugating($yozuv->foydalanuvchiId);
$this->pochta->yuboring(
$yozuv->email,
new ParolOzgartirildi($this->sorov->ip(), new DateTimeImmutable()),
);
}
}
Foydalanuvchi bu o'zgarishni qilmagan bo'lsa, u darhol bilishi kerak.
Xatda: qachon, qaysi IP dan, qaysi qurilmadan. Va "bu siz emasmisiz?" degan havola.
Parol o'zgarganda mavjud sessiyalar bekor qilinishi shart.
Aks holda hisobni o'g'irlagan hujumchi parol almashtirilgandan keyin ham kirishda davom etadi.
Parolsiz kirish #
| Usul | Qanday ishlaydi |
|---|---|
| Sehrli havola | Emailga bir martalik havola |
| WebAuthn | Qurilma kaliti bilan |
| OAuth | Google, GitHub orqali |
| SMS kod | Telefon raqami bilan |
final class SehrliHavolaXizmati
{
private const MUDDAT = 900; // 15 daqiqa
public function yuboring(string $email): void
{
$token = bin2hex(random_bytes(32));
$this->tokenlar->saqlang(
$email,
hash('sha256', $token),
time() + self::MUDDAT,
);
$havola = sprintf('%s/kirish/%s', $this->asosiyUrl, $token);
$this->pochta->yuboring($email, new SehrliHavolaXati($havola));
}
}
Agar foydalanuvchining emaili buzilgan bo'lsa, hujumchi barcha sehrli havolalar orqali kira oladi.
Shuning uchun:
- Havola qisqa muddatli bo'lsin (15 daqiqa)
- Bir martalik bo'lsin
- Muhim amallar uchun qo'shimcha tasdiq talab qilinsin
Ijtimoiy kirish (OAuth) #
// Google orqali kirish
$holat = bin2hex(random_bytes(16));
$_SESSION['oauth_holat'] = $holat;
$url = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query([
'client_id' => $this->mijozId,
'redirect_uri' => $this->qaytishUrl,
'response_type' => 'code',
'scope' => 'openid email profile',
'state' => $holat,
'nonce' => bin2hex(random_bytes(16)),
]);
// Qaytish
public function qaytish(Sorov $sorov): Javob
{
// CSRF himoyasi
if (!hash_equals($_SESSION['oauth_holat'] ?? '', $sorov->get('state'))) {
throw new NotogriHolat();
}
unset($_SESSION['oauth_holat']);
$token = $this->kodniTokengaAlmashtiring($sorov->get('code'));
$malumot = $this->foydalanuvchiMalumotiniOling($token);
// Email tasdiqlanganini tekshirish
if (!($malumot['email_verified'] ?? false)) {
throw new EmailTasdiqlanmagan();
}
// ...
}
state parametri majburiyUsiz hujumchi o'z hisobini qurbonning hisobiga bog'lashi mumkin (CSRF hujumi).
state - tasodifiy qiymat, sessiyada saqlanadi va qaytishda
tekshiriladi.
Xavfsizlik hodisalarini kuzatish #
// Shubhali faollik belgilari
$this->xavfsizlik->tekshiring($foydalanuvchi, [
'yangi_qurilma' => !$this->qurilmalar->tanishmi($barmoqIzi),
'yangi_joylashuv' => $this->geoIp->yangiMamlakatmi($ip, $foydalanuvchi),
'gayritabiiy_vaqt' => $this->vaqtTahlili->gayritabiimi($foydalanuvchi),
'tez_joylashuv_ozgarishi' => $this->geoIp->imkonsizSayohatmi($foydalanuvchi, $ip),
]);
Foydalanuvchi 10 daqiqa oldin Toshkentdan kirgan, hozir Berlindan kiryapti.
Jismonan imkonsiz - demak hisob buzilgan yoki VPN ishlatilyapti.
Bunday holatda qo'shimcha tasdiq so'ralishi kerak.
ParolTekshiruvchiklassini yozing va sinang.- Have I Been Pwned API bilan tarqalgan parol tekshiruvini qo'shing.
password_hashvapassword_verifybilan ro'yxat va kirish yozing.- Kirish urinishlarini cheklovchi tizim yozing (IP + hisob).
- Progressiv kechikishni qo'shing.
- TOTP xizmatini amalga oshiring va telefon ilovasi bilan sinang.
- Zaxira kodlar tizimini yozing.
- Xavfsiz sessiya sozlamalarini qo'llang.
- Parolni tiklash oqimini yozing - barcha xavfsizlik talablarini hisobga oling.
- Sessiyani o'g'irlashga urinib ko'ring va himoya ishlashini tekshiring.
Xulosa #
- Uch omil: bilgan, ega bo'lgan, o'zingiz bo'lgan narsa.
- Ikkita parol - 2FA emas, ikkalasi ham bir xil omil.
- Zamonaviy parol siyosati: uzunlik muhimroq, majburiy almashtirish zararli.
- Tarqalgan parollar ro'yxatini tekshiring (k-anonimlik bilan).
- Argon2 yoki bcrypt ishlating,
password_needs_rehashbilan avtomatik yangilang. - Kirish cheklovi IP va hisob bo'yicha birga bo'lsin.
- WebAuthn fishingga chidamli - kalit domenga bog'langan.
- Zaxira kodlarsiz 2FA foydalanuvchini hisobidan ayirishi mumkin.
- Kirishdan keyin
session_regenerate_id(true). - JWT shifrlanmagan - maxfiy ma'lumot qo'ymang; algoritmni albatta tekshiring.
- Parol o'zgarganda barcha sessiyalarni tugating va xabar bering.
- OAuth da
stateparametri majburiy.
Keyingi bo'limda avtorizatsiya va kirish nazoratini 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.