Вхід користувачів (Автентифікація)

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

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

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

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