Curso Symfony 2: Seguridad de acceso:
Uno de los aspectos más importantes en el desarrollo de cualquier aplicación es la Seguridad de acceso, para ello Symfony 2 dispone de una moderna librería que se encarga de las validaciones de acceso y seguridad.
En este capítulo nos dispondremos a crear un ejemplo básico de seguridad que nos permita hacer el “login” de los usuarios y a su vez bloquear el acceso a determinados usuarios según su rol, para ello tendremos que adentrarnos en como funciona la librería de seguridad de Symfony2, luego crearemos las estructuras necesarias para definir un mini-backend donde crearemos unos CRUD’s para usuarios y roles utilizando Doctrine como proveedor de usuarios.
El primer paso es verificar si el usuario está o no autenticado, en tal caso lo deja pasar y el segundo paso es verificar si el usuario tiene el rol necesario para dicha acción, para comprenderlo mejor veamos un ejemplo básico del archivo de configuración:
Vemos como primer elemento definido los conjuntos de firewall y en él una “secured_area” donde:
“providers” simplemente define el proveedor de usuario, que en este caso es en memoria, y “encoders” define el codificador de la contraseña, el cual debe ser de algún tipo de HASH como SHA512, en el ejemplo se usa texto plano. (más adelante se exponen ejemplos detallados).
Protegiendo por IP
Tan simple como añadir el parámetro ip, con la misma puedes obligar a que una ruta solo se pueda acceder desde dicha ip (ideal para el backend interno):
Protegiendo por Canal
Si dispones de un certificado SSL puedes obligar a que la ruta solo esté disponible desde https, especificando requires_channel:
Protegiendo un Controlador
En ocasiones necesitamos que el controlador se encargue del control de acceso, de forma que podamos flexibilizarlo según nuestro modelo de negocios, para ello podremos acceder al contexto de seguridad desde nuestros controladores:
También puede optar por instalar y utilizar el Bundle opcional JMSSecurityExtraBundle, el cual puede asegurar su controlador usando anotaciones:
Para más información, consulte la documentación de JMSSecurityExtraBundle. Si estás usando la distribución estándar de Symfony, este paquete está disponible de forma predeterminada. Si no es así, lo puedes descargar e instalar.
Controlando el acceso desde plantilla
Incluso puedes verificar si el usuario tiene acceso desde la misma plantilla, útil para ocultar segmentos a roles específicos:
Desde Twig
Desde PHP
Recuperando del objeto usuario
Desde un controlador, puedes acceder fácilmente a la instancia del usuario actual utilizando el Mecanismo de inyección de dependencias:
Paso 1: Crea las entidades básicas
Antes de empezar debemos de definir las entidades básicas para ser utilizadas como proveedor de usuarios y roles en Sf2, dichas entidades User y Role deben de implementar las interfaces Symfony\Component\Security\Core\User\UserInterface y Symfony\Component\Security\Core\Role\RoleInterface respectivamente, así que añade estas 2 entidades a tu directorio “proyecto/src/MDW/BlogBundle/Entity”:
User.php:
Role.php:
Una vez creadas nuestras entidades, accedemos a la consola de Symfony2 y generamos las tablas en Base de Datos:
Paso 2: Generando los CRUD’s
Una vez creadas las entidades en DB, procedemos a crear los CRUD’s desde la consola de symfony:
Seguimos los pasos colocando MDWBlogBundle:Role, luego nos solicita si deseamos crear las opciones de escritura, le decimos “y” (sí), formato del CRUD: annotation, y finalmente en el Routes prefix colocamos /admin/role, este paso es importante porque a la ruta le asignamos el prefijo /admin para que nos permita empatar luego con el access_control, confirmamos y aparecerá el mensaje “You can now start using the generated code!”
Procedemos a aplicar lo mismo pero en este caso con MDWBlogBundle:User y en Routes prefix colocamos /admin/user
Ahora añadiremos las rutas a nuestro archivo de rutas (proyecto/src/MDW/BlogBundle/Resources/Config/routing.yml), porque al crearlas como Anotaciones las mismas no se añaden automáticamente:
De ésta forma añadiremos todas las rutas definidas por anotaciones del directorio Controller, ésta técnica forma parte del SensioFrameworkExtraBundle y nos permite definir las rutas directamente en nuestros controladores. si utilizas la Versión estándar de Symfony2 este Bundle viene por defecto.
Ya con esto podemos acceder a nuestros crud’s desde localhost/proyecto/web/app_dev.php/admin/user, pero aún debemos modificar ciertos aspectos en el controlador User para codificar el hash de contraseña.
Primero añadiremos la siguiente función en el controlador de usuarios:
Luego modificamos las funciones de las acciones correspondientes a create y update, añadiendo la llamada al la función anterior para establecer el hash de la contraseña con el algoritmo SHA512:
Por último sólo nos queda eliminar del formulario (src/MDW/BlogBundle/Form/UserType.php) el campo salt el cual no debe ser modificado por el usuario:
Ahora puedes proceder a registrar usuarios y roles, es muy importante que al menos crees los roles “ROLE_ADMIN” y “ROLE_USER” y dos usuarios (uno con un rol diferente) antes de que procedas en aplicar el esquema de seguridad, de lo contrario no tendrás usuario con que loguearte .
Paso 3: Creando el esquema de seguridad
Ahora procedemos a sobreescribir nuestro esquema de seguridad (proyecto/app/config/security.yml), recomiendo que antes de hacerlo guardes una copia del security.yml.
Como puedes apreciar en “encoders” se ha definido un codificador especifico para la entidad User, utilizando el algoritmo SHA512, además codificandolo en Base64 con 10 iteracciones, tal cual se apreció en la función setSecurePassword del controlador.
En “providers” se estableció nuestra entidad User de Doctrine, especificando el campo correspondiente al username, es cual es el mismo username en nuestra entidad.
En “firewalls” se ha añadido la nueva regla (o firewall) login desde la cual se aplica el parametro security: false lo que permite acceder a la misma sin autenticarse, de lo contrario el formulario de login nunca lo podremos visializar.
Además en “secured_area” se ha eliminado anonymous, se ha establecido “form_login” donde definimos la ruta para el login del sistema, y se definió una ruta personalizada para el “log_out“, donde en “target” podemos definir el path hacia donde redirigir cuando los usuarios cierren sesión.
Para culminar sólo necesitamos crear el controlador y vista para nuestro login, por lo que debes de crear el archivo SecurityController.php en el directorio (proyecto/src/MDW/BlogBundle/Controller):
SecurityController.php
Ahora crea el directorio “Security” dentro de (proyecto/src/MDW/BlogBundle/Resources/views) y procede a crear el archivo de vista:
login.html.twig
Y con ello ya puedes intentar acceder a localhost/proyecto/web/admin/user y probar el sistema de seguridad de Symfony2 (vaciar la caché en el caso de entrar al entorno de producción), si creaste previamente 2 usuarios, intenta acceder con el usuario que no tiene el rol “ROLE_ADMIN” y verás como te niega el acceso, en cambio si pruebas con un usuario con dicho rol, puedes entrar perfectamente.
Además interactuamos con dicho sistema a través de un “rápido” tutorial que nos permitió resolver las inquietudes más directas en cuanto a creación de un básico RBAC (Role-based Access Control), reitero que no es la única forma de hacerlo y que existen muchos Bundles Prefabricados como el FOSUserBundle que nos facilita enormemente ésta tarea, pero si no se conoce debidamente la base puede resultar una verdadera caja negra el usar un Bundle sin el previo conocimiento de como Symfony2 implementa tales mecanismos.
Síguenos en: @maestros | Fan page
Uno de los aspectos más importantes en el desarrollo de cualquier aplicación es la Seguridad de acceso, para ello Symfony 2 dispone de una moderna librería que se encarga de las validaciones de acceso y seguridad.
En este capítulo nos dispondremos a crear un ejemplo básico de seguridad que nos permita hacer el “login” de los usuarios y a su vez bloquear el acceso a determinados usuarios según su rol, para ello tendremos que adentrarnos en como funciona la librería de seguridad de Symfony2, luego crearemos las estructuras necesarias para definir un mini-backend donde crearemos unos CRUD’s para usuarios y roles utilizando Doctrine como proveedor de usuarios.
Autenticación vs. Autorización
Representan los 2 conceptos más fundamentales de seguridad en Symfony, el primero se encarga de verificar si el usuario en cuestión está Autenticado (logeado) y se le conoce como “Firewall”, el segundo verifica si el usuario tiene los permisos o “roles” necesarios y se le conoce como “access_control”.El primer paso es verificar si el usuario está o no autenticado, en tal caso lo deja pasar y el segundo paso es verificar si el usuario tiene el rol necesario para dicha acción, para comprenderlo mejor veamos un ejemplo básico del archivo de configuración:
# proyecto/app/config/security.yml security: firewalls: secured_area: pattern: ^/ anonymous: ~ http_basic: realm: "Secured Demo Area" access_control: - { path: ^/admin, roles: ROLE_ADMIN } providers: in_memory: users: usuario: { password: user, roles: 'ROLE_USER' } admin: { password: kitten, roles: 'ROLE_ADMIN' } encoders: Symfony\Component\Security\Core\User\User: plaintext
- pattern: es una expresión regular para hacer empatar la URL, toda ruta que empate con ello obligará al mecanismo de firewall que verifique si el usuario está autenticado, si no lo está procederá a re-dirigirlo al formulario de autenticación (en el caso anteterior mostrar el díalogo nativo de autenticación HTTP del navegador).
- anonymous: ~: indica que permite usuarios anónimos, no se debe aplicar en caso de backends.
- http_basic: indica que utilice la autenticación HTTP.
- - { path: ^/admin, roles: ROLE_ADMIN }indica una regla básica para autorización, donde en toda ruta que coincida al principio con /admin, el usuario debe de tener dicho rol “ROLE_ADMIN” indicado.
Nota
Puedes añadir tantos access_control como necesites.
Configuraciones del Control de Acceso
El control de acceso no sólo se limita a controlar que el usuario cumpla con un rol determinado para un patrón de ruta determinada, permite cierta flexibilidad con el que podrás adaptarte a las necesidades de seguridad de tu aplicación.Protegiendo por IP
Tan simple como añadir el parámetro ip, con la misma puedes obligar a que una ruta solo se pueda acceder desde dicha ip (ideal para el backend interno):
# app/config/security.yml security: # ... access_control: - { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
Si dispones de un certificado SSL puedes obligar a que la ruta solo esté disponible desde https, especificando requires_channel:
# app/config/security.yml security: # ... access_control: - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https}
En ocasiones necesitamos que el controlador se encargue del control de acceso, de forma que podamos flexibilizarlo según nuestro modelo de negocios, para ello podremos acceder al contexto de seguridad desde nuestros controladores:
//dentro de un controlador use Symfony\Component\Security\Core\Exception\AccessDeniedException; // ... public function helloAction($name) { if (false === $this->get(’security.context’)->isGranted(’ROLE_ADMIN’)) { throw new AccessDeniedException(); } // ... }
//dentro de un controlador use JMS\SecurityExtraBundle\Annotation\Secure; /** * @Secure(roles="ROLE_ADMIN") */ public function helloAction($name) { // ... }
Controlando el acceso desde plantilla
Incluso puedes verificar si el usuario tiene acceso desde la misma plantilla, útil para ocultar segmentos a roles específicos:
Desde Twig
{% if is_granted(’ROLE_ADMIN’) %} <a href="...">Delete</a> {% endif %}
<?php if ($view[’security’]->isGranted(’ROLE_ADMIN’)): ?> <a href="...">Delete</a> <?php endif; ?>
Desde un controlador, puedes acceder fácilmente a la instancia del usuario actual utilizando el Mecanismo de inyección de dependencias:
//dentro de un controlador public function indexAction() { $user = $this->get(’security.context’)->getToken()->getUser(); }
Tutorial: mini-backend de usuarios con Doctrine
Realmente el ejemplo anterior es demasiado básico como para llevarlo a una aplicación real, y una de las opciones más tentadoras es utilizar Doctrine como proveedor de los Usuarios, con el cual podamos crear Roles y Usuarios desde CRUD’s elaborados por el mismo framework y crear nuestro propio esquema de seguridad, debo resaltar que existen muchos Bundles prefabricados como el FOSUserBundle que facilitan enormemente ésta tarea, pero si quieres profundizar puedes seguir el siguiente tutorial para conocer a fondo como se hace desde 0 con Doctrine .Paso 1: Crea las entidades básicas
Antes de empezar debemos de definir las entidades básicas para ser utilizadas como proveedor de usuarios y roles en Sf2, dichas entidades User y Role deben de implementar las interfaces Symfony\Component\Security\Core\User\UserInterface y Symfony\Component\Security\Core\Role\RoleInterface respectivamente, así que añade estas 2 entidades a tu directorio “proyecto/src/MDW/BlogBundle/Entity”:
User.php:
<?php // proyecto/src/MDW/BlogBundle/Entity/User.php namespace MDW\BlogBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="admin_user") */ class User implements UserInterface { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string", length="255") */ protected $username; /** * @ORM\Column(name="password", type="string", length="255") */ protected $password; /** * @ORM\Column(name="salt", type="string", length="255") */ protected $salt; /** * se utilizó user_roles para no hacer conflicto al aplicar ->toArray en getRoles() * @ORM\ManyToMany(targetEntity="Role") * @ORM\JoinTable(name="user_role", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")} * ) */ protected $user_roles; public function __construct() { $this->user_roles = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set username * * @param string $username */ public function setUsername($username) { $this->username = $username; } /** * Get username * * @return string */ public function getUsername() { return $this->username; } /** * Set password * * @param string $password */ public function setPassword($password) { $this->password = $password; } /** * Get password * * @return string */ public function getPassword() { return $this->password; } /** * Set salt * * @param string $salt */ public function setSalt($salt) { $this->salt = $salt; } /** * Get salt * * @return string */ public function getSalt() { return $this->salt; } /** * Add user_roles * * @param Maycol\BlogBundle\Entity\Role $userRoles */ public function addRole(\Maycol\BlogBundle\Entity\Role $userRoles) { $this->user_roles[] = $userRoles; } public function setUserRoles($roles) { $this->user_roles = $roles; } /** * Get user_roles * * @return Doctrine\Common\Collections\Collection */ public function getUserRoles() { return $this->user_roles; } /** * Get roles * * @return Doctrine\Common\Collections\Collection */ public function getRoles() { return $this->user_roles->toArray(); //IMPORTANTE: el mecanismo de seguridad de Sf2 requiere ésto como un array } /** * Compares this user to another to determine if they are the same. * * @param UserInterface $user The user * @return boolean True if equal, false othwerwise. */ public function equals(UserInterface $user) { return md5($this->getUsername()) == md5($user->getUsername()); } /** * Erases the user credentials. */ public function eraseCredentials() { } }
<?php // proyecto/src/MDW/BlogBundle/Entity/Role.php namespace MDW\BlogBundle\Entity; use Symfony\Component\Security\Core\Role\RoleInterface; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="admin_roles") */ class Role implements RoleInterface { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(name="nombre", type="string", length="255") */ protected $name; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set name * * @param string $name */ public function setName($name) { $this->name = $name; } /** * Get name * * @return string */ public function getName() { return $this->name; } public function getRole() { return $this->getName(); } public function __toString() { return $this->getRole(); } }
$ app/console doctrine:schema:update --force
Una vez creadas las entidades en DB, procedemos a crear los CRUD’s desde la consola de symfony:
$ app/console doctrine:generate:crud
Procedemos a aplicar lo mismo pero en este caso con MDWBlogBundle:User y en Routes prefix colocamos /admin/user
Ahora añadiremos las rutas a nuestro archivo de rutas (proyecto/src/MDW/BlogBundle/Resources/Config/routing.yml), porque al crearlas como Anotaciones las mismas no se añaden automáticamente:
#proyecto/src/MDW/BlogBundle/Resources/Config/routing.yml # final del archivo: MDWAnnotations: resource: "@MDWBlogBundle/Controller/" prefix: / type: annotation
Ya con esto podemos acceder a nuestros crud’s desde localhost/proyecto/web/app_dev.php/admin/user, pero aún debemos modificar ciertos aspectos en el controlador User para codificar el hash de contraseña.
Primero añadiremos la siguiente función en el controlador de usuarios:
// proyecto/src/MDW/BlogBundle/Controller/UserController.php // añadimos esta función private function setSecurePassword(&$entity) { $entity->setSalt(md5(time())); $encoder = new \Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder('sha512', true, 10); $password = $encoder->encodePassword($entity->getPassword(), $entity->getSalt()); $entity->setPassword($password); }
// proyecto/src/MDW/BlogBundle/Controller/UserController.php //funcion createAction: public function createAction() { $entity = new User(); $request = $this->getRequest(); $form = $this->createForm(new UserType(), $entity); $form->bindRequest($request); if ($form->isValid()) { //establecemos la contraseña: -------------------------- $this->setSecurePassword($entity); $em = $this->getDoctrine()->getEntityManager(); $em->persist($entity); $em->flush(); return $this->redirect($this->generateUrl('admin_user_show', array('id' => $entity->getId()))); } return array( 'entity' => $entity, 'form' => $form->createView() ); } //... //funcion updateAction: public function updateAction($id) { $em = $this->getDoctrine()->getEntityManager(); $entity = $em->getRepository('MDWBlogBundle:User')->find($id); if (!$entity) { throw $this->createNotFoundException('Unable to find User entity.'); } $editForm = $this->createForm(new UserType(), $entity); $deleteForm = $this->createDeleteForm($id); $request = $this->getRequest(); //obtiene la contraseña actual ----------------------- $current_pass = $entity->getPassword(); $editForm->bindRequest($request); if ($editForm->isValid()) { //evalua si la contraseña fue modificada: ------------------------ if ($current_pass != $entity->getPassword()) { $this->setSecurePassword($entity); } $em->persist($entity); $em->flush(); return $this->redirect($this->generateUrl('admin_user_edit', array('id' => $id))); } return array( 'entity' => $entity, 'edit_form' => $editForm->createView(), 'delete_form' => $deleteForm->createView(), ); }
<?php // proyecto/src/MDW/BlogBundle/Form/UserType.php namespace MDW\BlogBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class UserType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder ->add('username') ->add('password') //->add('salt') //No necesitamos que salt sea mostrado --------------- ->add('user_roles') ; } public function getName() { return 'mdw_blogbundle_usertype'; } }
Paso 3: Creando el esquema de seguridad
Ahora procedemos a sobreescribir nuestro esquema de seguridad (proyecto/app/config/security.yml), recomiendo que antes de hacerlo guardes una copia del security.yml.
# proyecto/app/config/security.yml security: encoders: MDW\BlogBundle\Entity\User: algorithm: sha512 encode-as-base64: true iterations: 10 providers: user_db: entity: { class: MDW\BlogBundle\Entity\User, property: username } firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false login: pattern: ^/admin/login$ security: false secured_area: pattern: ^/admin/ # http_basic: # realm: "Introduzca Usuario y Contraseña" form_login: login_path: /admin/login check_path: /admin/login_check logout: path: /admin/logout target: / access_control: - { path: ^/admin, roles: ROLE_ADMIN }
En “providers” se estableció nuestra entidad User de Doctrine, especificando el campo correspondiente al username, es cual es el mismo username en nuestra entidad.
En “firewalls” se ha añadido la nueva regla (o firewall) login desde la cual se aplica el parametro security: false lo que permite acceder a la misma sin autenticarse, de lo contrario el formulario de login nunca lo podremos visializar.
Además en “secured_area” se ha eliminado anonymous, se ha establecido “form_login” donde definimos la ruta para el login del sistema, y se definió una ruta personalizada para el “log_out“, donde en “target” podemos definir el path hacia donde redirigir cuando los usuarios cierren sesión.
Para culminar sólo necesitamos crear el controlador y vista para nuestro login, por lo que debes de crear el archivo SecurityController.php en el directorio (proyecto/src/MDW/BlogBundle/Controller):
SecurityController.php
<?php // proyecto/src/MDW/BlogBundle/Controller/SecurityController.php namespace MDW\BlogBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Symfony\Component\Security\Core\SecurityContext; /** * Security controller. * * @Route("/admin") */ class SecurityController extends Controller { /** * Definimos las rutas para el login: * @Route("/login", name="login") * @Route("/login_check", name="login_check") */ public function loginAction() { $request = $this->getRequest(); $session = $request->getSession(); // obtiene el error de inicio de sesión si lo hay if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); } return $this->render('MDWBlogBundle:Security:login.html.twig', array( // el último nombre de usuario ingresado por el usuario 'last_username' => $session->get(SecurityContext::LAST_USERNAME), 'error' => $error, )); } } ?>
login.html.twig
{# proyecto/src/MDW/BlogBundle/Resources/views/Security/login.html.twig #} {% 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>
Resumen Final
En esta ocasión apreciamos el complejo sistema de seguridad de Symfony2, en donde un firewall verifica si el usuario está o no logueado, y un access control vigila que dicho usuario no pueda acceder a contenido del cual no se le ha dado acceso, también conocimos que se pueden definir providers diferentes para contener a nuestros usuarios y encoders para personalizar el HASH de la contraseña.Además interactuamos con dicho sistema a través de un “rápido” tutorial que nos permitió resolver las inquietudes más directas en cuanto a creación de un básico RBAC (Role-based Access Control), reitero que no es la única forma de hacerlo y que existen muchos Bundles Prefabricados como el FOSUserBundle que nos facilita enormemente ésta tarea, pero si no se conoce debidamente la base puede resultar una verdadera caja negra el usar un Bundle sin el previo conocimiento de como Symfony2 implementa tales mecanismos.
Síguenos en: @maestros | Fan page
Comentaris
Publica un comentari a l'entrada