Вход пользователя (Аутентификация)
Почти ни одно веб-приложение не обходится без механизма входа пользователей и проверки их прав. В этой главе мы поговорим о:
- входе и выходе пользователей
- собственных аутентификаторах
В примерах мы будем использовать объект класса 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);
// ...
};