-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathauthentication.texy
More file actions
289 lines (211 loc) · 18 KB
/
authentication.texy
File metadata and controls
289 lines (211 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
Вхід користувачів (Автентифікація)
**********************************
<div class=perex>
Майже жоден веб-застосунок не обходиться без механізму входу користувачів та перевірки їхніх прав доступу. У цьому розділі ми поговоримо про:
- вхід та вихід користувачів
- власні автентифікатори
</div>
→ [Встановлення та вимоги |@home#Встановлення]
У прикладах ми будемо використовувати об'єкт класу [api:Nette\Security\User], який представляє поточного користувача і до якого ви можете отримати доступ, попросивши його передати за допомогою [dependency injection |dependency-injection:passing-dependencies]. У presenter'ах достатньо лише викликати `$user = $this->getUser()`.
Автентифікація
==============
Автентифікацією називається **вхід користувачів**, тобто процес, під час якого перевіряється, чи є користувач дійсно тим, за кого себе видає. Зазвичай він підтверджує свою особу за допомогою імені користувача та пароля. Перевірку проводить так званий [#Автентифікатор]. Якщо вхід не вдається, викидається `Nette\Security\AuthenticationException`.
```php
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Ім\'я користувача або пароль неправильні');
}
```
Таким чином ви виходите з системи користувача:
```php
$user->logout();
```
А перевірка, чи він залогінений:
```php
echo $user->isLoggedIn() ? 'так' : 'ні';
```
Дуже просто, чи не так? А всі аспекти безпеки Nette вирішує за вас.
У presenter'ах ви можете перевірити вхід у методі `startup()` і перенаправити незалогіненого користувача на сторінку входу.
```php
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
```
Термін дії
==========
Вхід користувача закінчується разом із [терміном дії сховища |#Сховище залогіненого користувача], яким зазвичай є сесія (див. налаштування [терміну дії сесії |http:configuration#Сесія]). Але можна встановити і коротший часовий інтервал, після закінчення якого користувач буде вилогінений. Для цього служить метод `setExpiration()`, який викликається перед `login()`. Як параметр вкажіть рядок з відносним часом:
```php
// вхід закінчиться після 30 хвилин неактивності
$user->setExpiration('30 minutes');
// скасування встановленого терміну дії
$user->setExpiration(null);
```
Чи був користувач вилогінений через закінчення часового інтервалу, повідомить метод `$user->getLogoutReason()`, який повертає або константу `Nette\Security\UserStorage::LogoutInactivity` (закінчився часовий ліміт), або `UserStorage::LogoutManual` (вилогінений методом `logout()`).
Автентифікатор
==============
Це об'єкт, який перевіряє облікові дані, тобто зазвичай ім'я та пароль. Тривіальною формою є клас [api:Nette\Security\SimpleAuthenticator], який ми можемо визначити в [конфігурації|configuration]:
```neon
security:
users:
# ім'я: пароль
frantisek: tajneheslo
katka: jestetajnejsiheslo
```
Це рішення підходить скоріше для тестових цілей. Покажемо, як створити автентифікатор, який буде перевіряти облікові дані за таблицею бази даних.
Автентифікатор — це об'єкт, що реалізує інтерфейс [api:Nette\Security\Authenticator] з методом `authenticate()`. Його завданням є або повернути так звану [#ідентичність], або викинути виняток `Nette\Security\AuthenticationException`. Можна було б ще вказати код помилки для більш точного розрізнення ситуації: `Authenticator::IdentityNotFound` та `Authenticator::InvalidCredential`.
```php
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|database:explorer] і працює з таблицею `users`, де в стовпці `username` знаходиться логін користувача, а в стовпці `password` — [відбиток пароля|passwords]. Після перевірки імені та пароля він повертає ідентичність, яка містить ID користувача, його роль (стовпець `role` у таблиці), про яку ми детальніше поговоримо [пізніше |authorization#Ролі], та масив з іншими даними (у нашому випадку ім'я користувача).
Автентифікатор ще додамо до конфігурації [як сервіс|dependency-injection:services] DI-контейнера:
```neon
services:
- MyAuthenticator
```
Події $onLoggedIn, $onLoggedOut
-------------------------------
Об'єкт `Nette\Security\User` має [події |nette:glossary#Події události] `$onLoggedIn` та `$onLoggedOut`, тому ви можете додати callback'и, які викликаються після успішного входу або виходу користувача відповідно.
```php
$user->onLoggedIn[] = function () {
// користувач щойно увійшов
};
```
Ідентичність
============
Ідентичність представляє набір інформації про користувача, який повертає автентифікатор і який потім зберігається в сесії, і ми отримуємо його за допомогою `$user->getIdentity()`. Таким чином, ми можемо отримати id, ролі та інші дані користувача, так як ми їх передали в автентифікаторі:
```php
$user->getIdentity()->getId();
// працює і скорочення $user->getId();
$user->getIdentity()->getRoles();
// дані користувача доступні як властивості
// ім'я, яке ми передали в MyAuthenticator
$user->getIdentity()->name;
```
Важливо, що при виході за допомогою `$user->logout()` **ідентичність не видаляється** і залишається доступною. Отже, хоча користувач має ідентичність, він може бути не залогіненим. Якщо ми хочемо явно видалити ідентичність, ми вилогінимо користувача викликом `logout(true)`.
Завдяки цьому ви можете надалі припускати, який користувач знаходиться за комп'ютером, і, наприклад, показувати йому в інтернет-магазині персоналізовані пропозиції, однак відображати його особисті дані можна лише після входу.
Ідентичність — це об'єкт, що реалізує інтерфейс [api:Nette\Security\IIdentity], стандартною реалізацією є [api:Nette\Security\SimpleIdentity]. І, як було згадано, вона зберігається в сесії, тому якщо, наприклад, ми змінимо роль одного із залогінених користувачів, старі дані залишаться в його ідентичності до його повторного входу.
Сховище залогіненого користувача
================================
Дві основні інформації про користувача, тобто чи він залогінений та його [#Ідентичність], зазвичай передаються в сесії. Це можна змінити. За зберігання цієї інформації відповідає об'єкт, що реалізує інтерфейс `Nette\Security\UserStorage`. Доступні дві стандартні реалізації: перша передає дані в сесії, а друга — в cookie. Це класи `Nette\Bridges\SecurityHttp\SessionStorage` та `CookieStorage`. Вибрати сховище та налаштувати його можна дуже зручно в конфігурації [security › authentication |configuration#Сховище].
Крім того, ви можете впливати на те, як саме буде відбуватися зберігання ідентичності (*sleep*) та відновлення (*wakeup*). Достатньо, щоб автентифікатор реалізував інтерфейс `Nette\Security\IdentityHandler`. Він має два методи: `sleepIdentity()` викликається перед записом ідентичності до сховища, а `wakeupIdentity()` — після її зчитування. Методи можуть змінювати вміст ідентичності або замінювати її новим об'єктом, який вони повернуть. Метод `wakeupIdentity()` може навіть повернути `null`, що призведе до виходу користувача з системи.
Як приклад покажемо вирішення частого питання, як оновити ролі в ідентичності одразу після завантаження з сесії. У методі `wakeupIdentity()` передамо до ідентичності актуальні ролі, наприклад, з бази даних:
```php
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`, в якому кожен користувач матиме [абсолютно випадковий, унікальний і невгадуваний|utils:random] рядок достатньої довжини (принаймні 13 символів). Сховище `CookieStorage` передає в cookie лише значення `$identity->getId()`, тому в `sleepIdentity()` ми замінимо оригінальну ідентичність на замінну з `authtoken` в ID, а навпаки, в методі `wakeupIdentity()` за authtoken'ом зчитаємо повну ідентичність з бази даних:
```php
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;
}
}
```
Кілька незалежних входів
========================
Одночасно в рамках одного веб-сайту та однієї сесії може бути кілька незалежних користувачів, що входять у систему. Якщо, наприклад, ми хочемо мати на веб-сайті окрему автентифікацію для адміністрації та публічної частини, достатньо кожній з них встановити власну назву:
```php
$user->getStorage()->setNamespace('backend');
```
Важливо пам'ятати, щоб ми завжди встановлювали простір імен у всіх місцях, що належать до відповідної частини. Якщо ми використовуємо presenter'и, ми встановимо простір імен у спільному предку для даної частини - зазвичай BasePresenter. Зробимо це, розширивши метод [checkRequirements() |api:Nette\Application\UI\Presenter::checkRequirements()]:
```php
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
```
Кілька автентифікаторів
-----------------------
Розділення застосунку на частини з незалежним входом зазвичай вимагає також різних автентифікаторів. Однак, якби ми зареєстрували в конфігурації сервісів два класи, що реалізують Authenticator, Nette не знало б, який з них автоматично призначити об'єкту `Nette\Security\User`, і відобразило б помилку. Тому ми повинні для автентифікаторів [autowiring |dependency-injection:autowiring] обмежити так, щоб він працював, лише коли хтось запитує конкретний клас, наприклад, FrontAuthenticator, чого досягнемо вибором `autowired: self`:
```neon
services:
-
create: FrontAuthenticator
autowired: self
```
```php
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
```
Автентифікатор об'єкта User встановлюємо перед викликом методу [login() |api:Nette\Security\User::login()], тобто зазвичай у коді форми, яка його залогінює:
```php
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};
```