La autenticación siempre es el primer paso en la seguridad, ya que se trata del proceso de verificar quién es el usuario. En Symfony, la autenticación se puede hacer de varias maneras: a través de un formulario de acceso, con la autenticación básica de HTTP, e incluso a través de Twitter o Facebook.
Una vez que el usuario se ha autenticado, comienza la autorización, que proporciona un mecanismo estándar y muy potente para decidir si un usuario puede acceder a algún recurso (una URL, un objeto, una llamada a un método, ...). Su funcionamiento se basa en asignar roles específicos a cada usuario y después hacer que las diferentes partes de la aplicación requieran de diferentes roles para poder acceder.
El proceso de autorización tiene por tanto dos componentes principales:
- El usuario dispones de uno o más roles.
- Un recurso requiere de un rol específico para poder acceder a él..
En esta sección, nos centraremos en cómo utilizar diferentes roles para proteger el acceso a los recursos. Más adelante, aprenderás más sobre cómo crear y asignar roles a los usuarios.
13.4.1. Protegiendo URL específicas mediante patrones
La forma más sencilla de proteger parte de tu aplicación consiste en utilizar expresiones regulares para proteger URL específicas. En el primer ejemplo de este capítulo ya se configuró la autorización para que cualquier URL que coincida con la expresión regular ^/admin
requiera el rol ROLE_ADMIN
.
Advertencia Entender cómo trabaja access_control
exactamente es muy importante para garantizar que tu aplicación esté completamente asegurada. Más adelante se explica su funcionamiento con detalle.
Puedes definir tantos patrones de URL como necesites, cada uno de ellos con una expresión regular:
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
- { path: ^/admin, roles: ROLE_ADMIN }
<!-- app/config/security.xml -->
<config>
<!-- ... -->
<rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
<rule path="^/admin" role="ROLE_ADMIN" />
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
// ...
'access_control' => array(
array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
),
));
Truco Al prefijar la ruta con ^
te aseguras que sólo coinciden las URL que comienzan con ese patrón. Si utilizaras por ejemplo la ruta /admin
(sin el ^
por delante) se aplicaría tanto a rutas del tipo /admin/foo
como a rutas de este otro tipo: /foo/admin
.
13.4.2. Entendiendo cómo trabaja access_control
Symfony2 comprueba para cada petición entrante si existe algún access_control
cuya expresión regular coincida con la URL de la petición. En cuanto encuentra un access_control
que coincide, detiene la búsqueda, ya que siempre se utiliza el primer access_control
coincidente (así que el orden en el que configures los diferentes access_control
es muy importante).
Cada access_control
tiene varias opciones de configuración que se encargan de dos tareas:
- Comprobar si la petición coincide con los datos del
access_control
- En caso de que coincida, qué tipo de control de acceso debe realizarse.
Para comprobar si la petición coincide con el access_control
, Symfony2 crea una instancia de la clase RequestMatcher
por cada uno de los access_control
. Esta clase determina si este access_control
debe aplicarse a la petición. Para realizar esta comprobación, se utilizan las siguientes opciones:
path
ip
host
methods
Considera las siguientes entradas de access_control
como ejemplo:
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
- { path: ^/admin, roles: ROLE_USER_HOST, host: symfony.com }
- { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
- { path: ^/admin, roles: ROLE_USER }
<access-control>
<rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
<rule path="^/admin" role="ROLE_USER_HOST" host="symfony.com" />
<rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" />
<rule path="^/admin" role="ROLE_USER" />
</access-control>
'access_control' => array(
array('path' => '^/admin', 'role' => 'ROLE_USER_IP', 'ip' => '127.0.0.1'),
array('path' => '^/admin', 'role' => 'ROLE_USER_HOST', 'host' => 'symfony.com'),
array('path' => '^/admin', 'role' => 'ROLE_USER_METHOD', 'method' => 'POST, PUT'),
array('path' => '^/admin', 'role' => 'ROLE_USER'),
),
Para cada petición entrante, Symfony decide qué access_control
utilizar basándose en la URI solicitada, la dirección IP del cliente, el nombre del servidor o host, y el método de la petición. Si para una entrada no se especifican las opciones ip
, host
o method
, ese access_control
se aplicará a cualquier dirección IP, host y método. Por último, recuerda que el orden es muy importante porque Symfony siempre utiliza la primera coincidencia:
Ejemplo #1:
- Datos de la petición entrante:
- URI:
/admin/user
- IP:
127.0.0.1
- Host:
example.com
- Method:
GET
- URI:
access_control
utilizado por Symfony:- Regla #1 (
ROLE_USER_IP
)
- Regla #1 (
- ¿Por qué?:
- Porque la URI coincide con
path
y la IP conip
.
- Porque la URI coincide con
Ejemplo #2:
- Datos de la petición entrante:
- URI:
/admin/user
- IP:
127.0.0.1
- Host:
symfony.com
- Method:
GET
- URI:
access_control
utilizado por Symfony:- Regla #1 (
ROLE_USER_IP
)
- Regla #1 (
- ¿Por qué?:
- Porque también coinciden los valores de
path
eip
. También se produce una coincidencia con la entrada del rolROLE_USER_HOST
, pero Symfony siempre utiliza la primera regla que coincida.
- Porque también coinciden los valores de
Ejemplo #3:
- Datos de la petición entrante:
- URI:
/admin/user
- IP:
168.0.0.1
- Host:
symfony.com
- Method:
GET
- URI:
access_control
utilizado por Symfony:- Regla #2 (
ROLE_USER_HOST
)
- Regla #2 (
- ¿Por qué?:
- La dirección IP no concuerda con la de la primera regla, por tanto se utiliza la segunda regla (cuya configuración sí que coincide con la petición).
Ejemplo #4:
- Datos de la petición entrante:
- URI:
/admin/user
- IP:
168.0.0.1
- Host:
symfony.com
- Method:
POST
- URI:
access_control
utilizado por Symfony:- Regla #2 (
ROLE_USER_HOST
)
- Regla #2 (
- ¿Por qué?:
- Los datos de la segunda regla siguen siendo válidos para esta petición. La tercera regla también se cumple, pero Symfony siempre utiliza dirección IP no concuerda con la de la primera regla, por tanto se utiliza la primera regla que coincida.
Ejemplo #5:
- Datos de la petición entrante:
- URI:
/admin/user
- IP:
168.0.0.1
- Host:
example.com
- Method:
POST
- URI:
access_control
utilizado por Symfony:- Regla #3 (
ROLE_USER_METHOD
)
- Regla #3 (
- ¿Por qué?:
- Los datos de la petición no coinciden ni con la primera ni con la segunda regla. La tercera regla sí que coincide y por eso es la que se utiliza.
Ejemplo #6:
- Datos de la petición entrante:
- URI:
/admin/user
- IP:
168.0.0.1
- Host:
example.com
- Method:
GET
- URI:
access_control
utilizado por Symfony:- Regla #4 (
ROLE_USER
)
- Regla #4 (
- ¿Por qué?:
- Los valores de
ip
,host
ymethod
impiden que alguna de las tres primeras reglas coincida.
- Los valores de
Ejemplo #7:
- Datos de la petición entrante:
- URI:
/foo
- IP:
127.0.0.1
- Host:
symfony.com
- Method:
POST
- URI:
access_control
utilizado por Symfony:- Ninguna.
- ¿Por qué?:
- La URI solicitada no coincide con la expresión regular de ninguna regla.
Una vez que Symfony2 ha decidido que entrada access_control
concuerda con la petición (si es que hay alguna), entonces aplica las restricciones de acceso basándose en las opciones roles
y requires_channel
:
role
: si el usuario no tiene del rol indicado, se le deniega el acceso (internamente se lanza la excepciónAccessDeniedException
).requires_channel
: si el canal de la petición (por ejemplohttp
) no coincide con el valor de esta opción (por ejemplohttps
), el usuario será redirigido (en este caso se le redirige dehttp
ahttps
).
Truco Si se deniega el acceso al usuario, el sistema de seguridad intentará autenticar al usuario si aún no lo ha hecho (redirigiéndole por ejemplo a la página del formulario de login). Si el usuario ya había iniciado sesión, se le muestra la página del error 403
(Access denied
).
13.4.3. Protegiendo por IP
En algunas situaciones puede ser útil restringir el acceso a una determinada ruta basándose en la IP desde la que se origina la petición. Esto es especialmente importante si utilizas ESI en tu aplicación (que es algo que está relacionado con la caché HTTP y se explicará más adelante en otro capítulo).
Cuándo ESI está habilitado, es recomendable proteger el acceso a las URL de ESI. De hecho, algunos fragmentos ESI pueden contener información tan sensible como todos los datos del usuario actualmente conectado en la aplicación. Para impedir cualquier posibilidad de acceder a estos fragmentos, la ruta ESI debe asegurarse de forma que solamente sea visible desde algún servidor privado.
El siguiente ejemplo muestra cómo proteger todas las rutas ESI que empiezan con un determinado prefijo (/esi
) para que no se accedan desde el exterior:
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/esi, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
- { path: ^/esi, roles: ROLE_NO_ACCESS }
<access-control>
<rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY" ip="127.0.0.1" />
<rule path="^/esi" role="ROLE_NO_ACCESS" />
</access-control>
'access_control' => array(
array('path' => '^/esi', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'ip' => '127.0.0.1'),
array('path' => '^/esi', 'role' => 'ROLE_NO_ACCESS'),
),
Ahora, cuando alguien trate de acceder por ejemplo a /esi/algo
desde la IP 10.0.0.1
:
- La primera regla del control de acceso se ignora porque
path
concuerda pero laip
no. - La segunda regla del control de acceso se activa porque la URL de la petición coincide con la expresión regular de su opción
path
. El usuario verá denegado su acceso porque la ruta requiere que tenga el rolROLE_NO_ACCESS
, pero este rol no existe en la aplicación (en eso consiste este truco, en exigir un rol que no existe).
Ahora, si la misma petición proviene de la IP 127.0.0.1
:
- Se activa la primera regla del control de acceso porque tanto
path
comoip
concuerdan: se permite el acceso al usuario porque todos los usuarios disponen del rolIS_AUTHENTICATED_ANONYMOUSLY
(Symfony lo aplica automáticamente a todos los usuarios). - La segunda regla de acceso ni siquiera se tiene en cuenta porque la primera regla ha producido una coincidencia.
13.4.4. Protegiendo por canal
También puedes requerir que un usuario acceda a una URL vía SSL. Para ello, añade la opción requires_channel
en cualquier entrada access_control
:
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
<access-control>
<rule path="^/cart/checkout" role="IS_AUTHENTICATED_ANONYMOUSLY" requires_channel="https" />
</access-control>
'access_control' => array(
array('path' => '^/cart/checkout', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https'),
),
13.4.5. Protegiendo un controlador
Proteger la aplicación en función de los patrones de las URL es fácil, pero puede no ser lo suficientemente granular. Si lo necesitas, también puedes restringir el acceso a un único controlador:
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
public function helloAction($name)
{
if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
// ...
}
También puedes optar por instalar y utilizar el bundle JMSSecurityExtraBundle
, con el que puedes asegurar tu controlador usando anotaciones:
// ...
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Secure(roles="ROLE_ADMIN")
*/
public function helloAction($name)
{
// ...
}
Para más información, consulta la documentación de JMSSecurityExtraBundle. Si estás usando la distribución estándar de Symfony, este bundle ya está instalado y configurado.
13.4.6. Protegiendo otros servicios
En Symfony puedes proteger cualquier otro elemento utilizando una estrategia similar a la explicada en la sección anterior. Supongamos por ejemplo que tienes un servicio (es decir, una clase PHP), cuyo trabajo consiste en enviar mensajes de correo electrónico. Si lo deseas puedes restringir el uso de esta clase a aquellos usuarios que tengan un rol específico.
Para más información sobre cómo utilizar el componente de seguridad para proteger diferentes servicios y métodos en tu aplicación, consulta el artículo How to secure any Service or Method in your Application.
13.4.7. Listas de control de acceso (ACL)
Imagina que has desarrollado un sistema de blog donde los usuarios pueden comentar tus artículos. Además, quieres que los usuarios puedan modificar sus propios comentarios pero no los de otros usuarios. Por último, quieres que el usuario admin
pueda modificar cualquier comentario.
El componente de seguridad incluye un sistema de listas de control de acceso (ACL) que puedes utilizar cuando necesites controlar el acceso a las instancias de un objeto. Sin ACL puedes proteger tu sistema para que sólo determinados usuarios puedan editar los comentarios del blog en general. Pero con ACL puedes restringir o permitir el acceso comentario por comentario.
Para más información, consulta el artículo How to use Access Control Lists (ACLs).