php

Login con base de datos de MySQL en PHP

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.

PHP y MySQL – Login, registro y autenticación de usuarios

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:

Conectar PHP y MySQL

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.

Login en PHP con MySQL y sesiones

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

13 horas hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

7 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

7 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

7 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

7 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

7 días hace

Esta web usa cookies.