Cómo hacer la autenticación de usuario con el componente de seguridad de Symfony
() translation by (you can also view the original English article)
En este artículo, aprenderá a configurar la autenticación de usuario en PHP utilizando el componente Symfony Security. Además de la autenticación, le mostraré cómo usar su autorización basada en roles, que puede ampliar de acuerdo con sus necesidades.
El componente de seguridad de Symfony
El componente de seguridad de Symfony le permite configurar funciones de seguridad como la autenticación, la autorización basada en roles, los tokens CSRF y mucho más fácilmente. De hecho, está dividido en cuatro subcomponentes, que puede elegir de acuerdo con sus necesidades.
El componente de seguridad tiene los siguientes subcomponentes:
- symfony/security-core
- symfony/security-http
- symfony/security-csrf
- symfony/security-acl
En este artículo, vamos a explorar la función de autenticación proporcionada por el componente symfony / security-core.
Como de costumbre, comenzaremos con las instrucciones de instalación y configuración, y luego exploraremos algunos ejemplos del mundo real para demostrar los conceptos clave.
Instalacion y configuracion
En esta sección, vamos a instalar el componente de seguridad de Symfony. Supongo que ya ha instalado Composer en su sistema; lo necesitaremos para instalar el componente de seguridad disponible en Packagist.
Así que adelante e instale el componente de Seguridad usando el siguiente comando.
1 |
$composer require symfony/security
|
Vamos a cargar usuarios de la base de datos MySQL en nuestro ejemplo, por lo que también necesitaremos una capa de abstracción de la base de datos. Instalemos una de las capas de abstracción de base de datos más populares: Doctrine DBAL.
1 |
$composer require doctrine/dbal
|
Eso debería haber creado el archivo composer.json, que debería verse así:
1 |
{
|
2 |
"require": { |
3 |
"symfony/security": "^4.1", |
4 |
"doctrine/dbal": "^2.7" |
5 |
}
|
6 |
}
|
Modifiquemos el archivo composer.json para que se vea como el siguiente.
1 |
{
|
2 |
"require": { |
3 |
"symfony/security": "^4.1", |
4 |
"doctrine/dbal": "^2.7" |
5 |
},
|
6 |
"autoload": { |
7 |
"psr-4": { |
8 |
"Sfauth\\": "src" |
9 |
},
|
10 |
"classmap": ["src"] |
11 |
}
|
12 |
}
|
Como
hemos agregado una nueva entrada de classmap
, continuemos y
actualicemos el autocargador del compositor ejecutando el siguiente
comando.
1 |
$composer dump -o |
Ahora puede usar el espacio de nombres de Sfauth
para autocargar clases en el directorio src.
Esa es la parte de la instalación, pero ¿cómo se supone que debes usarla? De hecho, solo se trata de incluir el archivo autoload.php creado por Composer en su aplicación, como se muestra en el siguiente fragmento.
1 |
<?php
|
2 |
require_once './vendor/autoload.php'; |
3 |
|
4 |
// application code
|
5 |
?>
|
Un ejemplo del mundo real
En primer lugar, veamos el flujo de autenticación habitual proporcionado por el componente Symfony Security.
- Lo primero es recuperar las credenciales del usuario y crear un token no autenticado.
- A continuación, pasaremos un token no autenticado al administrador de autenticación para su validación.
- El administrador de autenticación puede contener diferentes proveedores de autenticación, y uno de ellos se utilizará para autenticar la solicitud del usuario actual. La lógica de cómo se autentica el usuario se define en el proveedor de autenticación.
- El proveedor de autenticación contacta al proveedor del usuario para recuperar al usuario. Es responsabilidad del proveedor del usuario cargar usuarios desde el back-end respectivo.
- El proveedor de usuario intenta cargar al usuario utilizando las credenciales proporcionadas por el proveedor de autenticación. En la mayoría de los casos, el proveedor del usuario devuelve el objeto del usuario que implementa la interfaz
UserInterface
. - Si se encuentra el usuario, el proveedor de autenticación devuelve un token no autenticado y puede almacenar este token para las solicitudes posteriores.
En
nuestro ejemplo, vamos a hacer coincidir las credenciales del usuario
con la base de datos MySQL, por lo tanto, tendremos que crear el
proveedor del usuario de la base de datos. También crearemos el proveedor de autenticación de base de datos que maneja la lógica de autenticación. Y finalmente, crearemos la clase User, que implementa la interfaz UserInterface
.
La clase de usuario
En esta sección, crearemos la clase de usuario que representa la entidad de usuario en el proceso de autenticación.
Continúa y crea el archivo src / User / User.php con los siguientes contenidos.
1 |
<?php
|
2 |
namespace Sfauth\User; |
3 |
|
4 |
use Symfony\Component\Security\Core\User\UserInterface; |
5 |
|
6 |
class User implements UserInterface |
7 |
{
|
8 |
private $username; |
9 |
private $password; |
10 |
private $roles; |
11 |
|
12 |
public function __construct(string $username, string $password, string $roles) |
13 |
{
|
14 |
if (empty($username)) |
15 |
{
|
16 |
throw new \InvalidArgumentException('No username provided.'); |
17 |
}
|
18 |
|
19 |
$this->username = $username; |
20 |
$this->password = $password; |
21 |
$this->roles = $roles; |
22 |
}
|
23 |
|
24 |
public function getUsername() |
25 |
{
|
26 |
return $this->username; |
27 |
}
|
28 |
|
29 |
public function getPassword() |
30 |
{
|
31 |
return $this->password; |
32 |
}
|
33 |
|
34 |
public function getRoles() |
35 |
{
|
36 |
return explode(",", $this->roles); |
37 |
}
|
38 |
|
39 |
public function getSalt() |
40 |
{
|
41 |
return ''; |
42 |
}
|
43 |
|
44 |
public function eraseCredentials() {} |
45 |
}
|
Lo importante es que la clase User debe implementar la interfaz Symfony Security UserInterface
. Aparte de eso, no hay nada fuera de lo común aquí.
La clase de proveedor de base de datos
Es responsabilidad del proveedor del usuario cargar usuarios desde el back-end. En esta sección, crearemos el proveedor de usuario de la base de datos, que carga al usuario desde la base de datos MySQL.
Vamos a crear el archivo src / User / DatabaseUserProvider.php con los siguientes contenidos.
1 |
<?php
|
2 |
namespace Sfauth\User; |
3 |
|
4 |
use Symfony\Component\Security\Core\User\UserProviderInterface; |
5 |
use Symfony\Component\Security\Core\User\UserInterface; |
6 |
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; |
7 |
use Symfony\Component\Security\Core\Exception\UnsupportedUserException; |
8 |
use Doctrine\DBAL\Connection; |
9 |
use Sfauth\User\User; |
10 |
|
11 |
class DatabaseUserProvider implements UserProviderInterface |
12 |
{
|
13 |
private $connection; |
14 |
|
15 |
public function __construct(Connection $connection) |
16 |
{
|
17 |
$this->connection = $connection; |
18 |
}
|
19 |
|
20 |
public function loadUserByUsername($username) |
21 |
{
|
22 |
return $this->getUser($username); |
23 |
}
|
24 |
|
25 |
private function getUser($username) |
26 |
{
|
27 |
$sql = "SELECT * FROM sf_users WHERE username = :name"; |
28 |
$stmt = $this->connection->prepare($sql); |
29 |
$stmt->bindValue("name", $username); |
30 |
$stmt->execute(); |
31 |
$row = $stmt->fetch(); |
32 |
|
33 |
if (!$row['username']) |
34 |
{
|
35 |
$exception = new UsernameNotFoundException(sprintf('Username "%s" not found in the database.', $row['username'])); |
36 |
$exception->setUsername($username); |
37 |
throw $exception; |
38 |
}
|
39 |
else
|
40 |
{
|
41 |
return new User($row['username'], $row['password'], $row['roles']); |
42 |
}
|
43 |
}
|
44 |
|
45 |
public function refreshUser(UserInterface $user) |
46 |
{
|
47 |
if (!$user instanceof User) |
48 |
{
|
49 |
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); |
50 |
}
|
51 |
|
52 |
return $this->getUser($user->getUsername()); |
53 |
}
|
54 |
|
55 |
public function supportsClass($class) |
56 |
{
|
57 |
return 'Sfauth\User\User' === $class; |
58 |
}
|
59 |
}
|
El proveedor del usuario debe implementar la interfaz UserProviderInterface
. Estamos utilizando la doctrina DBAL para realizar las operaciones relacionadas con la base de datos. Como
hemos implementado la interfaz UserProviderInterface
, debemos
implementar los métodos loadUserByUsername
, refreshUser
y supportsClass
.
El método loadUserByUsername
debe cargar al usuario por el nombre de usuario, y eso se hace en el método getUser
. Si se encuentra el usuario, devolvemos el objeto Sfauth \ User \ User
correspondiente, que implementa la interfaz UserInterface
.
Por otro lado, el método refreshUser
actualiza el objeto User
suministrado obteniendo la última información de la base de datos.
Y
finalmente, el método supportsClass
comprueba si el proveedor
DatabaseUserProvider
es compatible con la clase de usuario suministrada.
La clase de proveedor de autenticación de base de datos
Finalmente, debemos implementar el proveedor de autenticación de usuario, que define la lógica de autenticación: cómo se autentica un usuario. En nuestro caso, tenemos que hacer coincidir las credenciales del usuario con la base de datos MySQL, y por lo tanto tenemos que definir la lógica de autenticación en consecuencia.
Continúa y crea el archivo src / User / DatabaseAuthenticationProvider.php con los siguientes contenidos.
1 |
<?php
|
2 |
namespace Sfauth\User; |
3 |
|
4 |
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider; |
5 |
use Symfony\Component\Security\Core\User\UserProviderInterface; |
6 |
use Symfony\Component\Security\Core\User\UserCheckerInterface; |
7 |
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; |
8 |
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; |
9 |
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; |
10 |
use Symfony\Component\Security\Core\User\UserInterface; |
11 |
use Symfony\Component\Security\Core\Exception\AuthenticationException; |
12 |
|
13 |
class DatabaseAuthenticationProvider extends UserAuthenticationProvider |
14 |
{
|
15 |
private $userProvider; |
16 |
|
17 |
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, bool $hideUserNotFoundExceptions = true) |
18 |
{
|
19 |
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); |
20 |
|
21 |
$this->userProvider = $userProvider; |
22 |
}
|
23 |
|
24 |
protected function retrieveUser($username, UsernamePasswordToken $token) |
25 |
{
|
26 |
$user = $token->getUser(); |
27 |
|
28 |
if ($user instanceof UserInterface) |
29 |
{
|
30 |
return $user; |
31 |
}
|
32 |
|
33 |
try { |
34 |
$user = $this->userProvider->loadUserByUsername($username); |
35 |
|
36 |
if (!$user instanceof UserInterface) |
37 |
{
|
38 |
throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); |
39 |
}
|
40 |
|
41 |
return $user; |
42 |
} catch (UsernameNotFoundException $e) { |
43 |
$e->setUsername($username); |
44 |
|
45 |
throw $e; |
46 |
} catch (\Exception $e) { |
47 |
$e = new AuthenticationServiceException($e->getMessage(), 0, $e); |
48 |
$e->setToken($token); |
49 |
throw $e; |
50 |
}
|
51 |
}
|
52 |
|
53 |
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) |
54 |
{
|
55 |
$currentUser = $token->getUser(); |
56 |
|
57 |
if ($currentUser instanceof UserInterface) |
58 |
{
|
59 |
if ($currentUser->getPassword() !== $user->getPassword()) |
60 |
{
|
61 |
throw new AuthenticationException('Credentials were changed from another session.'); |
62 |
}
|
63 |
}
|
64 |
else
|
65 |
{
|
66 |
$password = $token->getCredentials(); |
67 |
|
68 |
if (empty($password)) |
69 |
{
|
70 |
throw new AuthenticationException('Password can not be empty.'); |
71 |
}
|
72 |
|
73 |
if ($user->getPassword() != md5($password)) |
74 |
{
|
75 |
throw new AuthenticationException('Password is invalid.'); |
76 |
}
|
77 |
}
|
78 |
}
|
79 |
}
|
El proveedor de autenticación DatabaseAuthenticationProvider
amplía la clase abstracta UserAuthenticationProvider
. Por lo tanto, debemos implementar los métodos abstractos retrieveUser
y checkAuthentication
.
El trabajo del método retrieveUser
es cargar al usuario desde el proveedor de usuario correspondiente. En nuestro caso, utilizará el proveedor de usuario DatabaseUserProvider
para cargar al usuario desde la base de datos MySQL.
Por otro lado, el método checkAuthentication
realiza las comprobaciones necesarias para autenticar al usuario actual. Tenga en cuenta que he utilizado el método MD5 para el cifrado de contraseñas. Por supuesto, debe usar métodos de cifrado más seguros para almacenar las contraseñas de los usuarios.
Cómo funciona en conjunto
Hasta ahora, hemos creado todos los elementos necesarios para la autenticación. En esta sección, veremos cómo armar todo para configurar la funcionalidad de autenticación.
Continúe y cree el archivo db_auth.php y rellene con los siguientes contenidos.
1 |
<?php
|
2 |
require_once './vendor/autoload.php'; |
3 |
|
4 |
use Sfauth\User\DatabaseUserProvider; |
5 |
use Symfony\Component\Security\Core\User\UserChecker; |
6 |
use Sfauth\User\DatabaseAuthenticationProvider; |
7 |
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; |
8 |
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; |
9 |
use Symfony\Component\Security\Core\Exception\AuthenticationException; |
10 |
|
11 |
// init doctrine db connection
|
12 |
$doctrineConnection = \Doctrine\DBAL\DriverManager::getConnection( |
13 |
array('url' => 'mysql://{USERNAME}:{PASSWORD}@{HOSTNAME}/{DATABASE_NAME}'), new \Doctrine\DBAL\Configuration() |
14 |
);
|
15 |
|
16 |
// init our custom db user provider
|
17 |
$userProvider = new DatabaseUserProvider($doctrineConnection); |
18 |
|
19 |
// we'll use default UserChecker, it's used to check additional checks like account lock/expired etc.
|
20 |
// you can implement your own by implementing UserCheckerInterface interface
|
21 |
$userChecker = new UserChecker(); |
22 |
|
23 |
// init our custom db authentication provider
|
24 |
$dbProvider = new DatabaseAuthenticationProvider( |
25 |
$userProvider, |
26 |
$userChecker, |
27 |
'frontend'
|
28 |
);
|
29 |
|
30 |
// init authentication provider manager
|
31 |
$authenticationManager = new AuthenticationProviderManager(array($dbProvider)); |
32 |
|
33 |
try { |
34 |
// init un/pw, usually you'll get these from the $_POST variable, submitted by the end user
|
35 |
$username = 'admin'; |
36 |
$password = 'admin'; |
37 |
|
38 |
// get unauthenticated token
|
39 |
$unauthenticatedToken = new UsernamePasswordToken( |
40 |
$username, |
41 |
$password, |
42 |
'frontend'
|
43 |
);
|
44 |
|
45 |
// authenticate user & get authenticated token
|
46 |
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken); |
47 |
|
48 |
// we have got the authenticated token (user is logged in now), it can be stored in a session for later use
|
49 |
echo $authenticatedToken; |
50 |
echo "\n"; |
51 |
} catch (AuthenticationException $e) { |
52 |
echo $e->getMessage(); |
53 |
echo "\n"; |
54 |
}
|
Recuerde el flujo de autenticación que se discutió al principio de este artículo: el código anterior refleja esa secuencia.
Lo primero fue recuperar las credenciales del usuario y crear un token no autenticado.
1 |
$unauthenticatedToken = new UsernamePasswordToken( |
2 |
$username, |
3 |
$password, |
4 |
'frontend'
|
5 |
);
|
A continuación, pasamos ese token al administrador de autenticación para su validación.
1 |
// authenticate user & get authenticated token
|
2 |
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken); |
Cuando se llama al método de autenticación, ocurren muchas cosas detrás de escena.
En primer lugar, el administrador de autenticación selecciona un proveedor de autenticación apropiado. En nuestro caso, es el proveedor de autenticación DatabaseAuthenticationProvider
, que se seleccionará para la autenticación.
A continuación, recupera al usuario por el nombre de usuario del proveedor de usuario DatabaseUserProvider
. Finalmente, el método checkAuthentication
realiza las comprobaciones necesarias para autenticar la solicitud del usuario actual.
Si desea probar el script db_auth.php, deberá crear la tabla sf_users
en su base de datos MySQL.
1 |
CREATE TABLE `sf_users` ( |
2 |
`id` int(11) NOT NULL AUTO_INCREMENT, |
3 |
`username` varchar(255) NOT NULL, |
4 |
`password` varchar(255) NOT NULL, |
5 |
`roles` enum('registered','moderator','admin') DEFAULT NULL, |
6 |
PRIMARY KEY (`id`) |
7 |
) ENGINE=InnoDB; |
8 |
|
9 |
INSERT INTO `sf_users` VALUES (1,'admin','21232f297a57a5a743894a0e4a801fc3','admin'); |
Continúa y ejecuta el script db_auth.php para ver cómo funciona. Tras completar con éxito, debe recibir un token autenticado, como se muestra en el siguiente fragmento.
1 |
$php db_auth.php
|
2 |
UsernamePasswordToken(user="admin", authenticated=true, roles="admin") |
Una vez que el usuario está autenticado, puede almacenar el token autenticado en la sesión para las solicitudes posteriores.
¡Y con eso, hemos completado nuestra demostración de autenticación simple!
Conclusión
Hoy, miramos el componente Symfony Security, que le permite integrar funciones de seguridad en sus aplicaciones PHP. Específicamente, hablamos sobre la función de autenticación proporcionada por el subcomponente symfony / security-core, y le mostré un ejemplo de cómo esta funcionalidad puede implementarse en su propia aplicación.
¡Siéntete libre de publicar tus pensamientos usando el feed a continuación!