Login de usuários (Autenticação)
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
Nos exemplos, usaremos o objeto da classe 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. 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.
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:
$user->logout();
E para verificar se ele está logado:
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.
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, que
geralmente é a sessão (veja a configuração de expiração da
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:
// 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 Nette\Security\SimpleAuthenticator, que podemos definir na configuração:
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 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
.
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 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. 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, e um array com outros dados (no nosso caso, o nome de
usuário).
Adicionaremos ainda o autenticador à configuração como um serviço do contêiner DI:
services:
- MyAuthenticator
Eventos $onLoggedIn, $onLoggedOut
O objeto Nette\Security\User
tem 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.
$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:
$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 Nette\Security\IIdentity, a implementação padrão é 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.
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:
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 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:
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:
$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():
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 para os autenticadores para que funcione apenas
quando alguém solicitar uma classe específica, por exemplo, FrontAuthenticator, o que conseguimos escolhendo
autowired: self
:
services:
-
create: FrontAuthenticator
autowired: self
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(), então geralmente no código do formulário que o loga:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};