En la guía anterior vimos cómo implementar un sistema básico de login y logout con sesiones, cookies, PDO y contraseñas seguras. Ahora vamos a dar el siguiente paso: controlar qué puede hacer cada usuario.
Para eso necesitamos distinguir dos conceptos:
- autenticación: comprobar quién es el usuario;
- autorización: decidir qué acciones puede realizar.
Autenticación vs autorización
Cuando un usuario inicia sesión correctamente, queda autenticado.
Pero eso no significa que pueda acceder a cualquier parte del sistema.
Por ejemplo:
- un usuario común puede ver su perfil;
- un administrador puede gestionar usuarios;
- un moderador puede editar contenidos;
- un invitado no puede acceder a páginas privadas.
Agregar un rol a la tabla de usuarios
Una forma simple de comenzar es agregar una columna rol.
ALTER TABLE usuarios ADD rol VARCHAR(50) NOT NULL DEFAULT 'usuario';
Por ejemplo, podemos tener:
usuario;admin;moderador.
Guardar el rol en la sesión
Después de validar el login, podemos guardar el rol del usuario:
$_SESSION['usuario_id'] = $usuario['id']; $_SESSION['usuario_nombre'] = $usuario['nombre']; $_SESSION['usuario_rol'] = $usuario['rol'];
Proteger una página para usuarios autenticados
<?php session_start(); if (!isset($_SESSION['usuario_id'])) { header('Location: login.php'); exit; } ?>
Proteger una página solo para administradores
<?php session_start(); if ( !isset($_SESSION['usuario_id']) || $_SESSION['usuario_rol'] !== 'admin' ) { http_response_code(403); exit('Acceso denegado'); } ?>
No alcanza con ocultar botones
Un error muy común es esconder botones en la interfaz y pensar que eso protege la acción.
Por ejemplo:
<?php if ($_SESSION['usuario_rol'] === 'admin'): ?> <a href="eliminar_usuario.php?id=15"> Eliminar usuario </a> <?php endif; ?>
Esto mejora la interfaz, pero no reemplaza el control en el servidor.
La página eliminar_usuario.php también debe verificar que el usuario tenga permisos.
Centralizar la verificación
En lugar de repetir las mismas condiciones en todas las páginas, podemos crear funciones.
<?php function usuarioAutenticado(): bool { return isset($_SESSION['usuario_id']); } function esAdmin(): bool { return isset($_SESSION['usuario_rol']) && $_SESSION['usuario_rol'] === 'admin'; } ?>
Crear funciones para exigir permisos
function requireLogin(): void { if (!isset($_SESSION['usuario_id'])) { header('Location: login.php'); exit; } } function requireAdmin(): void { requireLogin(); if ($_SESSION['usuario_rol'] !== 'admin') { http_response_code(403); exit('No tenés permisos para acceder a esta sección'); } }
Después podemos utilizar:
<?php session_start(); require 'auth.php'; requireAdmin(); ?>
Roles en una aplicación real
Para proyectos pequeños, guardar el rol directamente en la tabla usuarios puede ser suficiente.
Sin embargo, cuando el sistema crece suele ser mejor separar la información.
Por ejemplo:
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(50) NOT NULL UNIQUE
);
ALTER TABLE usuarios ADD rol_id INT, ADD FOREIGN KEY (rol_id) REFERENCES roles(id);
¿Qué son los permisos?
Un rol es una agrupación de capacidades.
Por ejemplo, el rol admin podría tener estos permisos:
- ver usuarios;
- crear usuarios;
- editar usuarios;
- eliminar usuarios.
Mientras que el rol usuario podría tener solamente:
- ver su perfil;
- editar su perfil.
Modelo con roles y permisos
Una estructura más flexible podría utilizar:
usuarios;roles;permisos;rol_permiso.
Esto permite asignar muchos permisos a cada rol.
Tabla de permisos
CREATE TABLE permisos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL UNIQUE
);
Tabla intermedia rol_permiso
CREATE TABLE rol_permiso ( rol_id INT NOT NULL, permiso_id INT NOT NULL, PRIMARY KEY (rol_id, permiso_id), FOREIGN KEY (rol_id) REFERENCES roles(id), FOREIGN KEY (permiso_id) REFERENCES permisos(id) );
Consultar los permisos de un usuario
SELECT p.nombre FROM usuarios u INNER JOIN roles r ON u.rol_id = r.id INNER JOIN rol_permiso rp ON r.id = rp.rol_id INNER JOIN permisos p ON rp.permiso_id = p.id WHERE u.id = :usuario_id;
Función tienePermiso()
Una implementación simple podría ser:
function tienePermiso(PDO $pdo, int $usuarioId, string $permiso): bool { $sql = 'SELECT COUNT(*) FROM usuarios u INNER JOIN roles r ON u.rol_id = r.id INNER JOIN rol_permiso rp ON r.id = rp.rol_id INNER JOIN permisos p ON rp.permiso_id = p.id WHERE u.id = :usuario_id AND p.nombre = :permiso'; $stmt = $pdo->prepare($sql); $stmt->execute([ 'usuario_id' => $usuarioId, 'permiso' => $permiso ]); return (bool) $stmt->fetchColumn(); }
Usar permisos para proteger acciones
if (!tienePermiso( $pdo, $_SESSION['usuario_id'], 'usuarios.eliminar' )) { http_response_code(403); exit('Acceso denegado'); }
Controlar acciones, no solo páginas
Los permisos deben aplicarse también sobre operaciones específicas.
Por ejemplo, aunque un usuario pueda acceder a una lista, eso no significa necesariamente que pueda:
- crear registros;
- editarlos;
- eliminarlos;
- exportarlos.
HTTP 401 y 403
Son dos códigos que suelen confundirse.
- 401 Unauthorized: normalmente indica que falta autenticación válida.
- 403 Forbidden: el usuario está identificado, pero no tiene permiso para realizar la acción.
No confiar en datos enviados por el cliente
Nunca debemos decidir permisos basándonos en un campo enviado desde un formulario.
Esto sería inseguro:
<input type="hidden" name="rol" value="admin">
El usuario puede modificar fácilmente ese valor.
El rol y los permisos deben obtenerse desde una fuente confiable, como la sesión y la base de datos.
Actualizar permisos durante una sesión
Si guardamos el rol únicamente en $_SESSION y luego un administrador cambia ese rol en la base de datos, la sesión podría conservar temporalmente el valor anterior.
En sistemas donde los permisos cambian con frecuencia puede ser conveniente consultar los permisos desde la base de datos o implementar una estrategia para actualizar la sesión.
Ejemplo de estructura de archivos
proyecto/ │ ├── config/ │ └── conexion.php │ ├── auth/ │ └── auth.php │ ├── admin/ │ ├── index.php │ └── usuarios.php │ ├── usuario/ │ └── perfil.php │ ├── login.php ├── logout.php └── index.php
Esto ayuda a separar responsabilidades y mantener el proyecto ordenado.
Errores comunes
- confundir autenticación con autorización;
- proteger solamente los botones y no las acciones del servidor;
- repetir controles de permisos por todo el proyecto;
- confiar en roles enviados mediante formularios o parámetros;
- dar permisos excesivos a usuarios comunes;
- guardar demasiada información sensible dentro de la sesión;
- no devolver una respuesta adecuada cuando falta autorización.
Ejercicio práctico
Extendé el sistema de login de la guía anterior para tener dos roles:
admin;usuario.
Después implementá:
- una página accesible por cualquier usuario autenticado;
- una página disponible solamente para administradores;
- un menú que muestre opciones diferentes según el rol;
- protección en el servidor para las operaciones administrativas;
- una respuesta HTTP 403 cuando un usuario no tenga permisos.
¿Qué sigue?
En la próxima guía podemos avanzar con un tema fundamental para cualquier aplicación web: seguridad en PHP.
Veremos:
- inyección SQL;
- XSS;
- CSRF;
- validación y sanitización;
- protección de formularios;
- manejo seguro de contraseñas y sesiones.
¿Te sirvió esta guía? ☕
Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:
Descubre más desde Club Programador
Suscríbete y recibe las últimas entradas en tu correo electrónico.