Nette Documentation Preview

syntax
Вход на потребители (Автентикация)
**********************************

<div class=perex>

Почти няма уеб приложение, което да се справи без механизъм за влизане на потребители и проверка на потребителските права. В тази глава ще говорим за:

- влизане и излизане на потребители
- собствени автентикатори

</div>

→ [Инсталация и изисквания |@home#Инсталация]

В примерите ще използваме обект от клас [api:Nette\Security\User], който представлява текущия потребител и до който можете да стигнете, като си го поискате чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате `$user = $this->getUser()`.


Автентикация
============

Автентикацията означава **влизане на потребители**, тоест процес, при който се проверява дали потребителят е наистина този, за когото се представя. Обикновено се доказва с потребителско име и парола. Проверката се извършва от т.нар. [#автентикатор]. Ако влизането се провали, се хвърля `Nette\Security\AuthenticationException`.

```php
try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('Потребителското име или паролата са грешни');
}
```

По този начин излизате от системата:

```php
$user->logout();
```

И проверка дали е влязъл:

```php
echo $user->isLoggedIn() ? 'да' : 'не';
```

Много просто, нали? И всички аспекти на сигурността Nette решава за вас.

В презентерите можете да проверите влизането в метода `startup()` и да пренасочите невписания потребител към страницата за вход.

```php
protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}
```


Изтичане на валидност
=====================

Влизането на потребителя изтича заедно с [изтичането на валидността на хранилището |#Хранилище на влезлия потребител], което обикновено е сесия (вижте настройката на [изтичане на сесията |http:configuration#Сесия]). Възможно е обаче да се зададе и по-кратък времеви интервал, след изтичането на който потребителят ще бъде излязъл от системата. За това служи методът `setExpiration()`, който се извиква преди `login()`. Като параметър посочете низ с относително време:

```php
// влизането изтича след 30 минути неактивност
$user->setExpiration('30 minutes');

// отмяна на зададеното изтичане
$user->setExpiration(null);
```

Дали потребителят е бил излязъл поради изтичане на времевия интервал разкрива методът `$user->getLogoutReason()`, който връща или константата `Nette\Security\UserStorage::LogoutInactivity` (изтекъл е времевият лимит) или `UserStorage::LogoutManual` (излязъл с метода `logout()`).


Автентикатор
============

Това е обект, който проверява данните за вход, тоест обикновено име и парола. Тривиална форма е класът [api:Nette\Security\SimpleAuthenticator], който можем да дефинираме в [конфигурацията|configuration]:

```neon
security:
	users:
		# име: парола
		frantisek: tajneheslo
		katka: jestetajnejsiheslo
```

Това решение е подходящо по-скоро за тестови цели. Ще покажем как да създадем автентикатор, който ще проверява данните за вход спрямо таблица в база данни.

Автентикаторът е обект, имплементиращ интерфейса [api:Nette\Security\Authenticator] с метод `authenticate()`. Неговата задача е или да върне т.нар. [#идентичност] или да хвърли изключение `Nette\Security\AuthenticationException`. Би било възможно при нея още да се посочи код за грешка за по-фино разграничаване на възникналата ситуация: `Authenticator::IdentityNotFound` и `Authenticator::InvalidCredential`.

```php
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('Потребителят не е намерен.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Невалидна парола.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // или масив от няколко роли
			['name' => $row->username],
		);
	}
}
```

Класът MyAuthenticator комуникира с базата данни чрез [Nette Database Explorer|database:explorer] и работи с таблицата `users`, където в колоната `username` е потребителското име на потребителя, а в колоната `password` е [хешът на паролата|passwords]. След проверка на името и паролата връща идентичност, която носи ID на потребителя, неговата роля (колона `role` в таблицата), за която ще говорим повече [по-късно |authorization#Роля], и масив с други данни (в нашия случай потребителско име).

Автентикаторът още ще добавим към конфигурацията [като сървис|dependency-injection:services] на DI контейнера:

```neon
services:
	- MyAuthenticator
```


Събития $onLoggedIn, $onLoggedOut
---------------------------------

Обектът `Nette\Security\User` има [събития |nette:glossary#Събития events] `$onLoggedIn` и `$onLoggedOut`, така че можете да добавите callback-ове, които се извикват след успешно влизане респ. след излизане на потребителя.


```php
$user->onLoggedIn[] = function () {
	// потребителят току-що е влязъл
};
```


Идентичност
===========

Идентичността представлява набор от информация за потребителя, която връща автентикаторът и която впоследствие се съхранява в сесията и я получаваме с помощта на `$user->getIdentity()`. Можем така да получим id, роли и други потребителски данни, както сме ги предали в автентикатора:

```php
$user->getIdentity()->getId();
// работи и съкращението $user->getId();

$user->getIdentity()->getRoles();

// потребителските данни са достъпни като свойства
// името, което сме предали в MyAuthenticator
$user->getIdentity()->name;
```

Важното е, че при излизане с помощта на `$user->logout()` **идентичността не се изтрива** и е все още налична. Така че, въпреки че потребителят има идентичност, не е задължително да е влязъл. Ако искаме изрично да изтрием идентичността, излизаме от системата с извикване на `logout(true)`.

Благодарение на това можете да продължите да предполагате кой потребител е пред компютъра и например да му показвате персонализирани оферти в електронния магазин, но можете да му покажете личните му данни едва след като влезе.

Идентичността е обект, имплементиращ интерфейса [api:Nette\Security\IIdentity], имплементацията по подразбиране е [api:Nette\Security\SimpleIdentity]. И както беше споменато, поддържа се в сесията, така че ако например променим ролята на някой от влезлите потребители, старите данни ще останат в неговата идентичност до следващото му влизане.


Хранилище на влезлия потребител
===============================

Двете основни информации за потребителя, а именно дали е влязъл и неговата [#идентичност], обикновено се пренасят в сесията. Което може да се промени. За съхраняването на тази информация отговаря обект, имплементиращ интерфейса `Nette\Security\UserStorage`. Налични са две стандартни имплементации, първата пренася данни в сесията, а втората в бисквитка. Това са класовете `Nette\Bridges\SecurityHttp\SessionStorage` и `CookieStorage`. Можете да изберете хранилище и да го конфигурирате много удобно в конфигурацията [security › authentication |configuration#Хранилище].

Освен това можете да повлияете на това как точно ще протича съхраняването на идентичността (*sleep*) и възстановяването (*wakeup*). Достатъчно е автентикаторът да имплементира интерфейса `Nette\Security\IdentityHandler`. Той има два метода: `sleepIdentity()` се извиква преди запис на идентичността в хранилището и `wakeupIdentity()` след нейното прочитане. Методите могат да променят съдържанието на идентичността, евентуално да я заменят с нов обект, който връщат. Методът `wakeupIdentity()` може дори да върне `null`, с което потребителят ще бъде излязъл от системата.

Като пример ще покажем решение на често задавания въпрос как да актуализираме ролите в идентичността веднага след зареждане от сесията. В метода `wakeupIdentity()` ще предадем на идентичността актуалните роли, например от базата данни:

```php
final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// тук може да се промени идентичността преди запис в хранилището след влизане,
		// но това сега не ни е необходимо
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// актуализация на ролите в идентичността
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}
```

А сега ще се върнем към хранилището, базирано на бисквитки. То ви позволява да създадете уеб сайт, където потребителите могат да влизат, без да са необходими сесии. Тоест, не е необходимо да се записва на диска. Впрочем, така работи и уеб сайтът, който четете в момента, включително форумът. В този случай имплементацията на `IdentityHandler` е задължителна. В бисквитката ще съхраняваме само случаен токен, представляващ влезлия потребител.

Първо, в конфигурацията ще зададем желаното хранилище с помощта на `security › authentication › storage: cookie`.

В базата данни ще създадем колона `authtoken`, в която всеки потребител ще има [напълно случаен, уникален и невъзможен за отгатване|utils:random] низ с достатъчна дължина (поне 13 знака). Хранилището `CookieStorage` пренася в бисквитката само стойността `$identity->getId()`, така че в `sleepIdentity()` ще заменим оригиналната идентичност със заместваща с `authtoken` в ID, а обратно, в метода `wakeupIdentity()` по authtoken ще прочетем цялата идентичност от базата данни:

```php
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);
		// проверяваме паролата
		...
		// връщаме идентичност с всички данни от базата данни
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// връщаме заместваща идентичност, където в ID ще бъде authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// заместващата идентичност заменяме с пълна идентичност, както в authenticate()
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}
```


Няколко независими влизания
===========================

Едновременно е възможно в рамките на един уеб сайт и една сесия да има няколко независими влизащи потребители. Ако например искаме да имаме на уеб сайта отделна автентикация за администрацията и публичната част, е достатъчно на всяка от тях да зададем собствено име:

```php
$user->getStorage()->setNamespace('backend');
```

Важно е да помним, че трябва да зададем пространството от имена винаги на всички места, принадлежащи към дадената част. Ако използваме презентери, ще зададем пространството от имена в общия предшественик за дадената част - обикновено BasePresenter. Ще направим това чрез разширяване на метода [checkRequirements() |api:Nette\Application\UI\Presenter::checkRequirements()]:

```php
public function checkRequirements($element): void
{
	$this->getUser()->getStorage()->setNamespace('backend');
	parent::checkRequirements($element);
}
```


Няколко автентикатора
---------------------

Разделянето на приложението на части с независимо влизане обикновено изисква и различни автентикатори. Веднага щом обаче регистрираме в конфигурацията на услугите два класа, имплементиращи Authenticator, Nette няма да знае кой от тях автоматично да присвои на обекта `Nette\Security\User` и ще покаже грешка. Затова трябва за автентикаторите да ограничим [autowiring |dependency-injection:autowiring] така, че да работи само когато някой поиска конкретен клас, напр. FrontAuthenticator, което ще постигнем с избора `autowired: self`:

```neon
services:
	-
		create: FrontAuthenticator
		autowired: self
```

```php
class SignPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FrontAuthenticator $authenticator,
	) {
	}
}
```

Автентикаторът на обекта User ще зададем преди извикване на метода [login() |api:Nette\Security\User::login()], така че обикновено в кода на формата, който го вписва:

```php
$form->onSuccess[] = function (Form $form, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};
```

Вход на потребители (Автентикация)

Почти няма уеб приложение, което да се справи без механизъм за влизане на потребители и проверка на потребителските права. В тази глава ще говорим за:

  • влизане и излизане на потребители
  • собствени автентикатори

Инсталация и изисквания

В примерите ще използваме обект от клас Nette\Security\User, който представлява текущия потребител и до който можете да стигнете, като си го поискате чрез dependency injection. В презентерите е достатъчно само да извикате $user = $this->getUser().

Автентикация

Автентикацията означава влизане на потребители, тоест процес, при който се проверява дали потребителят е наистина този, за когото се представя. Обикновено се доказва с потребителско име и парола. Проверката се извършва от т.нар. автентикатор. Ако влизането се провали, се хвърля Nette\Security\AuthenticationException.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('Потребителското име или паролата са грешни');
}

По този начин излизате от системата:

$user->logout();

И проверка дали е влязъл:

echo $user->isLoggedIn() ? 'да' : 'не';

Много просто, нали? И всички аспекти на сигурността Nette решава за вас.

В презентерите можете да проверите влизането в метода startup() и да пренасочите невписания потребител към страницата за вход.

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

Изтичане на валидност

Влизането на потребителя изтича заедно с изтичането на валидността на хранилището, което обикновено е сесия (вижте настройката на изтичане на сесията). Възможно е обаче да се зададе и по-кратък времеви интервал, след изтичането на който потребителят ще бъде излязъл от системата. За това служи методът setExpiration(), който се извиква преди login(). Като параметър посочете низ с относително време:

// влизането изтича след 30 минути неактивност
$user->setExpiration('30 minutes');

// отмяна на зададеното изтичане
$user->setExpiration(null);

Дали потребителят е бил излязъл поради изтичане на времевия интервал разкрива методът $user->getLogoutReason(), който връща или константата Nette\Security\UserStorage::LogoutInactivity (изтекъл е времевият лимит) или UserStorage::LogoutManual (излязъл с метода logout()).

Автентикатор

Това е обект, който проверява данните за вход, тоест обикновено име и парола. Тривиална форма е класът Nette\Security\SimpleAuthenticator, който можем да дефинираме в конфигурацията:

security:
	users:
		# име: парола
		frantisek: tajneheslo
		katka: jestetajnejsiheslo

Това решение е подходящо по-скоро за тестови цели. Ще покажем как да създадем автентикатор, който ще проверява данните за вход спрямо таблица в база данни.

Автентикаторът е обект, имплементиращ интерфейса Nette\Security\Authenticator с метод authenticate(). Неговата задача е или да върне т.нар. идентичност или да хвърли изключение Nette\Security\AuthenticationException. Би било възможно при нея още да се посочи код за грешка за по-фино разграничаване на възникналата ситуация: Authenticator::IdentityNotFound и 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('Потребителят не е намерен.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Невалидна парола.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // или масив от няколко роли
			['name' => $row->username],
		);
	}
}

Класът MyAuthenticator комуникира с базата данни чрез Nette Database Explorer и работи с таблицата users, където в колоната username е потребителското име на потребителя, а в колоната password е хешът на паролата. След проверка на името и паролата връща идентичност, която носи ID на потребителя, неговата роля (колона role в таблицата), за която ще говорим повече по-късно, и масив с други данни (в нашия случай потребителско име).

Автентикаторът още ще добавим към конфигурацията като сървис на DI контейнера:

services:
	- MyAuthenticator

Събития $onLoggedIn, $onLoggedOut

Обектът Nette\Security\User има събития $onLoggedIn и $onLoggedOut, така че можете да добавите callback-ове, които се извикват след успешно влизане респ. след излизане на потребителя.

$user->onLoggedIn[] = function () {
	// потребителят току-що е влязъл
};

Идентичност

Идентичността представлява набор от информация за потребителя, която връща автентикаторът и която впоследствие се съхранява в сесията и я получаваме с помощта на $user->getIdentity(). Можем така да получим id, роли и други потребителски данни, както сме ги предали в автентикатора:

$user->getIdentity()->getId();
// работи и съкращението $user->getId();

$user->getIdentity()->getRoles();

// потребителските данни са достъпни като свойства
// името, което сме предали в MyAuthenticator
$user->getIdentity()->name;

Важното е, че при излизане с помощта на $user->logout() идентичността не се изтрива и е все още налична. Така че, въпреки че потребителят има идентичност, не е задължително да е влязъл. Ако искаме изрично да изтрием идентичността, излизаме от системата с извикване на logout(true).

Благодарение на това можете да продължите да предполагате кой потребител е пред компютъра и например да му показвате персонализирани оферти в електронния магазин, но можете да му покажете личните му данни едва след като влезе.

Идентичността е обект, имплементиращ интерфейса Nette\Security\IIdentity, имплементацията по подразбиране е Nette\Security\SimpleIdentity. И както беше споменато, поддържа се в сесията, така че ако например променим ролята на някой от влезлите потребители, старите данни ще останат в неговата идентичност до следващото му влизане.

Хранилище на влезлия потребител

Двете основни информации за потребителя, а именно дали е влязъл и неговата идентичност, обикновено се пренасят в сесията. Което може да се промени. За съхраняването на тази информация отговаря обект, имплементиращ интерфейса Nette\Security\UserStorage. Налични са две стандартни имплементации, първата пренася данни в сесията, а втората в бисквитка. Това са класовете Nette\Bridges\SecurityHttp\SessionStorage и CookieStorage. Можете да изберете хранилище и да го конфигурирате много удобно в конфигурацията security › authentication.

Освен това можете да повлияете на това как точно ще протича съхраняването на идентичността (sleep) и възстановяването (wakeup). Достатъчно е автентикаторът да имплементира интерфейса Nette\Security\IdentityHandler. Той има два метода: sleepIdentity() се извиква преди запис на идентичността в хранилището и wakeupIdentity() след нейното прочитане. Методите могат да променят съдържанието на идентичността, евентуално да я заменят с нов обект, който връщат. Методът wakeupIdentity() може дори да върне null, с което потребителят ще бъде излязъл от системата.

Като пример ще покажем решение на често задавания въпрос как да актуализираме ролите в идентичността веднага след зареждане от сесията. В метода wakeupIdentity() ще предадем на идентичността актуалните роли, например от базата данни:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// тук може да се промени идентичността преди запис в хранилището след влизане,
		// но това сега не ни е необходимо
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// актуализация на ролите в идентичността
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

А сега ще се върнем към хранилището, базирано на бисквитки. То ви позволява да създадете уеб сайт, където потребителите могат да влизат, без да са необходими сесии. Тоест, не е необходимо да се записва на диска. Впрочем, така работи и уеб сайтът, който четете в момента, включително форумът. В този случай имплементацията на IdentityHandler е задължителна. В бисквитката ще съхраняваме само случаен токен, представляващ влезлия потребител.

Първо, в конфигурацията ще зададем желаното хранилище с помощта на security › authentication › storage: cookie.

В базата данни ще създадем колона authtoken, в която всеки потребител ще има напълно случаен, уникален и невъзможен за отгатване низ с достатъчна дължина (поне 13 знака). Хранилището CookieStorage пренася в бисквитката само стойността $identity->getId(), така че в sleepIdentity() ще заменим оригиналната идентичност със заместваща с authtoken в ID, а обратно, в метода wakeupIdentity() по authtoken ще прочетем цялата идентичност от базата данни:

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);
		// проверяваме паролата
		...
		// връщаме идентичност с всички данни от базата данни
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// връщаме заместваща идентичност, където в ID ще бъде authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// заместващата идентичност заменяме с пълна идентичност, както в authenticate()
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

Няколко независими влизания

Едновременно е възможно в рамките на един уеб сайт и една сесия да има няколко независими влизащи потребители. Ако например искаме да имаме на уеб сайта отделна автентикация за администрацията и публичната част, е достатъчно на всяка от тях да зададем собствено име:

$user->getStorage()->setNamespace('backend');

Важно е да помним, че трябва да зададем пространството от имена винаги на всички места, принадлежащи към дадената част. Ако използваме презентери, ще зададем пространството от имена в общия предшественик за дадената част – обикновено BasePresenter. Ще направим това чрез разширяване на метода checkRequirements():

public function checkRequirements($element): void
{
	$this->getUser()->getStorage()->setNamespace('backend');
	parent::checkRequirements($element);
}

Няколко автентикатора

Разделянето на приложението на части с независимо влизане обикновено изисква и различни автентикатори. Веднага щом обаче регистрираме в конфигурацията на услугите два класа, имплементиращи Authenticator, Nette няма да знае кой от тях автоматично да присвои на обекта Nette\Security\User и ще покаже грешка. Затова трябва за автентикаторите да ограничим autowiring така, че да работи само когато някой поиска конкретен клас, напр. FrontAuthenticator, което ще постигнем с избора autowired: self:

services:
	-
		create: FrontAuthenticator
		autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FrontAuthenticator $authenticator,
	) {
	}
}

Автентикаторът на обекта User ще зададем преди извикване на метода login(), така че обикновено в кода на формата, който го вписва:

$form->onSuccess[] = function (Form $form, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};