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

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

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

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

В примерите ще използваме обект от клас 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);
	// ...
};
версия: 4.0