Kullanıcı Kimlik Doğrulama
Neredeyse hiçbir web uygulaması, kullanıcıların giriş ve çıkış yapmasını sağlayan bir mekanizma ile kullanıcı yetkilerinin doğrulanması olmadan yapamaz. Bu bölümde şunlardan söz edeceğiz:
- kullanıcıların giriş ve çıkış yapması
- özel doğrulayıcılar
Örneklerde Nette\Security\User sınıfının bir
nesnesini kullanacağız; bu nesne geçerli kullanıcıyı temsil eder ve onu, bağımlılık enjeksiyonuyla size aktarılmasını
sağlayarak elde edersiniz. Presenter'larda yalnızca $user = $this->getUser() çağırın.
Kimlik Doğrulama
Kimlik doğrulama, kullanıcı girişi anlamına gelir; yani kullanıcının kimliğinin doğrulandığı süreç.
Kullanıcı genellikle kendini bir kullanıcı adı ve parolayla tanıtır. Doğrulamayı Doğrulayıcı yapar. Giriş başarısız olursa Nette\Security\AuthenticationException
fırlatılır.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Girdiğiniz kullanıcı adı ya da parola hatalı.');
}
Kullanıcının çıkışını şöyle yaparsınız:
$user->logout();
Ve kullanıcının giriş yapmış olup olmadığını öğrenmek için:
echo $user->isLoggedIn() ? 'evet' : 'hayır';
Çok basit, değil mi? Ve tüm güvenlik yönlerini Nette sizin için üstlenir.
Presenter'larda girişi startup() metodunda doğrulayabilir ve giriş yapmamış kullanıcıları giriş
sayfasına yönlendirebilirsiniz.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Süre Dolması
Kullanıcı girişi, genellikle oturum olan deponun süresiyle birlikte
dolar (oturum süresi ayarına bakın). Ancak,
kullanıcının çıkışının yapılacağı daha kısa bir zaman aralığı da ayarlayabilirsiniz. Bunun için
login() çağrısından önce çağrılan setExpiration() metodu kullanılır. Argüman olarak göreli
bir zaman içeren bir dize aktarın:
// giriş, 30 dakikalık etkinsizlikten sonra sona erer
$user->setExpiration('30 minutes');
// ayarlanmış süreyi iptal et
$user->setExpiration(null);
$user->getLogoutReason() metodu, kullanıcının zaman aralığı dolduğu için mi çıkarıldığını
açığa çıkarır. Ya Nette\Security\User::LogoutInactivity sabitini (zaman sınırı doldu) ya da
User::LogoutManual sabitini (logout() metodu çağrıldı) döndürür.
Doğrulayıcı
Bu, giriş bilgilerini, tipik olarak kullanıcı adı ve parolayı doğrulayan bir nesnedir. Önemsiz bir biçimi, Nette\Security\SimpleAuthenticator sınıfıdır; onu yapılandırmada tanımlayabilirsiniz:
security:
users:
# kullanıcı adı: parola
johndoe: 'secret123'
kathy: 'evenmoresecretpassword'
Düz metin parolalar yerine onların hash'lerini de girebilirsiniz; bkz. yapılandırma.
Bu çözüm daha çok sınama amaçları için uygundur. Size, giriş bilgilerini bir veritabanı tablosuna karşı doğrulayan bir doğrulayıcının nasıl oluşturulacağını göstereceğiz.
Doğrulayıcı, authenticate() metoduyla Nette\Security\Authenticator arayüzünü
gerçekleştiren bir nesnedir. Görevi ya bir Kimlik döndürmek ya da bir
Nette\Security\AuthenticationException fırlatmaktır. Durumu daha ince ayırt etmek için bir hata kodu da belirtmek
mümkündür: Authenticator::IdentityNotFound ya da Authenticator::InvalidCredential.
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
public function __construct(
private Nette\Database\Explorer $database,
private Nette\Security\Passwords $passwords,
) {
}
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('User not found.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Invalid password.');
}
return new SimpleIdentity(
$row->id,
$row->role, // ya da roller dizisi
['name' => $row->username],
);
}
}
MyAuthenticator sınıfı veritabanıyla Nette Database
Explorer üzerinden iletişim kurar ve users tablosuyla çalışır; burada username sütunu
kullanıcının giriş adını, password sütunu ise parola
hash'ini içerir. Adı ve parolayı doğruladıktan sonra, kullanıcının ID'sini, ileride daha ayrıntılı ele alacağımız rolünü
(tablodaki role sütunu) ve ek verilerden oluşan bir diziyi (bizim durumumuzda kullanıcı adını) içeren kimliği
döndürür.
Doğrulayıcıyı yapılandırmaya DI container'ın bir servisi olarak ekleyeceğiz:
services:
- MyAuthenticator
$onLoggedIn, $onLoggedOut Olayları
Nette\Security\User nesnesinin $onLoggedIn ve $onLoggedOut olayları vardır; böylece başarılı bir girişten sonra ya da
kullanıcı çıkış yaptıktan sonra tetiklenen callback'ler ekleyebilirsiniz.
$user->onLoggedIn[] = function () {
// kullanıcı az önce giriş yaptı
};
Kimlik
Kimlik, doğrulayıcının döndürdüğü ve ardından oturumda saklanan, $user->getIdentity() ile alınabilen
kullanıcı bilgileri kümesidir. Bu bize, doğrulayıcıda aktardığımız gibi ID'yi, rolleri ve diğer kullanıcı verilerini
almamızı sağlar:
$user->getIdentity()->getId();
// $user->getId() kısayolu da çalışır
$user->getIdentity()->getRoles();
// kullanıcı verilerine özellik olarak erişilir
// MyAuthenticator içinde aktardığımız kullanıcı adı
$user->getIdentity()->name;
Önemli olan şu: $user->logout() ile çıkış yapıldığında kimlik silinmez ve kullanılabilir
kalmayı sürdürür. Yani bir kullanıcının kimliği olsa bile, giriş yapmış olması gerekmez. Kimliği açıkça silmek
istersek, kullanıcının çıkışını logout(true) çağırarak yaparız.
Bu sayede bilgisayarın başında hangi kullanıcının olduğunu yine varsayabilir ve örneğin bir e-mağazada kişiselleştirilmiş teklifler gösterebilirsiniz, ama kişisel bilgilerini yalnızca giriş yaptıktan sonra gösterebilirsiniz.
Kimliği logout(true) ile çağrı başına temizlemenin yanında, onun saklanmasını
$persistIdentity özelliğiyle tümüyle kapatabilirsiniz. false yapıldığında kimlik her çıkışta
ve süre dolduğunda atılır, dolayısıyla getIdentity() o zaman null döndürür. Kimliğin
korunması depoya da bağlıdır: çerez deposu, çerezi her zaman sildiğinden onu çıkıştan sonra koruyamaz.
Kimlik, Nette\Security\IIdentity arayüzünü gerçekleştiren bir nesnedir. Varsayılan gerçekleştirim Nette\Security\SimpleIdentity sınıfıdır. Ve anıldığı gibi oturumda tutulur, dolayısıyla örneğin giriş yapmış kullanıcılardan birinin rolünü değiştirirsek, eski veri o kullanıcı yeniden giriş yapana dek kimliğinde kalır.
Giriş Yapmış Kullanıcı İçin Depo
Kullanıcı hakkındaki iki temel bilgi, yani giriş yapmış olup olmadığı ve Kimlik, genellikle
oturumda taşınır. Bu değiştirilebilir. Bu bilgiyi saklamaktan Nette\Security\UserStorage arayüzünü
gerçekleştiren bir nesne sorumludur. İki standart gerçekleştirim kullanılabilir: veriyi oturumda taşıyan
Nette\Bridges\SecurityHttp\SessionStorage ve veriyi bir çerezde taşıyan CookieStorage. Depoyu seçip
onu security › authentication
yapılandırmasında çok elverişli biçimde ayarlayabilirsiniz.
Ayrıca kimliğin kaydedilmesinin (sleep) ve geri yüklenmesinin (wakeup) tam olarak nasıl işleyeceğini de
etkileyebilirsiniz. Gereken tek şey, doğrulayıcının Nette\Security\IdentityHandler arayüzünü
gerçekleştirmesidir. sleepIdentity() metodu kimlik depoya yazılmadan önce, wakeupIdentity() ise
okunduktan sonra çağrılır. Bu metotlar kimliğin içeriğini değiştirebilir ya da onu döndürdükleri yeni bir nesneyle
değiştirebilir. wakeupIdentity() metodu null bile döndürebilir; bu, kullanıcının çıkışını
yapar. Arayüz ayrıca getGuestIdentity() metodunu da bildirir, bkz. Konuk
Kimliği.
Örnek olarak, oturumdan yüklendikten hemen sonra kimlikteki rollerin nasıl güncelleneceği yolundaki sık sorulan sorunun
çözümünü gösterelim. wakeupIdentity() metodunda, güncel rolleri örneğin bir veritabanından kimliğe
aktarırız:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// burada kimliği, girişten sonra depoya yazılmadan önce değiştirebilirsiniz,
// ama şu anda buna gereksinimimiz yok
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// kimlikteki rolleri güncelle
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
public function getGuestIdentity(): ?IIdentity
{
// burada konuk kimliği kullanılmıyor
return null;
}
Şimdi çerezlere dayalı depoya geri dönelim. Bu, kullanıcıların oturumlara gerek kalmadan giriş yapabildiği bir web
sitesi oluşturmanızı sağlar. Böylece diske yazması gerekmez. Şu anda okumakta olduğunuz web sitesi, forum dahil, böyle
çalışır. Bu durumda IdentityHandler gerçekleştirimi bir zorunluluktur. Çerezde yalnızca, giriş yapmış
kullanıcıyı temsil eden rastgele bir token saklayacağız.
Önce yapılandırmada security › authentication › storage: cookie ile gereken depoyu ayarlayın.
Veritabanında authtoken sütununu oluşturun; burada her kullanıcının yeterli uzunlukta (en az 13 karakter),
tümüyle rastgele, benzersiz ve tahmin edilemez bir dizesi olacak.
CookieStorage, çerezde yalnızca $identity->getId() değerini taşır, dolayısıyla
sleepIdentity() içinde özgün kimliği, ID'sinde authtoken bulunan bir vekil kimlikle değiştiririz.
Tersine, wakeupIdentity() metodunda kimliğin tamamını authtoken'a göre veritabanından okuruz:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
// parolayı doğrula
// ...
// veritabanındaki tüm verilerle kimliği döndür
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// ID'sinde authtoken bulunan bir vekil kimlik döndür
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// vekil kimliği, authenticate() içindeki gibi tam kimlikle değiştir
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
public function getGuestIdentity(): ?IIdentity
{
// burada konuk kimliği kullanılmıyor
return null;
}
}
Konuk Kimliği
Bazen giriş yapmamış ziyaretçilerin de bir kimliğinin olması işe yarar; örneğin onlara varsayılan bir rol kümesi ya
da bir miktar veri vermek için. Doğrulayıcı IdentityHandler arayüzünü gerçekleştiriyorsa, kimse giriş
yapmamışken kullanılan getGuestIdentity() metoduyla bir kimlik sağlayabilir. O zaman getIdentity(),
getId() ve getRoles() ona geri düşer, böylece konukların yalın guest rolü yerine
kendi rolleri olabilir. Konuk kimliği istemiyorsanız null döndürün.
public function getGuestIdentity(): ?IIdentity
{
return new SimpleIdentity('guest', ['guest'], ['name' => 'Guest']);
}
Konuk kimliği asla depoya kaydedilmez ve giriş yapmak onu her zaman değiştirir.
Birden Çok Bağımsız Giriş
Tek bir web sitesi ve tek bir oturum içinde, aynı anda birden çok bağımsız kullanıcının giriş yapması mümkündür. Örneğin yönetim ile web sitesinin herkese açık bölümü için ayrı kimlik doğrulama istiyorsak, her biri için benzersiz bir ad alanı ayarlamamız yeterlidir:
$user->getStorage()->setNamespace('backend');
Ad alanını, ilgili bölüme ait tüm yerlerde her zaman ayarlamayı unutmamak önemlidir. Presenter'lar kullanıyorsak, ad alanını o bölümün ortak atasında, genellikle BasePresenter'da ayarlarız. Bunu checkRequirements() metodunu genişleterek yaparız:
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Ad alanını tek bir istek sırasında (kimlik doğrulama durumu zaten okunduktan sonra) değiştirirseniz, User
nesnesi hâlâ önceki ad alanından önbelleğe alınmış durumu tutar. Bu durumda önbelleği atmak ve yeni ad alanından
yeniden yüklemeyi zorlamak için refreshStorage() çağırın:
$user->getStorage()->setNamespace('admin');
$user->refreshStorage(); // durumu yeni ad alanından yeniden yükle
Birden Çok Doğrulayıcı
Bir uygulamayı bağımsız girişi olan bölümlere ayırmak genellikle farklı doğrulayıcılar da gerektirir. Ancak servis
yapılandırmasında Authenticator arayüzünü gerçekleştiren iki sınıf kaydedersek, Nette hangisini
Nette\Security\User nesnesine otomatik atayacağını bilemez ve bir hata gösterir. Bu yüzden doğrulayıcılar
için autowiring işlemini, yalnızca biri belirli bir
sınıfı, örneğin FrontAuthenticator sınıfını istediğinde çalışacak biçimde kısıtlamalıyız. Bu,
autowired: self seçilerek sağlanır:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
User nesnesinin doğrulayıcısını login() metodunu çağırmadan önce, yani genellikle girişi yapan formun kodunda ayarlarız:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};