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

Instalação e requisitos

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