Ders 13 / 21
Kimlik Doğrulama ve Yetkilendirme
Parolanın özetle saklanması ve doğrulanması, oturumdaki kullanıcı, yetki kapısı ve yetki politikası; meşru bir kullanıcının başkasının yazısını düzenleme isteğinde kapının 403 vermesi ve eylemin atlanması; tanımsız yeteneğin reddedilmesi ilkesi.
Yayın Güncelleme
Bu derste
- Kimlik doğrulama ile yetkilendirmeyi ayırır ve ikisinin hangi adımda koştuğunu gösterir
- Yetki kapısının politika sonucuna göre eylemi atladığını ve 403 verdiğini ölçer
- Tanımsız bir yeteneğe kapının ne döndürdüğünü ölçer ve reddetmenin neden varsayılan olması gerektiğini açıklar
İçindekiler
Sayfalama ve Kaynak Dönüşümü dersinde yanıta yeni bir alanın bağlanmadığını gördük: model değişmişti, yanıt 200 dönmüştü, ama değişiklik yanıtın içine ulaşmamıştı. Bu ders uygulama servislerinin ilkidir ve soru şudur: isteği yapan kişi bu işi yapabilir mi ve yapamıyorsa eylem koştu mu?
İki ayrı soru var ve çerçeveler ikisini ayrı adımlara koyar. Kimlik doğrulama (authentication) isteği yapanın kim olduğunu belirler: kullanıcı adı ve parola doğrudur, oturum bir kullanıcıya bağlıdır. Yetkilendirme (authorization) kimliği bilinen kullanıcının bu işi yapıp yapamayacağına karar verir. İlki 302 ile /giris sayfasına gönderir, ikincisi 403 ile reddeder. Birinin kimliğini doğrulamış olmak, ona her kaydı düzenleme hakkı vermez.
US1. Model ürün adı kullanmaz ve parolaları dışarıdan okumaz: parola password_hash() ile özetlenir, password_verify() ile doğrulanır; özetin kendisi hiçbir çıktıda basılmaz, çünkü her çağrıda farklı bir değer üretir. Oturum, çekirdekteki Istek sınıfının oturum alanının küçültülmüş bir biçimidir: bu derste alan oturumdaki kullanıcının kimliğini taşır ve session_* fonksiyonları çağrılmaz. Parola kırma, oturum çalma, belirteç taklidi ve yetki aşımı modellenmez; koşan kod savunma tarafındadır.
Ölçüm Çekirdeği
Kurs boyunca her derste aynı ölçüm kullanılır. kostu.php beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. Gunluk üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. sinifla() bunlara sırayla bakar. Karar sırası sinifla()’nın kendisidir: koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.
<?php // kostu.php — beklenen bir parça bu istekte koştu mu? declare(strict_types=1); final class Gunluk { public static array $yol = []; // isteğin yoluna konan parçalar public static array $kostu = []; // istek içinde çağrılanlar public static array $ertelenen = []; // isteğin dışına bırakılanlar public static function yeniIstek(): void { self::$yol = self::$kostu = self::$ertelenen = []; } public static function yola(string $parca): void { self::$yol[] = $parca; } public static function kostu(string $parca): void { self::$kostu[] = $parca; } public static function ertele(string $parca): void { self::$ertelenen[] = $parca; } } function sinifla(string $parca): string { return match (true) { in_array($parca, Gunluk::$kostu, true) => 'koştu', in_array($parca, Gunluk::$ertelenen, true) => 'ertelendi', in_array($parca, Gunluk::$yol, true) => 'atlandı', default => 'bağlanmadı', }; }
Bu ders çekirdeğe üç çağrıyla bağlanır: yola koyduğunda Gunluk::yola(), çağırdığında Gunluk::kostu(); ertelenen bir parça bu derste yoktur. Çerçevenin tamamı yeniden kurulmaz, yalnız bu dersin dilimi yazılır.
Parola Özeti
Kimlik doğrulamanın ilk adımı parolanın saklanış biçimidir. Parola olduğu gibi saklanmaz; password_hash() ona bir tuz ekleyip tek yönlü bir özet üretir ve password_verify() verilen parolayı özetle karşılaştırır. Tuz her çağrıda yenidir, bu yüzden aynı parolanın iki özeti farklıdır ama ikisi de doğrulanır. Aşağıdaki ölçüm özetin kendisini basmaz, yalnız karşılaştırma sonuçlarını basar.
<?php // parola.php — aynı parolanın iki özeti $ilk = password_hash('sifre-ayse', PASSWORD_DEFAULT); $ikinci = password_hash('sifre-ayse', PASSWORD_DEFAULT); echo 'iki özet eşit mi: ', $ilk === $ikinci ? 'evet' : 'hayır', "\n"; echo 'ilk özet doğrulanır mı: ', password_verify('sifre-ayse', $ilk) ? 'evet' : 'hayır', "\n"; echo 'ikinci özet doğrulanır mı: ', password_verify('sifre-ayse', $ikinci) ? 'evet' : 'hayır', "\n"; echo 'yanlış parola doğrulanır mı: ', password_verify('sifre-burak', $ilk) ? 'evet' : 'hayır', "\n";
iki özet eşit mi: hayır ilk özet doğrulanır mı: evet ikinci özet doğrulanır mı: evet yanlış parola doğrulanır mı: hayır
Özetlerin eşit olmaması, özet karşılaştırmasının === ile değil password_verify() ile yapılması gerektiğini söyler. KimlikDeposu::giris() bu yüzden kullanıcı adını eşitlikle, parolayı doğrulama fonksiyonuyla sınar. Depoda yalnız parolaOzeti alanı bulunur; düz parola hiçbir nesnede tutulmaz.
Kapı ve Politika
yetki.php dilimin tamamıdır ve dört parçadan oluşur. KimlikDeposu parolayı özet olarak saklar ve giris() ile doğrular. Yetki kapısı (gate), yani Kapi, bir yeteneğin adını (yazi.duzenle) bir yetki politikasına (policy) bağlar; politika kullanıcıyı ve hedef nesneyi alıp doğru ya da yanlış döndüren küçük bir fonksiyondur. YaziPolitikasi::duzenle() kuralı tek satırla söyler: yazıyı yalnız sahibi düzenler.
YaziUygulamasi::duzenle() üç beklenen parçayı yola koyar: kimlik (oturumdaki kullanıcıyı bulma), kapi (izin sorusu) ve duzenle (başlığı değiştiren eylem). Her adım çağrıldığında Gunluk::kostu() ile işaretlenir. Kimlik yoksa istek 302 ile döner, kapı reddederse 403 ile döner ve eylem hiç çağrılmaz.
<?php // yetki.php — kimlik doğrulama, yetki kapısı ve yetki politikası declare(strict_types=1); require_once 'kostu.php'; final class Kullanici { public function __construct(public int $id, public string $ad, public string $parolaOzeti) {} } final class Yazi { public function __construct(public int $id, public int $sahipId, public string $baslik) {} } final class Istek { public function __construct(public ?int $oturum = null, public array $govde = []) {} } final class KimlikDeposu { private array $kullanicilar = []; public function kaydet(int $id, string $ad, string $parola): void { $this->kullanicilar[$id] = new Kullanici($id, $ad, password_hash($parola, PASSWORD_DEFAULT)); } public function giris(string $ad, string $parola): ?Kullanici { foreach ($this->kullanicilar as $kullanici) { if ($kullanici->ad === $ad && password_verify($parola, $kullanici->parolaOzeti)) return $kullanici; } return null; } public function bul(int $id): ?Kullanici { return $this->kullanicilar[$id] ?? null; } } final class Kapi { private array $politikalar = []; public function __construct(private bool $tanimsizGecer = false) {} public function tanimla(string $yetenek, Closure $politika): void { $this->politikalar[$yetenek] = $politika; } public function izinVerir(string $yetenek, Kullanici $kullanici, object $hedef): bool { if (!isset($this->politikalar[$yetenek])) return $this->tanimsizGecer; return ($this->politikalar[$yetenek])($kullanici, $hedef); } } final class YaziPolitikasi { public static function duzenle(Kullanici $kullanici, Yazi $yazi): bool { return $kullanici->id === $yazi->sahipId; } } final class YaziUygulamasi { public function __construct(private KimlikDeposu $kimlik, private Kapi $kapi, public array $yazilar) {} public function duzenle(Istek $istek, int $yaziId, string $yetenek = 'yazi.duzenle'): array { Gunluk::yeniIstek(); foreach (['kimlik', 'kapi', 'duzenle'] as $parca) Gunluk::yola($parca); Gunluk::kostu('kimlik'); $kullanici = $istek->oturum === null ? null : $this->kimlik->bul($istek->oturum); if ($kullanici === null) return [302, '/giris']; Gunluk::kostu('kapi'); $yazi = $this->yazilar[$yaziId]; if (!$this->kapi->izinVerir($yetenek, $kullanici, $yazi)) return [403, 'yetki yok']; Gunluk::kostu('duzenle'); $yazi->baslik = $istek->govde['baslik']; return [200, 'guncellendi']; } }
<?php // kapi.php — iki kullanıcı, bir yazı, üç istek require_once 'yetki.php'; $kimlik = new KimlikDeposu(); $kimlik->kaydet(1, 'ayse', 'sifre-ayse'); $kimlik->kaydet(2, 'burak', 'sifre-burak'); echo 'giriş doğru parola: ', $kimlik->giris('burak', 'sifre-burak')?->ad ?? 'yok', "\n"; echo 'giriş yanlış parola: ', $kimlik->giris('burak', 'sifre-ayse')?->ad ?? 'yok', "\n"; $kapi = new Kapi(); $kapi->tanimla('yazi.duzenle', YaziPolitikasi::duzenle(...)); $uygulama = new YaziUygulamasi($kimlik, $kapi, [10 => new Yazi(10, 1, 'Merhaba')]); function olc(string $ad, array $yanit): void { printf("%s -> %d %s\n", $ad, $yanit[0], $yanit[1]); foreach (['kimlik', 'kapi', 'duzenle'] as $parca) printf(" %-8s %s\n", $parca, sinifla($parca)); } olc('oturumsuz', $uygulama->duzenle(new Istek(null, ['baslik' => 'Yeni']), 10)); olc('burak', $uygulama->duzenle(new Istek(2, ['baslik' => 'Yeni']), 10)); olc('ayse', $uygulama->duzenle(new Istek(1, ['baslik' => 'Yeni']), 10)); echo 'başlık: ', $uygulama->yazilar[10]->baslik, "\n";
giriş doğru parola: burak giriş yanlış parola: yok oturumsuz -> 302 /giris kimlik koştu kapi atlandı duzenle atlandı burak -> 403 yetki yok kimlik koştu kapi koştu duzenle atlandı ayse -> 200 guncellendi kimlik koştu kapi koştu duzenle koştu başlık: Yeni
İlk satırlar password_verify() sonucudur: doğru parola kullanıcıyı döndürür, başka birinin parolası döndürmez. Asıl gözlem istek satırlarındadır. Oturumsuz istekte kimlik koştu, kapı ve eylem yoldaydı ama çağrılmadı: ikisi de atlandı, durum kodu 302. İkinci istek bu dersin gözlemidir: Burak oturum açmış, kimliği doğrulanmış, meşru bir kullanıcıdır; Ayşe’nin yazısını düzenlemek ister. kimlik ve kapi koştu, kapı politikayı çalıştırdı, politika yanlış döndürdü, durum 403 oldu ve duzenle atlandı. Son satırdaki başlığın Merhaba değil Yeni olması, değişikliğin yalnız üçüncü istekten geldiğini söyler.
Atlanan parça kendini durum koduyla ele verir. Burak’ın isteğinde 403 görmek, eylemin neden koşmadığını açıklar; hata fırlatılmadı, çünkü ret beklenen bir sonuçtur.
Karşıt: Yazının Sahibi
Üçüncü istek aynı kod yolundan geçer ve tek fark kullanıcının yazının sahibi olmasıdır. Politika doğru döndürür, kapı geçirir, duzenle koştu ve durum 200 olur. Kapıda hiçbir şey değişmedi; değişen, politikaya verilen kullanıcı ile yazının sahipId alanının eşleşmesidir. Kapı bir engel değil bir karardır ve karar her istekte yeniden verilir.
Burada ayrımı koymak gerekir. kimlik her üç istekte de koştu: kimlik doğrulama başarıldı ya da başarısız olduysa bile adım çağrıldı. Oturumsuz istekte kapının atlanmasıyla Burak’ın isteğinde eylemin atlanması farklı nedenlerdendir, ama sınıf aynıdır: yol vardı, önündeki adım yanıtı erken verdi. Sınıfı sebep değil sinifla() belirler.
Sınırlayıcı Ölçüm
Kapıya hiç tanımlanmamış bir yetenek sorulursa ne olur? Gerçek uygulamalarda bu bir yazım hatasıyla olur: politika yazi.duzenle adına bağlanmış, eylem yazi.duzenel diye sorar. Model iki kapı kurar: biri tanımsız yeteneği reddeder (Kapi varsayılanı), öteki geçirir (tanimsizGecer: true). Aynı yazım hatalı yetenek adı hem yazının sahibi hem sahibi olmayan kullanıcı için sorulur.
<?php // tanimsiz.php — kapıya tanımlanmamış bir yetenek sorulur require_once 'yetki.php'; function kur(bool $tanimsizGecer): YaziUygulamasi { $kimlik = new KimlikDeposu(); $kimlik->kaydet(1, 'ayse', 'sifre-ayse'); $kimlik->kaydet(2, 'burak', 'sifre-burak'); $kapi = new Kapi($tanimsizGecer); $kapi->tanimla('yazi.duzenle', YaziPolitikasi::duzenle(...)); return new YaziUygulamasi($kimlik, $kapi, [10 => new Yazi(10, 1, 'Merhaba')]); } foreach ([false, true] as $gecer) { foreach ([[1, 'ayse'], [2, 'burak']] as [$oturum, $ad]) { $uygulama = kur($gecer); $yanit = $uygulama->duzenle(new Istek($oturum, ['baslik' => 'Yeni']), 10, 'yazi.duzenel'); printf("%-10s %-6s -> %d kapi %-8s duzenle %s\n", $gecer ? 'geciriyor' : 'reddediyor', $ad, $yanit[0], sinifla('kapi'), sinifla('duzenle')); } }
reddediyor ayse -> 403 kapi koştu duzenle atlandı reddediyor burak -> 403 kapi koştu duzenle atlandı geciriyor ayse -> 200 kapi koştu duzenle koştu geciriyor burak -> 200 kapi koştu duzenle koştu
Sonuç iki yönlüdür. Reddeden kapıda yazı sahibi bile 403 alır: hata yüksek sesle görünür ve geliştirici ilk denemede ad uyuşmazlığını fark eder. Geçiren kapıda ise Burak’ın isteği 200 ile döner ve eylem koşar; kapı koştu, ama hiçbir politika çalışmadı, yani kapı varlığı ile korumayı aynı şey sanmak yanıltır. Tanımsız yetenek reddedilmelidir ilkesi buradan çıkar: bilinmeyen bir soruya “evet” demek, korumanın sessizce kalkması demektir ve yanıtta hiçbir iz bırakmaz.
Özet
- Bu dersin ölçü sınıfı atlandı: Burak’ın isteğinde
kimlikvekapikoştu,duzenleyoldaydı ama kapı 403 verdi ve eylem çağrılmadı. - Kimlik doğrulama kim olduğunu, yetkilendirme ne yapabileceğini belirler; ilki 302 ile, ikincisi 403 ile kendini ele verir.
- Karşıtı: yazının sahibinde politika doğru döndürdü,
duzenlekoştu ve durum 200 oldu. - Sınırlayıcı ölçümde tanımsız yetenek reddeden kapıda iki kullanıcı da 403 aldı; geçiren kapıda ikisi de 200 aldı ve eylem koştu. Tanımsız yetenek reddedilmelidir.
- Parola
password_hash()ile saklanır vepassword_verify()ile doğrulanır; özet çıktıya yazılmaz.
Sonraki Adım
Kapı reddedince eylem atlandı ve yanıt hemen döndü. Peki eylem koşacaksa ve içindeki bir iş yanıtı geciktirecekse, bu işin yanıtı beklemesi şart mı? Kuyruklar ve İşler dersi uzun bir işi isteğin dışına bırakır ve yanıtın onu beklemediğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.