Вход пользователя (Аутентификация)

Почти ни одно веб-приложение не обходится без механизма входа пользователей и проверки их прав. В этой главе мы поговорим о:

  • входе и выходе пользователей
  • собственных аутентификаторах

Установка и требования

В примерах мы будем использовать объект класса 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('User not found.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Invalid password.');
		}

		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, поэтому вы можете добавить обратные вызовы (callbacks), которые будут вызваны после успешного входа или выхода пользователя соответственно.

$user->onLoggedIn[] = function () {
	// пользователь только что вошел в систему
};

Идентификатор (Identity)

Идентификатор представляет собой набор информации о пользователе, который возвращает аутентификатор и который затем сохраняется в сессии и получается с помощью $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. И, как было упомянуто, он хранится в сессии, поэтому если, например, мы изменим роль одного из вошедших пользователей, старые данные останутся в его идентификаторе до его повторного входа.

Хранилище вошедшего пользователя

Две основные информации о пользователе, то есть вошел ли он в систему и его identita, обычно передаются в сессии. Что можно изменить. За хранение этой информации отвечает объект, реализующий интерфейс Nette\Security\UserStorage. Доступны две стандартные реализации: первая передает данные в сессии, а вторая — в cookie. Это классы 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;
	}

А теперь вернемся к хранилищу на основе cookie. Оно позволяет вам создать веб-сайт, где пользователи могут входить в систему, и при этом не требует сессий. То есть не требует записи на диск. Впрочем, так работает и веб-сайт, который вы сейчас читаете, включая форум. В этом случае реализация IdentityHandler является необходимостью. В cookie мы будем хранить только случайный токен, представляющий вошедшего пользователя.

Сначала в конфигурации установим требуемое хранилище с помощью security › authentication › storage: cookie.

В базе данных создадим столбец authtoken, в котором у каждого пользователя будет совершенно случайная, уникальная и неугадываемая строка достаточной длины (не менее 13 символов). Хранилище CookieStorage передает в cookie только значение $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