-
-
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) · 12.9 KB
/
authentication.texy
File metadata and controls
289 lines (211 loc) · 12.9 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
Login de usuários (Autenticação)
********************************
<div class=perex>
Quase nenhuma aplicação web dispensa um mecanismo de login de usuários e verificação de permissões de usuário. Neste capítulo, falaremos sobre:
- login e logout de usuários
- autenticadores personalizados
</div>
→ [Instalação e requisitos |@home#Instalação]
Nos exemplos, usaremos o objeto da classe [api:Nette\Security\User], que representa o usuário atual e ao qual você pode acessar solicitando-o através de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar `$user = $this->getUser()`.
Autenticação
============
Autenticação significa **login de usuários**, ou seja, o processo pelo qual se verifica se o usuário é realmente quem ele diz ser. Geralmente, ele se comprova com nome de usuário e senha. A verificação é realizada pelo chamado [##autenticador]. Se o login falhar, uma `Nette\Security\AuthenticationException` é lançada.
```php
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Nome de usuário ou senha incorretos');
}
```
Desta forma, você faz logout do usuário:
```php
$user->logout();
```
E para verificar se ele está logado:
```php
echo $user->isLoggedIn() ? 'sim' : 'não';
```
Muito simples, não é? E todos os aspectos de segurança são tratados pelo Nette para você.
Nos presenters, você pode verificar o login no método `startup()` e redirecionar o usuário não logado para a página de login.
```php
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
```
Expiração
=========
O login do usuário expira junto com a [expiração do armazenamento |#Armazenamento do usuário logado], que geralmente é a sessão (veja a configuração de [expiração da sessão |http:configuration#Sessão]). No entanto, também é possível definir um intervalo de tempo menor, após o qual o usuário será desconectado. Para isso, serve o método `setExpiration()`, que é chamado antes de `login()`. Como parâmetro, forneça uma string com tempo relativo:
```php
// o login expira após 30 minutos de inatividade
$user->setExpiration('30 minutes');
// cancelamento da expiração definida
$user->setExpiration(null);
```
Se o usuário foi desconectado devido à expiração do intervalo de tempo, o método `$user->getLogoutReason()` informa, retornando a constante `Nette\Security\UserStorage::LogoutInactivity` (limite de tempo expirado) ou `UserStorage::LogoutManual` (desconectado pelo método `logout()`).
Autenticador
============
É um objeto que verifica as credenciais de login, ou seja, geralmente nome e senha. Uma forma trivial é a classe [api:Nette\Security\SimpleAuthenticator], que podemos definir na [configuração|configuration]:
```neon
security:
users:
# nome: senha
frantisek: tajneheslo
katka: jestetajnejsiheslo
```
Esta solução é mais adequada para fins de teste. Mostraremos como criar um autenticador que verificará as credenciais de login em uma tabela do banco de dados.
O autenticador é um objeto que implementa a interface [api:Nette\Security\Authenticator] com o método `authenticate()`. Sua tarefa é retornar a chamada [#identidade] ou lançar uma exceção `Nette\Security\AuthenticationException`. Seria possível ainda indicar um código de erro para distinguir mais finamente a situação ocorrida: `Authenticator::IdentityNotFound` e `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('User not found.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Invalid password.');
}
return new SimpleIdentity(
$row->id,
$row->role, // ou um array de múltiplos papéis
['name' => $row->username],
);
}
}
```
A classe MyAuthenticator comunica com o banco de dados através do [Nette Database Explorer|database:explorer] e trabalha com a tabela `users`, onde na coluna `username` está o nome de login do usuário e na coluna `password` está o [hash da senha|passwords]. Após verificar o nome e a senha, retorna a identidade, que carrega o ID do usuário, seu papel (coluna `role` na tabela), sobre o qual falaremos mais [posteriormente |authorization#Papéis], e um array com outros dados (no nosso caso, o nome de usuário).
Adicionaremos ainda o autenticador à configuração [como um serviço|dependency-injection:services] do contêiner DI:
```neon
services:
- MyAuthenticator
```
Eventos $onLoggedIn, $onLoggedOut
---------------------------------
O objeto `Nette\Security\User` tem [eventos |nette:glossary#Eventos] `$onLoggedIn` e `$onLoggedOut`, então você pode adicionar callbacks que são chamados após o login bem-sucedido ou após o logout do usuário.
```php
$user->onLoggedIn[] = function () {
// o usuário acabou de fazer login
};
```
Identidade
==========
A identidade representa um conjunto de informações sobre o usuário, que é retornado pelo autenticador e que é subsequentemente armazenado na sessão e obtido usando `$user->getIdentity()`. Podemos, portanto, obter o id, papéis e outros dados do usuário, da forma como os passamos no autenticador:
```php
$user->getIdentity()->getId();
// o atalho $user->getId() também funciona;
$user->getIdentity()->getRoles();
// os dados do usuário estão disponíveis como propriedades
// nome que passamos em MyAuthenticator
$user->getIdentity()->name;
```
O que é importante é que, ao fazer logout usando `$user->logout()`, **a identidade não é apagada** e continua disponível. Portanto, embora o usuário tenha uma identidade, ele pode não estar logado. Se quiséssemos apagar explicitamente a identidade, faríamos logout do usuário chamando `logout(true)`.
Graças a isso, você pode continuar a presumir qual usuário está no computador e, por exemplo, exibir ofertas personalizadas em uma loja online, mas só pode exibir seus dados pessoais após o login.
A identidade é um objeto que implementa a interface [api:Nette\Security\IIdentity], a implementação padrão é [api:Nette\Security\SimpleIdentity]. E como mencionado, ela é mantida na sessão, então se, por exemplo, alterarmos o papel de algum dos usuários logados, os dados antigos permanecerão em sua identidade até que ele faça login novamente.
Armazenamento do usuário logado
===============================
Duas informações básicas sobre o usuário, ou seja, se ele está logado e sua [#identidade], geralmente são transmitidas na sessão. O que pode ser alterado. O armazenamento dessas informações é responsabilidade de um objeto que implementa a interface `Nette\Security\UserStorage`. Existem duas implementações padrão disponíveis, a primeira transmite dados na sessão e a segunda em um cookie. São as classes `Nette\Bridges\SecurityHttp\SessionStorage` e `CookieStorage`. Você pode escolher o armazenamento e configurá-lo muito convenientemente na configuração [security › authentication |configuration#Armazenamento].
Além disso, você pode influenciar como exatamente o armazenamento da identidade ocorrerá (*sleep*) e a restauração (*wakeup*). Basta que o autenticador implemente a interface `Nette\Security\IdentityHandler`. Ela tem dois métodos: `sleepIdentity()` é chamado antes de escrever a identidade no armazenamento e `wakeupIdentity()` após sua leitura. Os métodos podem modificar o conteúdo da identidade ou substituí-la por um novo objeto que retornam. O método `wakeupIdentity()` pode até retornar `null`, o que desconecta o usuário.
Como exemplo, mostraremos a solução para a questão frequente de como atualizar os papéis na identidade logo após carregá-la da sessão. No método `wakeupIdentity()`, passaremos para a identidade os papéis atuais, por exemplo, do banco de dados:
```php
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// aqui é possível modificar a identidade antes de escrever no armazenamento após o login,
// mas não precisamos disso agora
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// atualização dos papéis na identidade
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
```
E agora voltaremos ao armazenamento baseado em cookies. Ele permite criar um site onde os usuários podem fazer login sem precisar de sessões. Ou seja, não precisa escrever no disco. Aliás, é assim que funciona o site que você está lendo agora, incluindo o fórum. Neste caso, a implementação de `IdentityHandler` é uma necessidade. No cookie, armazenaremos apenas um token aleatório representando o usuário logado.
Primeiro, na configuração, definiremos o armazenamento desejado usando `security › authentication › storage: cookie`.
No banco de dados, criaremos uma coluna `authtoken`, na qual cada usuário terá uma string [completamente aleatória, única e imprevisível|utils:random] de comprimento suficiente (pelo menos 13 caracteres). O armazenamento `CookieStorage` transmite no cookie apenas o valor `$identity->getId()`, então em `sleepIdentity()` substituiremos a identidade original por uma substituta com `authtoken` no ID, enquanto no método `wakeupIdentity()`, de acordo com o authtoken, leremos a identidade completa do banco de dados:
```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);
// verificamos a senha
...
// retornamos a identidade com todos os dados do banco de dados
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// retornamos a identidade substituta, onde no ID estará o authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// substituímos a identidade substituta pela identidade completa, como em authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
```
Múltiplos logins independentes
==============================
É possível ter vários usuários logados independentemente dentro de um único site e uma única sessão simultaneamente. Se, por exemplo, quisermos ter autenticação separada para a administração e a parte pública do site, basta definir um nome próprio para cada uma delas:
```php
$user->getStorage()->setNamespace('backend');
```
É importante lembrar de definir o namespace sempre em todos os lugares pertencentes à parte correspondente. Se usarmos presenters, definiremos o namespace no ancestral comum para a parte correspondente - geralmente BasePresenter. Faremos isso estendendo o método [checkRequirements() |api:Nette\Application\UI\Presenter::checkRequirements()]:
```php
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
```
Múltiplos autenticadores
------------------------
A divisão da aplicação em partes com login independente geralmente requer também autenticadores diferentes. No entanto, se registrássemos duas classes implementando Authenticator na configuração de serviços, o Nette não saberia qual delas atribuir automaticamente ao objeto `Nette\Security\User` e exibiria um erro. Portanto, precisamos restringir o [autowiring |dependency-injection:autowiring] para os autenticadores para que funcione apenas quando alguém solicitar uma classe específica, por exemplo, FrontAuthenticator, o que conseguimos escolhendo `autowired: self`:
```neon
services:
-
create: FrontAuthenticator
autowired: self
```
```php
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
```
Definiremos o autenticador do objeto User antes de chamar o método [login() |api:Nette\Security\User::login()], então geralmente no código do formulário que o loga:
```php
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};
```