lunes, 9 de junio de 2014

Implementando mi Módulo de Seguridad en SF2 Parte 3

Es momento de implementar el login en el sistema para que el usuario tenga los permisos necesarios. Lo primero es modificar el security.yml para definir el proveedor de los usuarios (providers), el firewall (firewall) y proteger las rutas (access_control).


Por lo que el archivo queda de la siguiente manera.
security:
    encoders:
        Xanadu\SeguridadBundle\Entity\Usuarios:
            algorithm: sha1
            encode_as_base64: false
            iterations: 1

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        administrators:
            entity: { class: XanaduSeguridadBundle:Usuarios }

    firewalls:
        area_segura:
            pattern:    ^/
            form_login:
                login_path:  login
                check_path:  login_check
                default_target_path: seguridad_usuarios
            anonymous:
            logout:
                target: login
            #anonymous: ~
            #http_basic:
            #    realm: "Secured Demo Area"

    access_control:
        - { path: ^/admin, roles: ROLE_USER }
        - { path: ^/seguridad, roles: ROLE_USER }
A partir de ahora cuando se quiera entrar a una URL que necesite un permiso, el sistema redireccionará a la pantalla de login para proporcionar las credenciales. El siguiente paso es modificar el archivo de enrutamiento del bundle XanaduSeguridadBundle para agregar las rutas login, login_check, logout.
#Archivo de enrutamiento

logout:
    path: /logout

login:
    path:   /login
    defaults:  { _controller: XanaduSeguridadBundle:Seguridad:login }

login_check:
    path:   /login_check

...
Para las rutas logout y login_check no se necesita hacer algo adicional, pues Symfony2 las gestiona de forma automática. Para la ruta login se necesita crear el controlador SeguridadController y agregar la acción que mostrará el formulario para ingresar las credenciales.
<?php

namespace Xanadu\SeguridadBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class SeguridadController extends Controller
{
    public function indexAction($name)
    {
        return $this->render('XanaduSeguridadBundle:Default:index.html.twig', array('name' => $name));
    }

