Autenticação de usuários
Poucas ou nenhumas aplicações web não precisam de nenhum mecanismo para login de usuário ou verificação de privilégios de usuário. Neste capítulo, vamos falar sobre isso:
- login e logout do usuário
- autenticadores e autorizadores personalizados
Nos exemplos, usaremos um objeto da classe Nette\Security\User, que representa o usuário atual e
que você obtém ao passá-lo usando a injeção de
dependência. Nos apresentadores, basta ligar para $user = $this->getUser()
.
Autenticação
Autenticação significa ** login de usuário**, ou seja, o processo durante o qual a identidade de um usuário é
verificada. O usuário normalmente se identifica usando nome de usuário e senha. A verificação é realizada pelo chamado autenticador. Se o login falhar, ele lança
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
Esta é a forma de efetuar o logout do usuário:
$user->logout();
E verificar se o usuário está logado:
echo $user->isLoggedIn() ? 'yes' : 'no';
Simples, certo? E todos os aspectos de segurança são tratados pela Nette para você.
No apresentador, você pode verificar o login no método startup()
e redirecionar um usuário sem login para a
página de login.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Validade
O login do usuário expira junto com a expiração do repositório, que geralmente
é uma sessão (veja a configuração de expiração da
sessão ). No entanto, também é possível definir um intervalo de tempo mais curto após o qual o usuário é
desconectado. O método setExpiration()
, que é chamado antes de login()
, é usado para este fim.
Fornecer uma seqüência com um tempo relativo como parâmetro:
// o login expira após 30 minutos de inatividade
$user->setExpiration('30 minutes');
// cancelar a expiração do conjunto
$user->setExpiration(null);
O método $user->getLogoutReason()
informa se o usuário foi desconectado porque o intervalo de tempo
expirou. Ele retorna ou a constante Nette\Security\UserStorage::LogoutInactivity
se o tempo expirou ou
UserStorage::LogoutManual
quando o método logout()
foi chamado.
Autenticador
É um objeto que verifica os dados de login, ou seja, geralmente o nome e a senha. A implementação trivial é a classe Nette\Security\SimpleAuthenticator, que pode ser definida na configuração:
security:
users:
# name: password
johndoe: secret123
kathy: evenmoresecretpassword
Esta solução é mais adequada para fins de teste. Mostraremos a você como criar um autenticador que verificará as credenciais em relação a uma tabela de banco de dados.
Um autenticador é um objeto que implementa a interface Nette\Security\Authenticator com o método
authenticate()
. Sua tarefa é devolver a chamada identidade ou lançar uma exceção
Nette\Security\AuthenticationException
. Também seria possível fornecer um código de erro de grão fino
Authenticator::IdentityNotFound
ou 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 matriz de papéis
['name' => $row->username],
);
}
}
A classe MyAuthenticator se comunica com o banco de dados através do Nette Database Explorer e trabalha com a tabela users
, onde a
coluna username
contém o nome de login do usuário e a coluna password
contém hash. Após verificar o nome e a senha, ele retorna a identidade com o ID
do usuário, função (coluna role
na tabela), que mencionaremos mais tarde, e um array com
dados adicionais (no nosso caso, o nome do usuário).
Acrescentaremos o autenticador à configuração como um serviço do recipiente DI:
services:
- MyAuthenticator
Eventos $onLoggedIn, $onLoggedOut
O objeto Nette\Security\User
tem eventos
$onLoggedIn
e $onLoggedOut
, assim você pode adicionar callbacks que são acionados depois de um login
bem sucedido ou depois que o usuário sai do sistema.
$user->onLoggedIn[] = function () {
// o usuário acabou de fazer o login
};
Identidade
Uma identidade é um conjunto de informações sobre um usuário que é devolvido pelo autenticador e que é então armazenado
em uma sessão e recuperado usando $user->getIdentity()
. Assim, podemos obter a identificação, funções e
outros dados do usuário à medida que os passamos no autenticador:
$user->getIdentity()->getId();
// também funciona como atalho $user->getId();
$user->getIdentity()->getRoles();
// os dados do usuário podem ser acessados como propriedades
// o nome que passamos no MyAuthenticator
$user->getIdentity()->name;
É importante ressaltar que quando o usuário faz logout usando $user->logout()
, identidade não é
apagada e ainda está disponível. Portanto, se a identidade existe, ela por si só não garante que o usuário também
esteja logado. Se quisermos excluir explicitamente a identidade, o usuário sai do sistema pelo endereço
logout(true)
.
Graças a isto, você ainda pode assumir qual usuário está no computador e, por exemplo, exibir ofertas personalizadas na loja virtual, no entanto, você só pode exibir seus dados pessoais após fazer o login.
A identidade é um objeto que implementa a interface Nette\Security\IIdentity, a implementação padrão é Nette\Security\SimpleIdentity. E como mencionado, a identidade é armazenada na sessão, portanto, se, por exemplo, mudarmos o papel de alguns dos usuários logados, os dados antigos serão mantidos na identidade até que ele se logue novamente.
Armazenamento para usuários logados
As duas informações básicas sobre o usuário, ou seja, se eles estão logados e sua identidade,
são geralmente carregadas na sessão. O que pode ser alterado. Para armazenar estas informações é responsável um objeto
implementando a interface Nette\Security\UserStorage
. Há duas implementações padrão, a primeira transmite dados
em uma sessão e a segunda em um cookie. Estas são as classes Nette\Bridges\SecurityHttp\SessionStorage
e
CookieStorage
. Você pode escolher o armazenamento e configurá-lo de forma muito conveniente na configuração de autenticação de segurança.
Você também pode controlar exatamente como será feita a economia de identidade (sleep) e o restabelecimento
(despertar). Tudo o que você precisa é que o autenticador implemente a interface
Nette\Security\IdentityHandler
. Isto tem dois métodos: sleepIdentity()
é chamada antes que a
identidade seja escrita para armazenamento, e wakeupIdentity()
é chamada após a leitura da identidade. Os métodos
podem modificar o conteúdo da identidade, ou substituí-la por um novo objeto que retorne. O método
wakeupIdentity()
pode até mesmo retornar null
, que registra o usuário fora.
Como exemplo, mostraremos uma solução para uma questão comum sobre como atualizar papéis de identidade logo após
o restabelecimento de uma sessão. No método wakeupIdentity()
, passamos os papéis atuais para a identidade, por
exemplo, a partir do banco de dados:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// aqui você pode mudar a identidade antes de armazenar após o login,
// mas não precisamos disso agora
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// atualizando papéis na identidade
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
E agora voltamos ao armazenamento baseado em cookies. Ele permite criar um website onde os usuários podem fazer o login sem a
necessidade de usar sessões. Portanto, não é necessário escrever em disco. Afinal, é assim que o website que você está
lendo agora funciona, incluindo o fórum. Neste caso, a implementação do IdentityHandler
é uma necessidade.
Armazenaremos apenas um token aleatório representando o usuário logado no cookie.
Portanto, primeiro definimos o armazenamento desejado na configuração usando
security › authentication › storage: cookie
.
Acrescentaremos uma coluna authtoken
no banco de dados, na qual cada usuário terá uma seqüência completamente aleatória, única e indiscutível, de comprimento suficiente (pelo
menos 13 caracteres). O repositório CookieStorage
armazena apenas o valor $identity->getId()
no
cookie, assim, em sleepIdentity()
substituímos a identidade original por um proxy por authtoken
no ID,
ao contrário, no método wakeupIdentity()
restauramos toda a identidade do banco de dados de acordo com
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);
// verificar senha
...
// devolvemos a identidade com todos os dados do banco de dados
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// devolvemos uma identidade proxy, onde no ID é authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// substituir a identidade proxy por uma 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;
}
}
Autenticações Independentes Múltiplas
É possível ter vários usuários independentes logados dentro de um site e uma sessão de cada vez. Por exemplo, se quisermos ter uma autenticação separada para o frontend e backend, apenas definiremos um espaço de nome de sessão único para cada um deles:
$user->getStorage()->setNamespace('backend');
É necessário ter em mente que isto deve ser estabelecido em todos os lugares pertencentes ao mesmo segmento. Ao utilizar os apresentadores, colocaremos o namespace no ancestral comum – geralmente o BasePresenter. Para isso, estenderemos o método checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Autenticadores Múltiplos
Dividir uma aplicação em segmentos com autenticação independente geralmente requer autenticadores diferentes. Entretanto,
o registro de duas classes que implementam o Authenticator em serviços de configuração acionaria um erro porque a Nette não
saberia qual delas deveria estar ligada automaticamente ao
objeto Nette\Security\User
. É por isso que devemos limitar a auto-cablagem para eles com
autowired: self
para que ela seja ativada somente quando sua classe for especificamente solicitada:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Só precisamos definir nosso autenticador para o objeto Usuário antes de chamar o método de login() que normalmente significa no formulário de retorno de chamada:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};