Autenticación de usuarios
Las aplicaciones web poco o nada necesitan ningún mecanismo para el login de los usuarios o para comprobar sus privilegios. En este capítulo hablaremos de:
- login y logout de usuario
- autenticadores y autorizadores personalizados
En los ejemplos utilizaremos un objeto de la clase Nette\Security\User, que representa al usuario actual y que
se obtiene pasándolo mediante inyección de dependencia. En los
presentadores basta con llamar a $user = $this->getUser()
.
Autenticación
Autenticación significa inicio de sesión del usuario, es decir, el proceso durante el cual se verifica la identidad de
un usuario. El usuario suele identificarse mediante un nombre de usuario y una contraseña. La verificación la realiza el llamado
autenticador. Si el login falla, se lanza
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
Así se cierra la sesión del usuario:
$user->logout();
Y comprobar si el usuario está conectado:
echo $user->isLoggedIn() ? 'yes' : 'no';
Sencillo, ¿verdad? Y todos los aspectos de seguridad son manejados por Nette para usted.
En el presentador, puede verificar el inicio de sesión en el método startup()
y redirigir a un usuario que no
haya iniciado sesión a la página de inicio de sesión.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Caducidad
El inicio de sesión del usuario expira junto con la expiración del repositorio,
que suele ser una sesión (véase el ajuste de expiración de sesión ). Sin embargo,
también se puede establecer un intervalo de tiempo más corto tras el cual se cierra la sesión del usuario. El método
setExpiration()
, que se llama antes de login()
, se utiliza para este propósito. Proporcione una cadena
con una hora relativa como parámetro:
// el login expira tras 30 minutos de inactividad
$user->setExpiration('30 minutes');
// cancelar la expiración establecida
$user->setExpiration(null);
El método $user->getLogoutReason()
indica si se ha cerrado la sesión del usuario porque ha expirado el
intervalo de tiempo. Devuelve la constante Nette\Security\UserStorage::LogoutInactivity
si el tiempo expiró o
UserStorage::LogoutManual
cuando se llamó al método logout()
.
Autenticador
Es un objeto que verifica los datos de acceso, es decir, normalmente el nombre y la contraseña. La implementación trivial es la clase Nette\Security\SimpleAuthenticator, que puede definirse en configuración:
security:
users:
# name: password
johndoe: secret123
kathy: evenmoresecretpassword
Esta solución es más adecuada para realizar pruebas. Le mostraremos cómo crear un autenticador que verificará las credenciales contra una tabla de base de datos.
Un autenticador es un objeto que implementa la interfaz Nette\Security\Authenticator con el método
authenticate()
. Su tarea es devolver la llamada identidad o lanzar una excepción
Nette\Security\AuthenticationException
. También sería posible proporcionar un código de error de grano fino
Authenticator::IdentityNotFound
o 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, // o array de roles
['name' => $row->username],
);
}
}
La clase MyAuthenticator se comunica con la base de datos a través de Nette
Database Explorer y trabaja con la tabla users
, donde la columna username
contiene el nombre de
usuario y la columna password
contiene el hash. Tras verificar el nombre y la
contraseña, devuelve la identidad con el ID del usuario, el rol (columna role
de la tabla), que mencionaremos más adelante, y un array con datos adicionales (en nuestro caso, el nombre de usuario).
Añadiremos el autenticador a la configuración como un servicio del contenedor DI:
services:
- MyAuthenticator
Eventos $onLoggedIn, $onLoggedOut
El objeto Nette\Security\User
tiene los eventos
$onLoggedIn
y $onLoggedOut
, por lo que puedes añadir callbacks que se activen después de un login
exitoso o después de que el usuario se desconecte.
$user->onLoggedIn[] = function () {
// el usuario acaba de iniciar sesión
};
Identidad
Una identidad es un conjunto de información sobre un usuario que devuelve el autenticador y que luego se almacena en una
sesión y se recupera utilizando $user->getIdentity()
. Así podemos obtener el id, roles y otros datos del usuario
tal y como los pasamos en el autenticador:
$user->getIdentity()->getId();
// también funciona el atajo $user->getId();
$user->getIdentity()->getRoles();
// se puede acceder a los datos del usuario como propiedades
// el nombre que pasamos en MyAuthenticator
$user->getIdentity()->name;
Es importante destacar que cuando el usuario cierra la sesión utilizando $user->logout()
, la identidad no
se borra y sigue estando disponible. Por lo tanto, si la identidad existe, por sí misma no garantiza que el usuario también
haya iniciado sesión. Si queremos borrar explícitamente la identidad, cerramos la sesión del usuario mediante
logout(true)
.
Gracias a esto, todavía se puede suponer qué usuario está en el ordenador y, por ejemplo, mostrar ofertas personalizadas en la tienda electrónica, sin embargo, sólo se pueden mostrar sus datos personales después de iniciar sesión.
Identity es un objeto que implementa la interfaz Nette\Security\IIdentity, la implementación por defecto es Nette\Security\SimpleIdentity. Y como se ha mencionado, la identidad se almacena en la sesión, por lo que si, por ejemplo, cambiamos el rol de alguno de los usuarios logueados, los datos antiguos se mantendrán en la identidad hasta que vuelva a loguearse.
Almacenamiento para el usuario conectado
Los dos datos básicos sobre el usuario, es decir, si ha iniciado sesión y su identidad, se suelen
guardar en la sesión. La cual puede ser modificada. Para almacenar esta información es responsable un objeto que implemente la
interfaz Nette\Security\UserStorage
. Existen dos implementaciones estándar, la primera transmite los datos en una
sesión y la segunda en una cookie. Estas son las clases Nette\Bridges\SecurityHttp\SessionStorage
y
CookieStorage
. Usted puede elegir el almacenamiento y configurarlo muy convenientemente en la configuración de seguridad › autenticación.
También puedes controlar exactamente cómo se realizará el guardado (sleep) y el restablecimiento (wakeup) de
la identidad. Todo lo que necesitas es que el autenticador implemente la interfaz Nette\Security\IdentityHandler
.
Este tiene dos métodos: sleepIdentity()
es llamado antes de que la identidad sea escrita en el almacenamiento, y
wakeupIdentity()
es llamado después de que la identidad sea leída. Los métodos pueden modificar el contenido de la
identidad, o sustituirla por un nuevo objeto devuelto. El método wakeupIdentity()
puede incluso devolver
null
, que cierra la sesión del usuario.
Como ejemplo, mostraremos una solución a una pregunta común sobre cómo actualizar los roles de identidad justo después de
restaurar desde una sesión. En el método wakeupIdentity()
pasamos los roles actuales a la identidad, por ejemplo
desde la base de datos:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// aquí puedes cambiar la identidad antes de almacenar después de iniciar sesión,
// pero no lo necesitamos ahora
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// actualización de funciones en la identidad
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
Y ahora volvemos al almacenamiento basado en cookies. Permite crear un sitio web en el que los usuarios pueden iniciar sesión
sin necesidad de utilizar sesiones. Por lo tanto, no necesita escribir en el disco. Después de todo, así es como funciona el
sitio web que estás leyendo ahora, incluido el foro. En este caso, la implementación de IdentityHandler
es una
necesidad. Sólo almacenaremos en la cookie un token aleatorio que representa al usuario logueado.
Así que primero establecemos el almacenamiento deseado en la configuración usando
security › authentication › storage: cookie
.
Añadiremos una columna authtoken
en la base de datos, en la que cada usuario tendrá una cadena completamente aleatoria, única e indescifrable de longitud suficiente (al menos
13 caracteres). El repositorio CookieStorage
almacena sólo el valor $identity->getId()
en la
cookie, así que en sleepIdentity()
reemplazamos la identidad original con un proxy con authtoken
en el
ID, por el contrario en el método wakeupIdentity()
restauramos la identidad completa desde la base de datos según
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);
// comprobar contraseña
...
// devolvemos la identidad con todos los datos de la base de datos
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// devolvemos una identidad proxy, donde en el ID es authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// sustituir la identidad proxy por una identidad completa, como en authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Autenticaciones Múltiples Independientes
Es posible tener varios usuarios registrados independientes dentro de un mismo sitio y una sesión a la vez. Por ejemplo, si queremos tener una autenticación independiente para el frontend y el backend, simplemente estableceremos un espacio de nombres de sesión único para cada uno de ellos:
$user->getStorage()->setNamespace('backend');
Es necesario tener en cuenta que esto debe establecerse en todos los sitios que pertenezcan al mismo segmento. Cuando utilicemos presentadores, estableceremos el espacio de nombres en el ancestro común – normalmente el BasePresenter. Para ello extenderemos el método checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Autenticadores múltiples
Dividir una aplicación en segmentos con autenticación independiente generalmente requiere diferentes autenticadores. Sin
embargo, registrar dos clases que implementan Authenticator en config services provocaría un error porque Nette no sabría cuál
de ellas debería autocablearse al objeto Nette\Security\User
. Por
eso debemos limitar el autocableado para ellos con autowired: self
de forma que se active sólo cuando se solicite
específicamente su clase:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Sólo necesitamos establecer nuestro autenticador al objeto User antes de llamar al método login() lo que típicamente significa en el callback del formulario de login:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};