Вхід користувачів (Автентифікація)
Майже жоден веб-застосунок не обходиться без механізму входу користувачів та перевірки їхніх прав доступу. У цьому розділі ми поговоримо про:
- вхід та вихід користувачів
- власні автентифікатори
У прикладах ми будемо використовувати об'єкт класу Nette\Security\User, який представляє
поточного користувача і до якого ви можете отримати доступ, попросивши
його передати за допомогою dependency injection. У presenter'ах
достатньо лише викликати $user = $this->getUser()
.
Автентифікація
Автентифікацією називається вхід користувачів, тобто процес, під
час якого перевіряється, чи є користувач дійсно тим, за кого себе видає.
Зазвичай він підтверджує свою особу за допомогою імені користувача та
пароля. Перевірку проводить так званий autentikátor. Якщо
вхід не вдається, викидається Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Ім\'я користувача або пароль неправильні');
}
Таким чином ви виходите з системи користувача:
$user->logout();
А перевірка, чи він залогінений:
echo $user->isLoggedIn() ? 'так' : 'ні';
Дуже просто, чи не так? А всі аспекти безпеки 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:
# ім'я: пароль
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. І, як було згадано, вона зберігається в сесії, тому якщо, наприклад, ми змінимо роль одного із залогінених користувачів, старі дані залишаться в його ідентичності до його повторного входу.
Сховище залогіненого користувача
Дві основні інформації про користувача, тобто чи він залогінений та
його 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');
Важливо пам'ятати, щоб ми завжди встановлювали простір імен у всіх місцях, що належать до відповідної частини. Якщо ми використовуємо presenter'и, ми встановимо простір імен у спільному предку для даної частини – зазвичай 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);
// ...
};