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

Kurulum ve gereksinimler

Ö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);
	// ...
};
versiyon: 4.x