Аутентификация пользователей

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

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

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

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

Аутентификация

Аутентификация означает вход пользователя в систему, т.е. процесс, в ходе которого проверяется личность пользователя. Пользователь обычно идентифицирует себя с помощью имени пользователя и пароля. Верификация выполняется так называемым аутентификатором. Если вход в систему не удается, происходит выброс Nette\Security\AuthenticationException.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('The username or password you entered is incorrect.');
}

Вот как выйти из системы:

$user->logout();

И проверить, вошел ли пользователь в систему:

echo $user->isLoggedIn() ? 'yes' : 'no';

Просто, правда? И все аспекты безопасности обрабатываются Nette за вас.

В Presenter вы можете проверить вход в систему в методе 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:
		# name: password
		johndoe: secret123
		kathy: evenmoresecretpassword

Это решение больше подходит для целей тестирования. Мы покажем вам, как создать аутентификатор, который будет проверять учетные данные по таблице базы данных.

Аутентификатор – это объект, реализующий интерфейс 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 Events

Объект Nette\Security\User имеет события $onLoggedIn и $onLoggedOut, поэтому вы можете добавить обратные вызовы, которые срабатывают после успешного входа в систему или после выхода пользователя из системы.

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

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

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

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

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

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

Важно отметить, что когда пользователь выходит из системы с помощью $user->logout(), идентичность не удаляется и все еще доступна. Таким образом, если идентификатор существует, он сам по себе не гарантирует, что пользователь также вошел в систему. Если мы хотим явным образом удалить идентификатор, мы выходим из системы с помощью logout(true).

Благодаря этому вы все еще можете определить, какой пользователь находится за компьютером, и, например, отображать персонализированные предложения в интернет-магазине, однако вы можете отображать его личные данные только после входа в систему.

Identity – это объект, реализующий интерфейс Nette\Security\IIdentity, реализация по умолчанию – Nette\Security\SimpleIdentity. Как уже упоминалось, идентификатор хранится в сессии, поэтому если, например, мы изменим роль какого-то из вошедших в систему пользователей, старые данные будут храниться в идентификаторе до тех пор, пока он снова не войдет в систему.

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

Две основные части информации о пользователе, т.е. вошел ли он в систему и его личность, обычно хранятся в сессии. Которая может быть изменена. За хранение этой информации отвечает объект, реализующий интерфейс 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 хранит только значение $identity->getId() в cookie, поэтому в методе sleepIdentity() мы заменим оригинальную личность на прокси с authtoken в ID, а в методе wakeupIdentity(), наоборот, восстановим всю личность из базы данных по auttoken:

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
	{
		// мы возвращаем идентификатор прокси, где в качестве идентификатора выступает 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;
	}
}

Множественная независимая аутентификация

Можно иметь несколько независимых зарегистрированных пользователей в рамках одного сайта и одной сессии одновременно. Например, если мы хотим иметь отдельную аутентификацию для frontend и backend, мы просто установим уникальное пространство имен сессии для каждого из них:

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

Необходимо помнить, что оно должно быть задано во всех местах, принадлежащих одному сегменту. При использовании презентаторов мы установим пространство имен в общем предке – обычно BasePresenter. Для этого мы расширим метод checkRequirements():

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

Множественные аутентификаторы

Разделение приложения на сегменты с независимой аутентификацией обычно требует использования разных аутентификаторов. Однако регистрация двух классов, реализующих Authenticator, в конфигурационных службах приведет к ошибке, поскольку Nette не будет знать, какой из них должен быть автоподключен к объекту Nette\Security\User. Вот почему мы должны ограничить автоподключение для них с помощью 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);
	// ...
};