Posted on March 12, 2015
Autentificación con MySQL Symfony 2 Parte 1
Después de luchar durante horas finalmente pude integrar el bundle de seguridad de Symfony para realizar el proceso de autentificación de usuarios, documenté todo para poder consultarlo si llegara a olvidar algo y también puede que sea útil para otros.
Primero montamos nuestro proyecto Symfony y además generamos un bundle, para este escenario creé un bundle llamado “LoginBundle”.
Damos de alta un catálogo de nombre “user” en la base de datos MySQL con los campos: id, username, password y isActive. Este almacenará todos los usuarios que serán autentificados desde el sistema.
SQL del catálogo user:
1 2 3 4 5 6 7 |
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `isActive` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=UTF-8; |
Ya que hayamos dado de alta el catálogo, procedemos a generar el XML donde se mapea la información de la tabla y después ese archivo lo utilizaremos para generar la entidad User en Symfony.
Generamos el XML de mapeo con el siguiente comando de Doctrine:
1 |
doctrine:mapping:import "LoginBundle" --filter="User" |
Le estamos diciendo que genere la entidad User del bundle LoginBundle, internamente Doctrine (ORM usado por Symfony) busca la tabla user en la base de datos y crea el XML con la información necesaria para generar dicha entidad.
Ahora con el siguiente comando generamos la entidad User con el XML recién generado localizado en: LoginBundle/Resources/config/doctrine/User.orm.xml
1 |
doctrine:generate:entities LoginBundle:User |
Con esto se crea la entidad User en la carpeta Entity de nuestro bundle.
Por defecto vamos a tener la entidad User con los atributos: id, username, password, isactive junto con sus getters y setters, se puede ver en LoginBundle/Entity/User.php.
Para activar la autentificación de usuarios desde la base de datos en Symfony la entidad usuario debe implementar 3 interfaces: UserInterface, Serializable y AdvancedUserInterface, así que abrimos el archivo de la entidad y procedemos a editarlo pero antes una explicación sobre las interfaces.
Entre las tres mencionadas UserInterface es la más importante ya que cuenta con los métodos: getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials().
Los métodos getPassword y getUsername no es necesario implementarlos ya que se crearon en la entidad User con el comando que vimos anteriormente, con respecto a los métodos restantes habrá que implementarlos.
Aunado a esto la interfaz serializable tiene dos métodos, serialize y unserialize, se implementan para que la entidad User sea serilizada en la sesión.
Por último la interfaz AdvancedUserInterface tiene los siguientes métodos: isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled().
En esta caso se utilizó esta interfaz para indicar cuales usuarios están inactivos o activos por medio del método isEnabled, que lo vamos a saber usando el atributo isactive de nuestra entidad User.
La entidad User quedaría de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
namespace Com\Jahepi\LoginBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\AdvancedUserInterface; class User implements UserInterface, AdvancedUserInterface, \Serializable { /** * @var integer */ private $id; /** * @var string */ private $username; /** * @var string */ private $password; /** * @var boolean */ private $isactive; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set username * * @param string $username * @return User */ public function setUsername($username) { $this->username = $username; return $this; } /** * Get username * * @return string */ public function getUsername() { return $this->username; } /** * Set password * * @param string $password * @return User */ public function setPassword($password) { $this->password = $password; return $this; } /** * Get password * * @return string */ public function getPassword() { return $this->password; } /** * Set isactive * * @param boolean $isactive * @return User */ public function setIsactive($isactive) { $this->isactive = $isactive; return $this; } /** * Get isactive * * @return boolean */ public function getIsactive() { return $this->isactive; } public function getRoles() { return array('ROLE_ADMIN'); } public function getSalt() { return null; } public function eraseCredentials() { } public function isAccountNonExpired() { return true; } public function isAccountNonLocked() { return true; } public function isCredentialsNonExpired() { return true; } public function isEnabled() { return $this->isactive; } public function serialize() { return serialize(array($this->id)); } public function unserialize($serialized) { list($this->id) = unserialize($serialized); } } |
Explicación de algunos métodos importantes:
1 2 3 |
public function getRoles() { return array('ROLE_ADMIN'); } |
El método getRoles() por el momento tenemos el código en duro, regresa un arreglo con la cadena ROLE_ADMIN, todos los usuarios van a tener ese rol asignado, lo mejor sería que esos roles también los obtuviera de un catálogo de la base de datos pero esto lo dejaré pendiente para otra entrada.
1 2 3 |
public function getSalt() { return null; } |
El método getSalt() regresa un valor nulo, este método es utilizado para codificar la contraseña un nivel más, lo que hace internamente es agregar el valor “salt” a la cadena del password, codificando estos dos elementos da como resultado que sea mucho más difícil descifrar cual es la contraseña si alguien llegara a tener acceso a estas, en este escenario no añaderimos ningún “salt” por eso regresamos valor nulo, pero es recomendable hoy en día agregar el salt a la contraseña por cuestiones de seguridad.
1 2 3 4 5 6 7 |
public function serialize() { return serialize(array($this->id)); } public function unserialize($serialized) { list($this->id) = unserialize($serialized); } |
Estos dos métodos son de la interfaz Serializable, el atributo id es el valor más importante que se serializa porque hay un método interno en Symfony llamado refreshUser() responsable de cargar el usuario en cada petición usando el id.
1 2 3 |
public function isEnabled() { return $this->isactive; } |
El método isEnabled() lo encontramos en la interfaz AdvancedUserInterface, este método es utilizado por Symfony para saber que usuarios están activos o inactivos, para ello sólo vamos a regresar el valor del atributo isactive, es un valor booleano que viene del catálogo user de nuestra base de datos.
Vamos a generar el controlador responsable de verificar el acceso del usuario sea correcto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
namespace Com\Jahepi\LoginBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Security; use Symfony\Component\HttpFoundation\Response; class SecurityController extends Controller { public function loginAction(Request $request) { $error = ''; $session = $request->getSession(); if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) { $error = $request->attributes->get(Security::AUTHENTICATION_ERROR); } if ($session->has(Security::AUTHENTICATION_ERROR)) { $error = $session->get(Security::AUTHENTICATION_ERROR); } return $this->render( 'LoginBundle:Security:login.html.twig', array( 'last_username' => $session->get(Security::LAST_USERNAME), 'error' => $error, ) ); } public function loggedAction() { $user = $this->getUser(); return new Response('Sesion iniciada del usuario: ' . $user->getUsername()); } } |
Lo siguiente es crear en LoginBundle/Resources/views la carpeta Security, y dentro la plantilla: login.html.twig
El código de esa plantilla es:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{# empty Twig template #} <html> <head></head> <body> {% if error %} <div>{{ error.message }}</div> {% endif %} <form action="{{ path('login_check') }}" method="POST"><label for="username">Username:</label> <input id="username" type="text" name="_username" value="{{ last_username }}" /> <label for="password">Password:</label> <input id="password" type="password" name="_password" /> <input type="submit" name="login" /> </form> </body> </html> |
Los campos de texto de la contraseña y el usuario deben de tener como nombre _username y _ password, esto es debido a que internamente Symfony agarra esos nombres por defecto al hacer la validación, se pueden cambiar si asi se requiere desde la configuración de seguridad, la ruta login_check que se llama dentro de la función path esta declarada en el archivo de configuración de las rutas que veremos enseguida.
El archivos de configuración de la rutas se encuentra en LoginBundle/Resources/config/routing.yml, definiremos las siguientes rutas:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
login: path: /admin/login defaults: { _controller: LoginBundle:Security:login } login_check: path: /admin/login_check exit: path: /admin/exit logged: path: /admin defaults: { _controller: LoginBundle:Security:logged } |
El siguiente paso es habilitar en la configuración de seguridad la validación de la entidad User que esta vinculada a la tabla “user” de nuestra base de datos para restringir los accesos al sistema, vamos abrir el archivo app/config/security.yml, pero antes crea una copia de este archivo para su respaldo.
El contenido del archivo security.yml es:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# you can read more about security in the related section of the documentation # http://symfony.com/doc/current/book/security.html security: # http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password encoders: Com\Jahepi\LoginBundle\Entity\User: algorithm: sha1 encode_as_base64: false iterations: 1 # http://symfony.com/doc/current/book/security.html#hierarchical-roles role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers providers: user_db: entity: { class: LoginBundle:User, property: username } # the main part of the security, where you can set up firewalls # for specific sections of your app firewalls: # disables authentication for assets and the profiler, adapt it according to your needs dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # the login page has to be accessible for everybody login: pattern: ^/admin/login$ security: false # secures part of the application secured_area: pattern: ^/admin # it's important to notice that in this case _demo_security_check and _demo_login # are route names and that they are specified in the AcmeDemoBundle form_login: check_path: /admin/login_check login_path: /admin/login default_target_path: /admin logout: path: /admin/exit target: /admin/login # anonymous: ~ # http_basic: true # realm: "Secured Demo Area" # with these settings you can restrict or allow access for different parts # of your application based on roles, ip, host or methods # http://symfony.com/doc/current/cookbook/security/access_control.html access_control: - { path: ^/admin, roles: ROLE_ADMIN } |
Esta parte es la que me causó más quebraderos de cabeza, considero muy importante comprender cada una de estas partes para que todo funcione sin problemas, así que pasaré a explicar cada una de las secciones del archivo de configuración.
Sección Codificadores:
1 2 3 4 5 |
encoders: Com\Jahepi\LoginBundle\Entity\User: algorithm: sha1 encode_as_base64: false iterations: 1 |
En esta sección definimos el codificador que se va a utilizar para que la contraseña de nuestro usuario sea cifrada, en la línea 2 se especifica la ruta (namespace) de la entidad User que será utilizada para este proceso, en la línea 3 asignamos el algoritmo de codificación, en este ejemplo es sha1 pero puedes utilizar otros si lo requieres, en la línea 4 es una bandera que indica si la contraseña también además de utilizar el algoritmo sha1 se utilizará la codificación base 64, lo deje en falso para este ejemplo y por último en la línea 5 es el número de iteraciones que será codificada la contraseña de usuario, sólo haremos el cifrado sha1 una sola vez, pero puedes indicar cualquier número de iteraciones para ofuscar más la contraseña.
Sección Jerarquía de Roles:
1 2 3 |
role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] |
En la jerarquía de roles se pueden especificar qué otros roles entran o son válidos en determinado rol, es como una relación padre e hijo, estas restricciones de que partes del sitio son accedidas dependiendo del rol se declaran en la sección access_control que veremos más adelante.
En el ejemplo anterior vemos que el rol “ROLE_ADMIN” también tiene asignado el rol “ROLE_USER”, esto quiere decir que si hay ciertas rutas restrigidas donde sólo usuarios con el rol “ROLE_USER” pueden entrar, significa que también podran acceder los usuarios que tenga el rol “ROLE_ADMIN”.
Sección de Proveedores:
1 2 3 |
providers: user_db: entity: { class: LoginBundle:User, property: username } |
Le sección del proveedor (providers), declara el proveedor user_db, un proveedor de usuarios es la fuente de dónde se cargan los usuarios en el proceso de autentificación, estos pueden venir de un texto plano, xml, memoria, base de datos, etc. la fuente puede ser cualquiera, en este ejemplo la fuente es nuestra entidad vinculada al catálogo de nuestra base de datos. Dentro de la configuración en el atributo entity se especifica la entidad User que generamos anteriormente y la propiedad username es el criterio de búsqueda, esto quiere decir que Symfony buscará la entidad en la base de datos por su atributo username.
Seccion Firewall:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
firewalls: # disables authentication for assets and the profiler, adapt it according to your needs dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # the login page has to be accessible for everybody login: pattern: ^/admin/login$ security: false # secures part of the application secured_area: pattern: ^/admin # it's important to notice that in this case _demo_security_check and _demo_login # are route names and that they are specified in the AcmeDemoBundle form_login: check_path: /admin/login_check login_path: /admin/login default_target_path: /admin logout: path: /admin/exit target: /admin/login |
En el firewall el apartado login, tiene dos atributos pattern y security, en el pattern especificamos con una expresión regular que rutas serán descartadas en el proceso de autentificación, vemos que tiene el valor ^/admin/login$, así la ruta /admin/login será omitida del proceso de autentificación ya que el parámetro security tiene valor falso para que salte el proceso de validación. (esta ruta es la que muestra el formulario de login).
Ahora iremos al apartado de secure_area donde pattern es otra expresión regular (^/admin) que dice que todas las rutas que empiecen con /admin estarán protegidas, no podrán ser accedidas hasta que el usuario se autentifique.
Vamos a la parte de form_login, donde check_path apunta a la ruta que definimos en el achivo de configuración y tiene como valor /admin/login_check, esta ruta dispara un proceso interno de Symfony responsable de la validación del usuario y ver si la autentificación se ha hecho corréctamente, aquí no es necesario implementar nada más que definir la ruta en el archivo de configuración de rutas que vimos anteriormente.
El login_path tiene como valor /admin/login, es la ruta que apunta a nuestro controlador que pinta la vista del formulario de login y verifica que el usuario se haya autentificado correctamente, es el controlador SecurityController que creamos anteriormente donde se ejecuta el método loginAction del controlador SecurityController.
El atributo default_target_path, es la ruta donde nos redireccionará la autentificación en caso de ser exitosa, en este ejemplo nos redirecciona a la ruta /admin, esta ruta llama al método loggedAction del controlador SecurityController.
Por último vamos al apartado de logout, el atributo path es la ruta que hace el proceso de salir de la sesión, esto también es interno de Symfony no se tiene que implementar nada más que definir la ruta en la configuración al igual que el parámetro check_path anterior.
Para finalizar el atributo target solo es la ruta a donde nos redirigirá Symfony cuando el usuario sale de la sesión.
Más info: http://symfony.com/doc/current/reference/configuration/security.html
Sección Control de Acceso:
1 2 |
access_control: - { path: ^/admin, roles: ROLE_ADMIN } |
En esta parte definimos la rutas y que roles tendrán acceso a esas rutas, el path es una expresión regular de la ruta y el atributo roles, qué roles tienen acceso a esta ruta.
Con esto finalizamos la primera parte de la autentificación, deberíamos tener la funcionalidad básica para que los usuarios puedan iniciar sesión.
En la siguiente entrada vamos a ver la parte de roles para que no estén en código duro y los tome de una catálogo de la base de datos.