    public function loginAction() {
        $request = $this->getRequest();
        $session = $request->getSession();

        // get the login error if there is one
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(
                SecurityContext::AUTHENTICATION_ERROR
            );
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render(
            'XanaduSeguridadBundle:Seguridad:login.html.twig',
            array(
                // last username entered by the user
                'last_username' => $session->get(SecurityContext::LAST_USERNAME),
                'error'         => $error,
            )
        );
    }
}
El siguiente paso es crear la plantilla login.html.twig con el siguiente contenido.
{% extends "::base.html.twig" %}
{% block body %}
    <h4>INICIO DE SESION</h4>
    {% if error %}
        <div>{{ error.message }}</div>
    {% endif %}

    <form action="{{ path('login_check') }}" method="post">
        <label for="username">Usuario:</label>
        <input type="text" id="username" ayuda="Proporcione nombre de usuario o email" name="_username" value="{{ last_username }}" />

        <label for="password">Contraseña:</label>
        <input type="password" id="password" name="_password" />

        {#
            If you want to control the URL the user is redirected to on success (more details below)
            <input type="hidden" name="_target_path" value="/account" />
        #}

        <button type="submit">login</button>
    </form>


{% endblock %}
Con esto ya se encuentra listo nuestro formulario para ingresar al sistema, el siguiente que el repositorio UsuariosRepository sirva como proveedor de usuarios, para esto es necesario que implemente la interfaz UserProviderInterfaz y definir sus métodos necesarios, por lo que el repositorio quedaría de la siguiente manera.
<?php

namespace Xanadu\SeguridadBundle\Repository;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\EntityRepository;

/**
* UsuariosHasPermisosRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class UsuariosRepository extends EntityRepository implements UserProviderInterface
{
    //Inicio de funciones que se implementan de la Interfaz

    public function loadUserByUsername($username)
    {
        $q = $this
            ->createQueryBuilder('u')
            ->where('u.nombreUsuario = :username OR u.email = :email or u.id = :id')
            ->setParameter('username', $username)
            ->setParameter('email', $username)
            ->setParameter('id', $username)
            ->getQuery();

        try {
            // The Query::getSingleResult() method throws an exception
            // if there is no record matching the criteria.
            $user = $q->getSingleResult();
        } catch (NoResultException $e) {
            $message = sprintf(
                'Unable to find an active admin AcmeUserBundle:User object identified by "%s".',
                $username
            );
            throw new UsernameNotFoundException($message, 0, $e);
        }

        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if (!$this->supportsClass($class)) {
            throw new UnsupportedUserException(
                sprintf(
                    'Instances of "%s" are not supported.',
                    $class
                )
            );
        }

        return $this->loadUserByUsername($user->getId());
    }

    public function supportsClass($class)
    {
        return $this->getEntityName() === $class
            || is_subclass_of($class, $this->getEntityName());
    }

    ...
}
Como último paso se necesita modificar la función getUsername de la entidad Usuarios para que devuelva el nombre de usuario, así como la función getRoles para que de momento decuelva el rol ROLE_USER.
<?php

    ...
    public function getRoles() {
        return array("ROLE_USER");
    }

    public function getUsername() {
        return $this->nombreUsuario;
    }
Ahora si se puede iniciar sesión en el sistema para acceder a las URL protegidas. Para consultar el código ejecutar la instrucción git checkout seguridad1.5.

Obtener roles desde la base de datos

El objetivo de este tutorial es cargar los roles desde la base de datos, para lograr esto se necesita que la entidad Permisos se modifique para que extienda de la interfaz RoleInterface e implementar el método getRole.
<?php

namespace Xanadu\SeguridadBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Role\RoleInterface;

/**
* Permisos
*
* @ORM\Table()
* @ORM\Entity
*/
class Permisos implements RoleInterface
{
    ...
    public function getRole() {
      return $this->nombre;
    }
    ...
}
El siguiente paso consiste en modificar la función getRoles de la entidad Usuarios.
<? php

class Usuarios implements AdvancedUserInterface, \Serializable
{
    ...
    public function getRoles() {
        $rolUser = new Permisos();
        $rolUser->setNombre("ROLE_USER");
        $roles = array($rolUser);
        foreach($this->grupos as $group)
        {
            $roles = array_merge($roles, $group->getPermisos()->toArray());
        }

        $roles = array_merge($roles, $this->permisos->toArray());
        $roles = array_unique($roles);

        return $roles;

    }
    ...
}
Hasta aqui los roles ya deberian cargarse desde la base de datos, el codigo puede consultarse con la instrucción git checkout seguridad1.6.

Optimizando la consulta para recuperar el usuario

Ya se puede hacer login al sistema, sin embargo si observamos en la barra de depuración, es necesario realizar 5 consultas a la bd para recuperar el usuario, para optimizar esto, es necesario modificar el método loadUserByUsername de Usuariosrepository.
class UsuariosRepository extends EntityRepository implements UserProviderInterface
{
    //Inicio de funciones que se implementan de la Interfaz

    public function loadUserByUsername($username)
    {
        $q = $this
            ->createQueryBuilder('u')
            ->select('u', 'g', 'p', 'p1', 'p2')
            ->where('u.nombreUsuario = :valor OR u.email = :valor or u.id = :valor')
            ->setParameter('valor', $username)
            ->leftJoin('u.grupos', 'g')
            ->leftJoin('g.permisos', 'p')
            ->leftJoin('u.perfil', 'p1')
            ->leftJoin('u.permisos', 'p2')
            ->getQuery();

        ...
    }
Con esto hemos concluido el tutorial para crear nuestro bundle de administración de usuarios, quedan pendientes las acciones de recuperar contraseña, modificar contraseña entre otros. Las cuales se agregaran más adelante.
Para consultar el código ejecuta la instrucción git checkout seguridad1.7.

2 comentarios:

  1. Buena práctica hacerlo paso a paso y sin embargo es más conveniente usar el FOSUserBundle y seguir las instrucciones para la personalización de lo que necesitemos para el proyecto.

    ResponderBorrar
  2. Compañero, me da un error en la entidad Usuarios, [Symfony\Component\Debug\Exception\FatalErrorException]
    Compile Error: Cannot redeclare AppBundle\Entity\Usuarios::getRoles()
    Dice que hay error al declarar esa funcion, podrias colaborarme gracias.

    ResponderBorrar