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

Instalação e requisitos

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);
	// ...
};
versão: 4.0