Аутентифікація користувачів

Мало-мальськи значущі веб-додатки не потребують механізму для входу користувачів у систему або перевірки їхніх привілеїв. У цьому розділі ми поговоримо про:

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

Встановлення та вимоги

У прикладах ми будемо використовувати об'єкт класу 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);
	// ...
};
версію: 4.0