Autenticar, registrar y comprobar credenciales de usuarios usando PHP con MySQL
Esta es la parte 2 del tutorial para un simple login con PHP. En el login anterior vimos un ejemplo básico en donde se introducen las credenciales y si coinciden se inicia la sesión.
En este tutorial veremos cómo registrar usuarios en una base de datos, los cuales tendrán correo y contraseña. Más tarde, en el apartado del login vamos a comprobar que los datos coincidan de acuerdo a los que existen en la base de datos.
Finalmente, tendremos una página protegida a donde solamente los usuarios que hayan iniciado sesión tendrán acceso. En ella mostraremos el correo del usuario actualmente logueado.
Resumiendo, haremos un login con PHP y MySQL, manejando sesiones, así como el registro de los usuarios. De igual manera indicaremos si un usuario ya está registrado.
Nota: aunque aquí se usa MySQL, PDO permite cambiar el motor de base de datos. Un claro ejemplo es este CRUD con SQLite.
Lecturas recomendadas
Recuerda que debes tener configurado e instalado MySQL con PHP.
A lo largo del tutorial veremos algunas funciones y características que ya he expuesto antes. Te invito a ver los siguientes artículos:
PHP con MySQL parte 2: comprobar si existe y usar cursores
Ah, también mira la parte anterior de este post: login simple con PHP.
Si quieres ver un ejemplo más complejo aunque no usa sesiones, mira un sistema de ventas que hice anteriormente con PHP.
Algo más complejo que sí usa sesiones es un sistema de cotizaciones web.
En caso de que no sepas MySQL mira cómo administrarlo desde la CLI o échale un vistazo a los ejercicios parte 1, parte 2 o parte 3.
Código fuente y demostración
Al final tendremos un sitio web que permitirá registrar usuarios y dejar que inicien sesión para mostrar una página protegida con la sesión.
Puedes ver el código fuente en GitHub. Si quieres descárgalo o clónalo y pruébalo en tu entorno local.
Igualmente puedes ver cómo implementar un límite de intentos para iniciar sesión.
Registrar usuario para que se loguee más tarde
Comencemos viendo lo que se muestra al usuario cuando se quiere registrar. Es un formulario HTML que tiene un campo para el correo electrónico y dos campos para las contraseñas. Queda así:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Registro para login con PHP | parzibyte.me</title>
</head>
<body>
<!-- Se va a procesar en registrar.php y se enviará por POST, no por GET-->
<form action="registrar.php" method="post">
<!--
Nota: el atributo name es importante, pues lo vamos a recibir de esa manera
en PHP
-->
<input required name="correo" type="email" placeholder="Tu correo electrónico">
<br><br>
<input required name="palabra_secreta" type="password" placeholder="Contraseña">
<br><br>
<input required name="palabra_secreta_confirmar" type="password" placeholder="Confirma tu contraseña">
<br><br>
<!--Lo siguiente envía el formulario-->
<input type="submit" value="Registrarme">
</form>
<a href="login.html">Ya tengo una cuenta</a>
<br><br><a href="//parzibyte.me">Creado por Parzibyte</a>
</body>
</html>
Cuando el formulario sea enviado, los datos serán procesados en registrar.php cuyo código es este:
<?php
# Nota: no estamos haciendo validaciones
$correo = $_POST["correo"];
$palabra_secreta = $_POST["palabra_secreta"];
$palabra_secreta_confirmar = $_POST["palabra_secreta_confirmar"];
# Si no coinciden ambas contraseñas, lo indicamos y salimos
if ($palabra_secreta !== $palabra_secreta_confirmar) {
echo "Las contraseñas no coinciden, intente de nuevo";
exit;
}
# Incluimos las funciones, mira funciones.php para una mejor idea
include_once "funciones.php";
# Primero debemos saber si existe o no
$existe = usuarioExiste($correo);
if ($existe) {
echo "Lo siento, ya existe alguien registrado con ese correo";
exit; # Salir para no ejecutar el siguiente código
}
# Si no existe, se ejecuta esta parte
# Ahora intentamos registrarlo
$registradoCorrectamente = registrarUsuario($correo, $palabra_secreta);
if ($registradoCorrectamente) {
echo "Registrado correctamente. Ahora puedes iniciar sesión";
} else {
echo "Error al registrarte. Intenta más tarde";
}
Por ahora no te preocupes por el include de las funciones, simplemente basta con que sepas que hay una función que te dice si el usuario existe, la cual devuelve un booleano indicando si existe o no.
También hay otra función que se llama registrarUsuario
, la cual recibe la contraseña y el usuario. Por cierto, ya te habrás dado cuenta que primero comparamos que las contraseñas coincidan y si no, lo indicamos.
Si el usuario existe, se indica. Y si no existe, se trata de registrar y se muestra el resultado.
Las funciones
Es un buen momento para introducir nuestro archivo de funciones. Tiene muchos métodos útiles que nos permitirán separar la lógica y manejar todo lo relacionado a la gestión de usuarios para el login.
El código fuente es el que se ve a continuación; el de la base de datos lo veremos más tarde, por ahora basta saber que existe una función llamada obtenerBaseDeDatos
que regresa una base de datos de MySQL.
<?php
# Incluir lo de la BD, podría ser con un autoload pero eso es más avanzado
# Mira la explicación de PDO: https://parzibyte.me/blog/2019/02/16/php-pdo-parte-2-iterar-cursor-comprobar-si-elemento-existe/
include_once "base_de_datos.php";
function usuarioExiste($correo)
{
$base_de_datos = obtenerBaseDeDatos();
$sentencia = $base_de_datos->prepare("SELECT correo FROM usuarios WHERE correo = ? LIMIT 1;");
$sentencia->execute([$correo]);
return $sentencia->rowCount() > 0;
}
function obtenerUsuarioPorCorreo($correo)
{
$base_de_datos = obtenerBaseDeDatos();
$sentencia = $base_de_datos->prepare("SELECT correo, palabra_secreta FROM usuarios WHERE correo = ? LIMIT 1;");
$sentencia->execute([$correo]);
return $sentencia->fetchObject();
}
function registrarUsuario($correo, $palabraSecreta)
{
# NUNCA guardes contraseñas en texto plano
$palabraSecreta = hashearPalabraSecreta($palabraSecreta);
$base_de_datos = obtenerBaseDeDatos();
$sentencia = $base_de_datos->prepare("INSERT INTO usuarios(correo, palabra_secreta) values(?, ?)");
return $sentencia->execute([$correo, $palabraSecreta]);
}
function login($correo, $palabraSecreta)
{
# Primero obtener usuario...
$posibleUsuarioRegistrado = obtenerUsuarioPorCorreo($correo);
if ($posibleUsuarioRegistrado === false) {
# Si no existe, salimos y regresamos false
return false;
}
# Esto se ejecuta en caso de que exista
# Comprobar contraseñas
# Sacar el hash que tenemos en la BD
$palabraSecretaDeBaseDeDatos = $posibleUsuarioRegistrado->palabra_secreta;
$coinciden = coincidenPalabrasSecretas($palabraSecreta, $palabraSecretaDeBaseDeDatos);
# Si no coinciden, salimos de una vez
if (!$coinciden) {
return false;
}
# En caso de que sí hayan coincidido iniciamos sesión pasando el objeto
iniciarSesion($posibleUsuarioRegistrado);
# Y regresamos true ;)
return true;
}
function iniciarSesion($usuario)
{
// Se encarga de poner los datos dentro de la sesión
session_start();
# Y poner los datos, no recomiendo poner la contraseña
$_SESSION["correo"] = $usuario->correo;
}
# Para las contraseñas mira lo siguiente
# https://parzibyte.me/blog/2017/11/13/cifrando-comprobando-contrasenas-en-php/
function coincidenPalabrasSecretas($palabraSecreta, $palabraSecretaDeBaseDeDatos)
{
return password_verify($palabraSecreta, $palabraSecretaDeBaseDeDatos);
}
function hashearPalabraSecreta($palabraSecreta)
{
return password_hash($palabraSecreta, PASSWORD_BCRYPT);
}
Cada función se explica por sí mismo e interactúa con la base de datos dependiendo de la necesidad. Una de ellas obtiene el usuario como objeto, con todo y contraseña.
Otra función te indica si un usuario con determinado correo ya existe, y finalmente otra registra un usuario.
Hay 2 funciones que permiten comparar o hashear contraseñas, basadas en este post.
La más importante es la de login, la cual comprueba los datos e inicia la sesión en caso de que coincidan.
Todo esto lo iremos viendo a lo largo del post.
La base de datos
Es buen momento para introducir la base de datos. Cabe mencionar que ya estamos protegidos contra ataques de inyecciones SQL en los ejemplos de código. Su esquema es este:
/*
Puedes copiar y pegar el contenido
de este script directamente en la consola
de MySQL
*/
CREATE DATABASE IF NOT EXISTS usuarios_login;
USE usuarios_login;
/* Luego crea la tabla de los usuarios */
CREATE TABLE IF NOT EXISTS usuarios(
id bigint unsigned not null auto_increment,
correo varchar(255) not null unique, /*UNIQUE para evitar la duplicidad de usuarios*/
palabra_secreta varchar(255) not null,
primary key(id)
);
/* Nota: no borres la siguiente línea en blanco */
Puedes cambiar el nombre de la base de datos. Luego de ello veamos el script que conecta a MySQL desde PHP; el cual queda así:
<?php
function obtenerBaseDeDatos()
{
/*
Antes de todo, crea una base de datos con el nombre
que quieras y ponlo abajo en las credenciales
CREATE DATABASE usuarios_login;
USE usuarios_login;
Luego crea la tabla de los usuarios
CREATE TABLE IF NOT EXISTS usuarios(
id bigint unsigned not null auto_increment,
correo varchar(255) not null unique,
palabra_secreta varchar(255) not null,
primary key(id)
);
Esto lo puedes hacer desde la consola o desde PhpMyAdmin
recomiendo la consola, pues programador que se respeta no usa PMA ;)
Más información: esquema.sql
*/
// Nota: rellena con tus credenciales
$nombre_base_de_datos = "usuarios_login";
$usuario = "root";
$contraseña = "";
try {
$base_de_datos = new PDO('mysql:host=localhost;dbname=' . $nombre_base_de_datos, $usuario, $contraseña);
$base_de_datos->query("set names utf8;");
$base_de_datos->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$base_de_datos->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$base_de_datos->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
return $base_de_datos;
} catch (Exception $e) {
# Nota: ¡en la vida real no imprimas errores!
echo "Error obteniendo BD: " . $e->getMessage();
return null;
}
}
Si cambiaste el nombre de la base de datos o tus credenciales son distintas recuerda cambiar todo acorde a tus datos.
Login de usuario
Veamos el formulario que permite iniciar sesión. Queda así:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Ejemplo simple de login con PHP | parzibyte.me</title>
</head>
<body>
<!-- Se va a procesar en login.php y se enviará por POST, no por GET-->
<form action="login.php" method="post">
<!--
Nota: el atributo name es importante, pues lo vamos a recibir de esa manera
en PHP
-->
<input name="correo" type="email" placeholder="Escribe tu correo electrónico">
<br><br>
<input name="palabra_secreta" type="password" placeholder="Contraseña">
<br><br>
<!--Lo siguiente envía el formulario-->
<input type="submit" value="Iniciar sesión">
</form>
<a href="registro.html">Registrarme</a>
<br><br><a href="//parzibyte.me">Creado por Parzibyte</a>
</body>
</html>
Ahora no necesitamos que ponga la contraseña 2 veces, solamente una, pues se hace así únicamente en el registro.
Cuando se envíe el formulario, los datos serán procesados en login.php. Ahí los recibimos y usamos de nuevo las funciones que vimos arriba.
Lo bueno de separar todo esto es que el código se reduce y se promueve la reutilización de código.
<?php
# Nota: no estamos haciendo validaciones
$correo = $_POST["correo"];
$palabra_secreta = $_POST["palabra_secreta"];
# Luego de haber obtenido los valores, ya podemos comprobar
# Incluimos a las funciones, mira funciones.php
include_once "funciones.php";
$logueadoConExito = login($correo, $palabra_secreta);
if ($logueadoConExito) {
# Redirigir a secreta
header("Location: secreta.php");
# Y salir
exit;
} else {
# Si no, entonces indicarlo
echo "Usuario o contraseña incorrecta";
}
Hacemos una redirección a la página protegida o secreta en donde solamente entran los usuarios logueados; recuerda que el inicio de sesión y la comprobación de datos se hace en el archivo de funciones.php en conjunto con base_de_datos.php.
Página secreta o protegida
Finalmente veamos la página a la que solamente los usuarios que han iniciado sesión pueden ver. Ahí mostramos también el correo del usuario que está logueado, el cual guardamos en $_SESSION
dentro de funciones.php.
Queda así:
<?php
# Si no entiendes el código, primero mira a login.php
# Iniciar sesión para usar $_SESSION
session_start();
# Y ahora leer si NO hay algo llamado correo en la sesión,
# usando empty (vacío, ¿está vacío?)
# Recomiendo: https://parzibyte.me/blog/2018/08/09/isset-vs-empty-en-php/
if (empty($_SESSION["correo"])) {
# Lo redireccionamos al formulario de inicio de sesión
header("Location: login.html");
# Y salimos del script
exit();
}
# No hace falta un else, pues si el usuario no se loguea, todo lo de abajo no se ejecuta
echo "Soy un mensaje secreto.";
# Podemos recuperar datos de la sesión
echo "<br>Sé que tu correo es: <strong>" . $_SESSION["correo"] . "</strong>";
?>
<!-- Por cierto, también se puede usar HTML como en todos los scripts de PHP-->
<p>
Hola mundo, soy un párrafo HTML que solamente pueden ver los usuarios logueados
</p>
<!-- Y aprovechando, le indicamos al usuario un enlace para salir-->
<a href="logout.php">Cerrar sesión</a>
Con eso queda completado el tutorial.
Conclusión
Parece simple, pero con esta protección podemos tener una pequeña página protegida. Claro que eso no es suficiente, pues debemos ver todo eso de la regeneración del id o la protección contra ataques CSRF.
En caso de querer proteger otras páginas, simplemente inicia sesión y verifica que haya algo en $_SESSION["correo"]
.
Si te quejas de los estilos, traté de quitar la parte visual para enfocarnos en la funcionalidad. Te invito a ver el sistema de cotizaciones (que además es gratuito y open source) en donde usamos una variante de Bootstrap y además hacemos que la página sea responsiva.
Se puede implementar en un proyecto de Vue?
Claro, es totalmente posible
Gracias por el codigo y la explicacion, de verdad.
En el codigo descargado he hecho una pequeña correccion, en el index apuntaba a formulario.html y debiera ser login.html como muestro abajo:
bueno quise poner “pegar” pero parece que eso no funciona aquí así que en definitiva al iniciar el index.htm poner header(“Location: login.html”);
Saludos!
Hola. Gracias por tus comentarios. Te invito a seguirme y compartir.
Saludos 🙂