Autentificar usuarios en PHP
Se ha escrito mucho sobre este tema, y es fácil verificarlo con una sencilla búsqueda en Google, pero no me resisto a añadir mi propia versión.
A veces necesitamos crear sitios web en los que una parte o todo el contenido debe ser accesible sólo por determinados usuarios. También es posible que queramos personalizar ciertas partes del contenido, dependiendo de quién sea el usuario.
En cualquiera de estos casos, será necesario:
- Proporcionar una forma de acceso y verificación de usuarios.
- Diseñar una forma de evitar el acceso a determinadas áreas y funcionalidades del sitio.
Autentificar usuarios
La primera parte consiste en verificar el acceso a usuarios. Para ello primero obtendremos ciertos valores que nos permitan identificar a un usuario de forma clara, y después verificaremos que efectivamente, para esos datos existe un usuario con acceso.
Los datos a obtener puede que no sean tan evidentes como parece. Por supuesto, como todos estamos acostumbrados a usar formularios de identificación (o login), damos por hecho que se necesitan al menos dos valores para autentificar la identidad de un usuario. En la mayor parte de las aplicaciones esto es cierto. Un dato identifica al usuario, y el segundo lo autentifica.
Si sólo leemos un identificador, cualquier persona que conozca el identificador de otro usuario podría entrar al sistema. El identificador suele ser conocido, o al menos no siempre es un secreto. Normalmente se usa un nombre, un apodo (un nick), o una dirección de correo. Con sólo el identificador podríamos determinar quién se ha conectado, pero no podríamos estar seguros de que el usuario es quien dice ser.
Si sólo leemos el dato de autentificación, es decir, una contraseña, probablemente estaremos seguros de quien se ha conectado; pero, a no ser que sea el administrador quien asigne las contraseñas, se trata de una técnica peligrosa, ya que es imposible asegurar que dos usuarios no usen la misma contraseña por casualidad.
Podemos pensar en las contraseñas como si fuesen llaves. Las llaves son tanto más seguras cuanto más difíciles sean de copiar. Sin embargo, el acceso a una página no es como el acceso a un edificio, se parece más a una caja de seguridad de un banco. Lo que protegemos en una página es la propiedad de cada usuario. Generalmente cada usuario que accede a una página protegida sólo tiene acceso a sus datos, y sólo algunos usuarios especiales (los administradores), tienen acceso a datos de otros usuarios. Los datos de cada uno estarán seguros siempre que las llaves no se dupliquen o se presten, o en el caso de páginas web, siempre que no se compartan.
Usando los dos valores, usuario y contraseña, la aplicación sólo tiene que asegurarse de que los identificadores de usuario son únicos. En nuestras aplicaciones diseñaremos mecanismos para impedir que dos usuarios usen el mismo identificador. Aunque nada impide, en principio, que varios usuarios usen la misma clave.
Es muy frecuente usar una dirección de correo como identificador de usuario. Esto tiene algunas ventajas, por ejemplo:
- Permite una autentificación extra. La técnica habitual es enviar un mensaje a la dirección proporcionada, que el usuario debe responder, indicando un asunto determinado o con un enlace en el propio mensaje que el usuario debe seguir. Esto asegura que el usuario es realmente el dueño de la dirección de correo indicada.
- Es posible enviar notificaciones a los usuarios, o añadir opciones para restablecer o recordar contraseñas.
- Hace más difícil que se repitan identificadores. En sistemas donde no se requiere una dirección de correo como identificador, es habitual que se tenga que probar varias veces hasta que se encuentra un identificador válido, que no haya sido usado por otra persona.
Como inconveniente, los usuarios que no tengan una cuenta de correo no podrán darse de alta. Aunque es relativamente sencillo conseguir una cuenta de correo gratuita, se considera una mala costumbre obligar a los usuarios a cumplir determinadas condiciones para acceder a algún servicio, y en determinados casos se puede considerar una técnica discriminatoria.
En cuanto a la autentificación, implica la consulta de los datos leídos dentro de algún tipo de base de datos. La elección de esa base de datos depende de nosotros. En sistemas flexibles, en los que se pueden asignar o eliminar usuarios de forma dinámica, se suele usar una base de datos como MySQL. Si se trata de un sistema más rígido, podemos optar por otro tipo de bases de datos, como simples ficheros de texto, o en el caso más simple posible, a la codificación dura de datos, es decir, con variables definidas en PHP.
Este último método no es demasiado práctico, ya que cualquier cambio en los datos de usuarios implica modificar el código y actualizarlo en el servidor.
En este artículo usaremos una base de datos MySQL, ya que hay algunos detalles que hay que tener en cuenta en ese caso particular.
Base de datos de usuarios
En una tabla de una base de datos almacenaremos los datos de los usuarios. Para este ejemplo nos basta con un identificador y una contraseña.
También se asume que la base de datos se llama "database" y la tabla "usuario", si fuera necesario, es fácil cambiar estos nombres.
Para crear la tabla y añadir un par de usuarios para probar puedes ejecutar estas consultas SQL, dentro de la base de datos "database":
----8<------ CREATE TABLE IF NOT EXISTS `usuario` `iduser` int(11) NOT NULL AUTO_INCREMENT, `usuario` varchar(64) NOT NULL, `password` varchar(42) NOT NULL, PRIMARY KEY (`iduser`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; INSERT INTO `usuario` (`usuario`,`password`) VALUES("prueba",PASSWORD("prueba")); INSERT INTO `usuario` (`usuario`,`password`) VALUES("ejemplo",PASSWORD("ejemplo")); ----8<------
Hay que destacar que no almacenaremos las contraseñas tal cual. Esto sería un problema de seguridad, ya que cualquiera que acceda a la base de datos, incluido el administrador, podría conocer todas las contraseñas.
Nota: aunque el administrador siempre tiene la posibilidad de saltarse esta medida no debemos menospreciar esta seguridad extra, ya que es frecuente que los usuarios usen las mismas contraseñas en varios lugares. Este sistema protege las contraseñas, aunque la cuenta no sea del todo privada para el administrador.
En su lugar, lo normal es almacenar un código HASH asociado a la contraseña. MySQL dispone de una función específica para eso, que se llama "PASSWORD". En cualquier caso, se puede usar cualquier otra función HASH, como MD5 o SHA.
La ventaja de estas funciones para este caso es que no son reversibles, es decir, no es posible obtener la contraseña a partir de su código HASH, o al menos no de una forma sencilla (siempre se puede usar la fuerza bruta).
Lo que haremos para validar un usuario es verificar que la contraseña indicada tiene el mismo código HASH que está almacenado en la base de datos.
Formulario de entrada
Empecemos con la página de entrada, "index.php", que mostrará el formulario de "login":
----8<------ <!DOCTYPE HTML> <html> <head> </head> <body> <fieldset> <legend>Formulario de entrada</legend> <form method="POST" action="login.php"> <br/> <label>Usuario: </label> <input type="text" name="usuario" size="40" value=""> <br/> <br/> <label>Contraseña: </label> <input type="password" name="password" size="40" value=""> <br/> <br/> <input type="submit" name="entrar" value="Entrar"> </form> </fieldset> </body> </html> ----8<------
Verificar usuario
Cuando el usuario pulsa en "Entrar" se ejecuta el script PHP "login.php", que verifica si los valores suministrados pertenecen a un usuario válido de la base de datos. Si es así, se abre una sesión y se almacenan un par de variables de sesión:
----8<------ <?php $msUser = "tu usuario MySQL"; $msPassword = "tu contraseña MySQL"; $IdConexion = mysql_connect('localhost', $msUser, $msPassword) or die ('Imposible conectar con base de datos.'); $head = "Location: http:index.php"; if(isset($_POST["password"]) && isset($_POST["usuario"])) { if(isset($_POST["entrar"])) { $Query = "SELECT * FROM database.usuario "; $Query .= "WHERE usuario=\"".$_POST["usuario"]."\" AND password=PASSWORD(\"".$_POST["password"]."\")"; $IdConsulta = mysql_query($Query, $IdConexion); if((mysql_num_rows($IdConsulta) > 0)) { session_start(); $_SESSION["autentificado"]= true; $_SESSION["usuario"] = mysql_fetch_array($IdConsulta); $head = "Location: http:index2.php"; } } mysql_close($IdConexion); } header($head); ?> ----8<------
Si la consulta tiene éxito, creamos dos variables de sesión. En una guardamos los datos del usuario, y en la otra un bit que indica si está conectado o no.
Una vez creada las variables de sesión, almacenamos la página que se abrirá a continuación en $head, que será "index2.php". Si el usuario no es válido, $head contienen el valor inicial, que es la página con el formulario de entrada.
Al finalizar, indicamos que se cargue la página $head, mediante la directiva header().
Si el usuario no existe o la contraseña no es válida, volvemos al formulario de entrada. Si existe, se visualiza "index2.php", que de momento sólo muestra el formulario de salida.
Mecanismo de protección de contenidos
En cada página con acceso restringido tenemos que incorporar un mecanismo que evite que la página sea mostrada si el usuario no ha hecho login correctamente.
----8<------ <!DOCTYPE HTML> <?php $msUser = "tu usuario MySQL"; $msPassword = "tu contraseña MySQL"; $IdConexion = mysql_connect('localhost', $msUser, $msPassword) or die ('Imposible conectar con base de datos.'); session_start(); if(isset($_SESSION["autentificado"]) && $_SESSION["autentificado"]) { $Query = "SELECT * FROM database.usuario WHERE iduser=\"".$_SESSION["usuario"]["iduser"]."\""; $IdConsulta = mysql_query($Query, $IdConexion); // Si el usuario ya no está en la base de datos, hacer logout: if(mysql_num_rows($IdConsulta) == 0) { // Logout: $_SESSION["autentificado"] = false; session_destroy(); } } else { // Logout: $_SESSION["autentificado"] = false; session_destroy(); } mysql_close($IdConexion); if(!$_SESSION["autentificado"]) { header("location: http:index.php"); exit(0); } ?> <html> <head> </head> <body> <h1>Usuario <?php echo $_SESSION["usuario"]["usuario"]; ?> ingresado con éxito.</h1> <fieldset> <legend>Salir</legend> <form method="POST" action="logout.php"> <br/> <input type="submit" name="salir" value="Salir"> </form> </fieldset> </body> </html> ----8<------
El código PHP anterior al tag <html> verifica si el usuario tiene abierta una sesión. Si es así, no tiene ningún efecto, y el resto de la página se carga correctamente. Si el usuario no tiene una sesión abierta, o ha sido borrado desde que la abrió, la página no sigue cargando, y en su lugar se muestra de nuevo la página de "login".
Es importante verificar si el usuario sigue estando en la base de datos. Supongamos que el administrador elimine a un usuario que está actualmente conectado; si no verificamos esta condición, mantendría su acceso hasta que cierre la sesión, que es algo que normalmente no nos interesa.
Este ejemplo es simple, y sólo muestra un formulario de salida, si se pulsa "Salir" se ejecuta el script "logout.php".
----8<------ <?php session_start(); $_SESSION["autentificado"] = false; session_destroy(); header("Location: http:index.php"); ?> ----8<------
Modificamos el valor del bit que indica que el usuario está conectado. Cerramos la sesión, y cargamos la página de "login".
En casos más sofisticados podemos añadir a la base de datos tablas que permitan dar acceso a determinadas zonas de la página, definiendo niveles de acceso, o estableciendo permisos para determinadas áreas.
El administrador, o los usuarios con el nivel de acceso adecuado podrá otorgar o denegar el acceso de cada usuarios a cada zona.
También, como medida de seguridad adicional, es interesante limitar el tiempo de vida de la sesión, de modo que si un usuario abandona la página sin hacer "logout", su sesión se cierre automáticamente al cabo de unos minutos. El tiempo de fin de sesión se puede restablecer cada vez que se cargue una página. Esto evita que otras personas puedan acceder a la página con la cuenta de ese usuario desde el mismo ordenador. El inconveniente es que, si el usuario se demora mucho tiempo en una página, por ejemplo, completando un formulario, al seguir navegando se haya cerrado la sesión, y se pierdan los datos introducidos. Hay que medir bien estos tiempos, o al menos avisar al usuario en determinadas páginas del tiempo del que dispone para completar la tarea.