Вход на потребители (Автентикация)
Почти няма уеб приложение, което да се справи без механизъм за влизане на потребители и проверка на потребителските права. В тази глава ще говорим за:
- влизане и излизане на потребители
- собствени автентикатори
В примерите ще използваме обект от клас 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('Потребителят не е намерен.');
}
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. И както беше споменато, поддържа се в сесията, така че ако например променим ролята на някой от влезлите потребители, старите данни ще останат в неговата идентичност до следващото му влизане.
Хранилище на влезлия потребител
Двете основни информации за потребителя, а именно дали е влязъл и
неговата идентичност, обикновено се пренасят в сесията.
Което може да се промени. За съхраняването на тази информация отговаря
обект, имплементиращ интерфейса Nette\Security\UserStorage
. Налични са две
стандартни имплементации, първата пренася данни в сесията, а втората в
бисквитка. Това са класовете 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;
}
А сега ще се върнем към хранилището, базирано на бисквитки. То ви
позволява да създадете уеб сайт, където потребителите могат да влизат,
без да са необходими сесии. Тоест, не е необходимо да се записва на
диска. Впрочем, така работи и уеб сайтът, който четете в момента,
включително форумът. В този случай имплементацията на IdentityHandler
е задължителна. В бисквитката ще съхраняваме само случаен токен,
представляващ влезлия потребител.
Първо, в конфигурацията ще зададем желаното хранилище с помощта на
security › authentication › storage: cookie
.
В базата данни ще създадем колона authtoken
, в която всеки
потребител ще има напълно случаен, уникален
и невъзможен за отгатване низ с достатъчна дължина (поне 13 знака).
Хранилището CookieStorage
пренася в бисквитката само стойността
$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);
// ...
};