Inici de sessió: diferència entre les revisions

De Wiki personal d'en Guillem Serrat
Es crea la pàgina amb «== Inici de sessió == == Connexió a la BD i inici de sessió (PHP) == == Formulari d'inici de sessió == Es mostra A més, en cas que no s’hagi registrat, tindrà un botó per registrar-se. A part, si no recorda de les seves credencials, tindrà un botó per poder recuperar la contrasenya. Per últim, també se li dona a l’usuari la oportunitat (sense obligar-lo) a autenticar-se amb el 2FA. == Inici de sessió manual == === Obtenció de les dades d'usua...».
 
Cap resum de modificació
 
(Hi ha 4 revisions intermèdies que no es mostren del mateix usuari)
Línia 1: Línia 1:
== Inici de sessió ==
== Inici de sessió ==
Quan els usuaris desitjen iniciar sessió, tenen un formulari on poden introduir el seu usuari i contrasenya. A més, tenen l'opció de que l'aplicatiu els recordi desant un token d'autenticació únic dins d'una galeta, si així ho desitja.


== Connexió a la BD i inici de sessió (PHP) ==
== Connexió a la BD i inici de sessió (PHP) ==
Sempre que es treballa amb sessions, el primer que hem de fer és iniciar-la abans d'escriure el codi HTML
session_start();
A més, hem de requerir el fitxer amb les funcions i una connexió a la BD. En aquest cas, si realitzem una autenticació errònia, haurem d'incrementar el número d'intents de login de la BD i per tant requerim la [[Connexions a la BD (A5.3)#Connexió d'escriptura|connexió d'escriptura]].
require 'funcions.php';
require './connexioBD/connexioRW.php';
== Comprovar si està autenticat ==
En cas que l'usuari està autenticat, el redirigirem directament a la seva pàgina privada. Per comprovar si l'usuari està autenticat farem servir la funció [[Funcions de la pràctica 5.3#esta autenticat|esta_autenticat]].<pre>
// Redirigir si ja hi ha una sessió activa
if (esta_autenticat()) {
    header('Location: privada.php');
    exit;
}
</pre>


== Formulari d'inici de sessió ==
== Formulari d'inici de sessió ==
Es mostra A més, en cas que no s’hagi registrat, tindrà un botó per registrar-se. A part, si no recorda de les seves credencials, tindrà un botó per poder recuperar la contrasenya.
Al formulari es mostra dues entrades de text per introduir el nom d'usuari i la contrasenya. A més, en cas que no s’hagi registrat, tindrà un botó per registrar-se, i si no recorda de les seves credencials, tindrà un botó per poder recuperar la contrasenya.
Per últim, també se li dona a l’usuari la oportunitat (sense obligar-lo) a autenticar-se amb el 2FA.
 
També es mostra una casella de selecció perque l'aplicatiu el "recordi"
 
Per últim, també se li dona a l’usuari la oportunitat (sense obligar-lo) a autenticar-se amb 2FA.
 
El formulari es visualitzaria de la següent forma:
[[Fitxer:FormulariIniciSessio.png|center|miniatura|559x559px]]


== Inici de sessió manual ==
== Inici de sessió manual ==


=== Obtenció de les dades d'usuari ===
=== Obtenció de les dades d'usuari del formulari i la BD ===
Un cop es respongui el formulari, obtindrem les dades del formulari i realitzarem una consulta per comprovar si el nom d'usuari realment existeix<pre>
// Recuperem les dades del formulari
$nom_usuari = trim($_POST['nom_usuari']);
$contrasenya = $_POST['contrasenya'];
$recordar = isset($_POST['recordar']); // Opció per crear o no galetes
 
// Recuperem les dades de l'usuari amb el nom d'usuari especificat
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?");
$stmt->execute([$nom_usuari]);
$usuari = $stmt->fetch();
 
// Verifiquem que l'usuari existeixi
if ($usuari) {
</pre>


=== Comprovació d'usuari bloquejat ===
=== Comprovació d'usuari bloquejat ===
En cas que existeixi, primerament comprovarem si el compte està bloquejat. Per això comprovarem:
* Que el nombre d’intents d’inici de sessió que s’ha enregistrat a la BD sigui major o igual a $max_intents, en aquest cas 3 intents
* Si l’últim intent d’inici de sessió s’ha produit fa menys de $bloqueig_minuts, en aquest cas 2 minuts
Si es cumpleixen els 2 requisits, el compte estarà bloquejat.<pre>
// Comprovar si hi ha bloqueig
if ($usuari['intents_login'] >= $max_intents
      && strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) {
</pre>Per fer el càlcul de les dates fem servir la funció interna de PHP strtotime(), la qual retorna la data introduida en temps UNIX (nº de segons des de 1/1/1970).
strtotime($usuari['ultim_intent']) retorna el número de segons que han passat del 1/1/1970 a la data especificada (o dit d'una altra manera, quans segons han passat des de l’1/1/1970 fins el 10/1/2026 a les 15:00)
strtotime("-$bloqueig_minuts minutes") retorna el número de segons que han passat des de l’1/1/1970 fa $bloqueig_minuts minuts (o dit d'una altra manera, quans segons han passat des de l’1/1/1970 fa 2 minuts)
Així, tenim la mateixa “base temporal” amb la que fer càlculs
En cas que el compte estigui bloquejat, mostrarem a l’usuari el temps que queda per desbloquejar-lo.
Per això:
# Calculem el temps (l’hora) UNIX de l’últim intent
## Exemple (Exemple: 15:00 emmagatzemat en format UNIX)
# Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
## Agafem el temps de l’últim intent i li sumem X minuts en segons, en aquest cas 2 minuts per 60 segons
## Exemple: (Exemple: 15:02, emmagatzemat en format UNIX)
# Calculem els segons restants
## Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual
## Exemple: 15:02 - 15:01 = 00:01 (60 segons)
# Convertim els segons restants a minuts i segons
## 80 segons = 1 minut i 20 segons
# Mostrem els minuts i segons restants
<pre>
// Calcular temps restant
$ultim_intent_ts = strtotime($usuari['ultim_intent']); //Calculem el temps (l’hora) UNIX de l’últim intent
$final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60); //Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
$segons_restants = $final_bloqueig_ts - time(); // Calculem els segons restants. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual
$minuts_restants = floor($segons_restants / 60); // Convertim els segons restants a minuts
$segons_restants = $segons_restants % 60; // Recuperem els segons restants després de calcular les hores
$error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts_restants} minuts i {$segons_restants} segons.";
</pre>Per tant el fluxe del codi seria el següent:
# L’usuari s’equivoca 3 vegades
# Intenta accedir una 4ta, no pot (3 intents_login, 3 segons últim intent)
# Espera 2 minuts (3 intents_login, 2 minuts últim intent, pot accedir)
# Es torna a equivocar una 5na vegada  (3 intents_login, 3 segons últim intent)
# Torna a esperar 2 minuts
# Accedeix correctament


=== Comprovació de contrasenya ===
=== Comprovació de contrasenya ===
Un cop verificat que l'usuari no està bloquejat, comprovarem que la contrasenya sigui correcta amb la funció [[Funcions de la pràctica 5.3#verificar contrasenya|verificar_contrasenya]].<pre>
if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) {
</pre>En cas que la contrasenya sigui errònia, augmentarem en 1 els número d'intents d'autenticació, registrarem una acció a la taula activitat de nom "error-login" i registrarem un missatge d'error<pre>
// Si la contrasenya és incorrecte, sumem un intent de login
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?");
$stmt->execute([$usuari['id']]);
// Registrar intent fallit
registrar_activitat($pdo, $usuari['id'], 'error-login');
$error = "Usuari o contrasenya incorrecte";
</pre>


=== Inici de sessió ===
=== Inici de sessió ===
Si la contrasenya és correcte,  actualitzarem el nombre d’intents de login a 0<pre>
// Login correcte, resetajer intents
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?");
$stmt->execute([$usuari['id']]);
</pre>Seguidament, regenerarem la ID de sessió per evitar atacs de sessió fixada<pre>
session_regenerate_id(true); // Protecció session fixation
</pre>Per acabar creant un vector de nom “usuari” dins de la sessió amb els següents vectors:
* ID de l’usuari dins la BD
* Nom d’usuari
* Nom complet
* Rol, per definir si és administrador o no i quin tipus
* Autenticat, per definir que està autenticat
<pre>
$_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
      'id' => $usuari['id'], // El seu ID a la BD
      'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
      'nom_complet' => $usuari['nom_complet'], // El nom complet
      'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
      'autenticat' => true // Definim que està autenticat
];
</pre>Un cop autenticat, registrarem una acció a la taula activitat de nom "login" i redirigirem l'usuari a la seva pàgina privada<pre>
// Registrem l'acció "login"
registrar_activitat($pdo, $usuari['id'], 'login');
header('Location: privada.php');
exit;
</pre>


=== "Recordar-me" ===
=== "Recordar-me" ===
En cas que l'usuari hagi decidit que l'aplicació el recordi, generarem un token únic que ens servirà per identificar la seva identitat posteriorment en l'inici de sessió amb cookies<pre>
// Generar un token únic i desar-lo a la BD
$token = bin2hex(random_bytes(16));
</pre>Aquest token, juntament amb la ID de l'usuari les guardarem en galetes<pre>
setcookie('recordar_id', $usuari['id'], time() + 30*24*60*60, '/'); // Recordem la ID de l'usuari
setcookie('recordar_token', $token, time() + 30*24*60*60, '/'); // Recordem el token desat a la BD
</pre>I per últim, desarem el token únic dins la BD en el registre de l'usuari<pre>
$stmt = $pdo->prepare("UPDATE usuaris SET token_recordar = ? WHERE id = ?");
$stmt->execute([$token, $usuari['id']]);
</pre>


== Inici de sessió mitjançant cookies ==
== Inici de sessió mitjançant cookies ==
Un cop l'usuari tanca el navegador i torna a obrir-lo, en cas que tingui cookies guardades, el codi les recuperarà<pre>
// AUTO-LOGIN en cas d'haver-hi cookies
if (!esta_autenticat() && isset($_COOKIE['recordar_id'], $_COOKIE['recordar_token'])) {
    $id = $_COOKIE['recordar_id']; // Recuperem la ID de l'usuari
    $token = $_COOKIE['recordar_token']; // Recuperem el token d'autenticació
</pre>Per verificar que les cookies són correctes, es realitzarà una consulta SQL on es cercarà un usuari amb la ID i el token que s'han desat a la cookie.
Si les cookies són correctes, es trobarà amb l'usuari que havia iniciat sessió.<pre>
// Validar cookies amb la BD
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE id = ? AND token_recordar = ?");
$stmt->execute([$id, $token]);
$usuari = $stmt->fetch();
// Si hi ha un usuari amb la ID i el token que està guardat a la cookie
if ($usuari) {
</pre>Si l’usuari existeix (vol dir que la ID i el token són correctes), regenerarem la ID de sessió per evitar atacs de sessió fixada i definirem els mateixos vectors que s’ha comentat anteriorment. <pre>
session_regenerate_id(true); // Regenerem la ID de sessió
$_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
      'id' => $usuari['id'], // El seu ID a la BD
      'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
      'nom_complet' => $usuari['nom_complet'], // El nom complet
      'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
      'autenticat' => true // Definim que està autenticat
];
</pre>Codi Per últim, registrarem una acció nomenada “auto-login” i redirigirem a l’usuari a la seva pàgina privada<pre>
registrar_activitat($pdo, $usuari['id'], 'auto-login'); // Registrem acció "auto-login"
header('Location: privada.php');
exit;
</pre>En cas que el valor de les cookies sigui erroni (el token no sigui correcte o la ID no existeixi), no només no permetrem l'inici de sessió sinó que a més a més esborrarem les cookies per evitar més intents<pre>
} else { // Si la consulta no retorna res
// La ID o el token son erronis i no s'inicia sessió. Al ser erroni s'esborra la cookie per evitar nous intents
    setcookie('recordar_id', '', time() - 3600, '/');
    setcookie('recordar_token', '', time() - 3600, '/');
}
</pre>
== Mostra de missatges d'error o èxit ==
En cas que l'usuari introdueixi unes credencials errònies, es mostrarà un missatge d'error indicant-ho.
Un exemple de missatge es visualitzaria de la següent forma
[[Fitxer:MissatgeErrorLogin.png|center|miniatura|499x499px]]
== Codi complet ==
<pre>
<?php
date_default_timezone_set('Europe/Madrid');
session_start();
require 'funcions.php';
require './connexioBD/connexioRW.php';
$error = '';
// Redirigir si ja hi ha una sessió activa
if (esta_autenticat()) {
    header('Location: privada.php');
    exit;
}
try{
    // AUTO-LOGIN en cas d'haver-hi cookies
    if (!esta_autenticat() && isset($_COOKIE['recordar_id'], $_COOKIE['recordar_token'])) {
        $id = $_COOKIE['recordar_id']; // Recuperem la ID de l'usuari
        $token = $_COOKIE['recordar_token']; // Recuperem el token d'autenticació
        // Validar cookies amb la BD
        $stmt = $pdo->prepare("SELECT * FROM usuaris WHERE id = ? AND token_recordar = ?");
        $stmt->execute([$id, $token]);
        $usuari = $stmt->fetch();
        // Si hi ha un usuari amb la ID i el token que està guardat a la cookie
        if ($usuari) {
            session_regenerate_id(true); // Regenerem la ID de sessió
            $_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
                'id' => $usuari['id'], // El seu ID a la BD
                'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
                'nom_complet' => $usuari['nom_complet'], // El nom complet
                'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
                'autenticat' => true // Definim que està autenticat
            ];
            registrar_activitat($pdo, $usuari['id'], 'auto-login'); // Registrem acció "auto-login"
            header('Location: privada.php');
            exit;
        } else { // Si la consulta no retorna res
            // La ID o el token son erronis i no s'inicia sessió. Al ser erroni s'esborra la cookie per evitar nous intents
            setcookie('recordar_id', '', time() - 3600, '/');
            setcookie('recordar_token', '', time() - 3600, '/');
        }
    }
} catch (PDOException $e) {
    $error = "Error en iniciar sessió automàticament. Motiu " . $e->getMessage();
}
// Variables de bloqueig
$max_intents = 3;      // Màxim d'intents erronis
$bloqueig_minuts = 2;  // Temps de bloqueig (minuts)
try{
    // LOGIN manual
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        // Recuperem les dades del formulari
        $nom_usuari = trim($_POST['nom_usuari']);
        $contrasenya = $_POST['contrasenya'];
        $recordar = isset($_POST['recordar']); // Opció per crear o no galetes
        // Recuperem les dades de l'usuari amb el nom d'usuari especificat
        $stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?");
        $stmt->execute([$nom_usuari]);
        $usuari = $stmt->fetch();
        // Verifiquem que l'usuari existeixi
        if ($usuari) {
            // Comprovar si hi ha bloqueig
            if ($usuari['intents_login'] >= $max_intents
                && strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) {
                // Calcular temps restant
                $ultim_intent_ts = strtotime($usuari['ultim_intent']); //Calculem el temps (l’hora) UNIX de l’últim intent
                $final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60); //Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
                $segons_restants = $final_bloqueig_ts - time(); // Calculem els segons restants. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual
                $minuts_restants = floor($segons_restants / 60); // Convertim els segons restants a minuts
                $segons_restants = $segons_restants % 60; // Recuperem els segons restants després de calcular les hores
                $error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts_restants} minuts i {$segons_restants} segons.";
            } else { // Si l'usuari no està bloquejat
                if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) {
                    // Login correcte, resetajer intents
                    $stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?");
                    $stmt->execute([$usuari['id']]);
                    session_regenerate_id(true); // Protecció session fixation
                    $_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
                        'id' => $usuari['id'], // El seu ID a la BD
                        'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
                        'nom_complet' => $usuari['nom_complet'], // El nom complet
                        'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
                        'autenticat' => true // Definim que està autenticat
                    ];
                    if ($recordar) { // En cas que volguem desar la sessió tot i tancar el navegador
                        // Generar un token únic i desar-lo a la BD
                        $token = bin2hex(random_bytes(16));
                        setcookie('recordar_id', $usuari['id'], time() + 30*24*60*60, '/'); // Recordem la ID de l'usuari
                        setcookie('recordar_token', $token, time() + 30*24*60*60, '/'); // Recordem el token desat a la BD
                        $stmt = $pdo->prepare("UPDATE usuaris SET token_recordar = ? WHERE id = ?");
                        $stmt->execute([$token, $usuari['id']]);
                    }
                    // Registrem l'acció "login"
                    registrar_activitat($pdo, $usuari['id'], 'login');
                    header('Location: privada.php');
                    exit;
                } else {
                    // Si la contrasenya és incorrecte, sumem un intent de login
                    $stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?");
                    $stmt->execute([$usuari['id']]);
                    // Registrar intent fallit
                    registrar_activitat($pdo, $usuari['id'], 'error-login');
                    $error = "Usuari o contrasenya incorrecte";
                }
            }
        } else {
            $error = "L'usuari no existeix";
        }
    }
} catch (PDOException $e) {
    $error = "Error en iniciar sessió. Motiu: " . $e->getMessage();
}
?>
<html>
<head>
    <meta lang="ca">
    <meta charset="UTF-8">
    <title>Inici de sessió</title>
    <link rel="stylesheet" href="./css/login.css">
</head>
<body>
    <div class="login-card">
        <h2>Login</h2>
        <?php if ($error): ?>
            <div class="error-container">
                <p style='color:red;'><?= $error ?></p>
            </div>
        <?php endif; ?>
       
        <form method="post">
            <div class="input-group">
                <label>Nom d'usuari:</label>
                <input type="text" name="nom_usuari" required>
                <a href="registre.php" class="helper-link">No tens usuari? Crea'n un</a>
            </div>
            <div class="input-group">
                <label>Contrasenya:</label>
                <input type="password" name="contrasenya" required>
                <a href="recuperacio.php" class="helper-link">He oblidat la contrasenya</a>
            </div>
            <div class="checkbox-group">
                <input type="checkbox" name="recordar" id="recordar">
                <label for="recordar">Recordar-me</label>
            </div>
            <input type="submit" value="Entra">
        </form>
        <div class="separator"><span>O també pots</span></div>
        <div class="mfa-section">
            <a href="mfa.php"><button type="button" class="btn-2fa">Iniciar sessió amb 2FA</button></a>
        </div>
    </div>
</body>
</html>
</pre>

Revisió de 02:19, 12 gen 2026

Inici de sessió

Quan els usuaris desitjen iniciar sessió, tenen un formulari on poden introduir el seu usuari i contrasenya. A més, tenen l'opció de que l'aplicatiu els recordi desant un token d'autenticació únic dins d'una galeta, si així ho desitja.

Connexió a la BD i inici de sessió (PHP)

Sempre que es treballa amb sessions, el primer que hem de fer és iniciar-la abans d'escriure el codi HTML

session_start();

A més, hem de requerir el fitxer amb les funcions i una connexió a la BD. En aquest cas, si realitzem una autenticació errònia, haurem d'incrementar el número d'intents de login de la BD i per tant requerim la connexió d'escriptura.

require 'funcions.php';
require './connexioBD/connexioRW.php';

Comprovar si està autenticat

En cas que l'usuari està autenticat, el redirigirem directament a la seva pàgina privada. Per comprovar si l'usuari està autenticat farem servir la funció esta_autenticat.

// Redirigir si ja hi ha una sessió activa
if (esta_autenticat()) {
    header('Location: privada.php');
    exit;
}

Formulari d'inici de sessió

Al formulari es mostra dues entrades de text per introduir el nom d'usuari i la contrasenya. A més, en cas que no s’hagi registrat, tindrà un botó per registrar-se, i si no recorda de les seves credencials, tindrà un botó per poder recuperar la contrasenya.

També es mostra una casella de selecció perque l'aplicatiu el "recordi"

Per últim, també se li dona a l’usuari la oportunitat (sense obligar-lo) a autenticar-se amb 2FA.

El formulari es visualitzaria de la següent forma:

Inici de sessió manual

Obtenció de les dades d'usuari del formulari i la BD

Un cop es respongui el formulari, obtindrem les dades del formulari i realitzarem una consulta per comprovar si el nom d'usuari realment existeix

// Recuperem les dades del formulari
$nom_usuari = trim($_POST['nom_usuari']);
$contrasenya = $_POST['contrasenya'];
$recordar = isset($_POST['recordar']); // Opció per crear o no galetes

// Recuperem les dades de l'usuari amb el nom d'usuari especificat
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?");
$stmt->execute([$nom_usuari]);
$usuari = $stmt->fetch();

// Verifiquem que l'usuari existeixi
if ($usuari) {

Comprovació d'usuari bloquejat

En cas que existeixi, primerament comprovarem si el compte està bloquejat. Per això comprovarem:

  • Que el nombre d’intents d’inici de sessió que s’ha enregistrat a la BD sigui major o igual a $max_intents, en aquest cas 3 intents
  • Si l’últim intent d’inici de sessió s’ha produit fa menys de $bloqueig_minuts, en aquest cas 2 minuts

Si es cumpleixen els 2 requisits, el compte estarà bloquejat.

// Comprovar si hi ha bloqueig
if ($usuari['intents_login'] >= $max_intents 
      && strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) {

Per fer el càlcul de les dates fem servir la funció interna de PHP strtotime(), la qual retorna la data introduida en temps UNIX (nº de segons des de 1/1/1970).

strtotime($usuari['ultim_intent']) retorna el número de segons que han passat del 1/1/1970 a la data especificada (o dit d'una altra manera, quans segons han passat des de l’1/1/1970 fins el 10/1/2026 a les 15:00)

strtotime("-$bloqueig_minuts minutes") retorna el número de segons que han passat des de l’1/1/1970 fa $bloqueig_minuts minuts (o dit d'una altra manera, quans segons han passat des de l’1/1/1970 fa 2 minuts)

Així, tenim la mateixa “base temporal” amb la que fer càlculs

En cas que el compte estigui bloquejat, mostrarem a l’usuari el temps que queda per desbloquejar-lo.

Per això:

  1. Calculem el temps (l’hora) UNIX de l’últim intent
    1. Exemple (Exemple: 15:00 emmagatzemat en format UNIX)
  2. Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
    1. Agafem el temps de l’últim intent i li sumem X minuts en segons, en aquest cas 2 minuts per 60 segons
    2. Exemple: (Exemple: 15:02, emmagatzemat en format UNIX)
  3. Calculem els segons restants
    1. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual
    2. Exemple: 15:02 - 15:01 = 00:01 (60 segons)
  4. Convertim els segons restants a minuts i segons
    1. 80 segons = 1 minut i 20 segons
  5. Mostrem els minuts i segons restants
// Calcular temps restant
$ultim_intent_ts = strtotime($usuari['ultim_intent']); //Calculem el temps (l’hora) UNIX de l’últim intent 
$final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60); //Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
$segons_restants = $final_bloqueig_ts - time(); // Calculem els segons restants. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual

$minuts_restants = floor($segons_restants / 60); // Convertim els segons restants a minuts
$segons_restants = $segons_restants % 60; // Recuperem els segons restants després de calcular les hores

$error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts_restants} minuts i {$segons_restants} segons.";

Per tant el fluxe del codi seria el següent:

  1. L’usuari s’equivoca 3 vegades
  2. Intenta accedir una 4ta, no pot (3 intents_login, 3 segons últim intent)
  3. Espera 2 minuts (3 intents_login, 2 minuts últim intent, pot accedir)
  4. Es torna a equivocar una 5na vegada  (3 intents_login, 3 segons últim intent)
  5. Torna a esperar 2 minuts
  6. Accedeix correctament

Comprovació de contrasenya

Un cop verificat que l'usuari no està bloquejat, comprovarem que la contrasenya sigui correcta amb la funció verificar_contrasenya.

 if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) {

En cas que la contrasenya sigui errònia, augmentarem en 1 els número d'intents d'autenticació, registrarem una acció a la taula activitat de nom "error-login" i registrarem un missatge d'error

// Si la contrasenya és incorrecte, sumem un intent de login $stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?"); $stmt->execute([$usuari['id']]);

// Registrar intent fallit registrar_activitat($pdo, $usuari['id'], 'error-login'); $error = "Usuari o contrasenya incorrecte";

Inici de sessió

Si la contrasenya és correcte, actualitzarem el nombre d’intents de login a 0

// Login correcte, resetajer intents
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?");
$stmt->execute([$usuari['id']]);

Seguidament, regenerarem la ID de sessió per evitar atacs de sessió fixada

session_regenerate_id(true); // Protecció session fixation

Per acabar creant un vector de nom “usuari” dins de la sessió amb els següents vectors:

  • ID de l’usuari dins la BD
  • Nom d’usuari
  • Nom complet
  • Rol, per definir si és administrador o no i quin tipus
  • Autenticat, per definir que està autenticat
$_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
      'id' => $usuari['id'], // El seu ID a la BD
      'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
      'nom_complet' => $usuari['nom_complet'], // El nom complet
      'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
      'autenticat' => true // Definim que està autenticat
];

Un cop autenticat, registrarem una acció a la taula activitat de nom "login" i redirigirem l'usuari a la seva pàgina privada

// Registrem l'acció "login" registrar_activitat($pdo, $usuari['id'], 'login'); header('Location: privada.php'); exit;

"Recordar-me"

En cas que l'usuari hagi decidit que l'aplicació el recordi, generarem un token únic que ens servirà per identificar la seva identitat posteriorment en l'inici de sessió amb cookies

// Generar un token únic i desar-lo a la BD
$token = bin2hex(random_bytes(16));

Aquest token, juntament amb la ID de l'usuari les guardarem en galetes

setcookie('recordar_id', $usuari['id'], time() + 30*24*60*60, '/'); // Recordem la ID de l'usuari setcookie('recordar_token', $token, time() + 30*24*60*60, '/'); // Recordem el token desat a la BD

I per últim, desarem el token únic dins la BD en el registre de l'usuari

$stmt = $pdo->prepare("UPDATE usuaris SET token_recordar = ? WHERE id = ?"); $stmt->execute([$token, $usuari['id']]);

Inici de sessió mitjançant cookies

Un cop l'usuari tanca el navegador i torna a obrir-lo, en cas que tingui cookies guardades, el codi les recuperarà

// AUTO-LOGIN en cas d'haver-hi cookies
if (!esta_autenticat() && isset($_COOKIE['recordar_id'], $_COOKIE['recordar_token'])) {
    $id = $_COOKIE['recordar_id']; // Recuperem la ID de l'usuari
    $token = $_COOKIE['recordar_token']; // Recuperem el token d'autenticació

Per verificar que les cookies són correctes, es realitzarà una consulta SQL on es cercarà un usuari amb la ID i el token que s'han desat a la cookie. Si les cookies són correctes, es trobarà amb l'usuari que havia iniciat sessió.

// Validar cookies amb la BD
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE id = ? AND token_recordar = ?");
$stmt->execute([$id, $token]);
$usuari = $stmt->fetch();

// Si hi ha un usuari amb la ID i el token que està guardat a la cookie
if ($usuari) {

Si l’usuari existeix (vol dir que la ID i el token són correctes), regenerarem la ID de sessió per evitar atacs de sessió fixada i definirem els mateixos vectors que s’ha comentat anteriorment.

session_regenerate_id(true); // Regenerem la ID de sessió $_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant

      'id' => $usuari['id'], // El seu ID a la BD
      'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
      'nom_complet' => $usuari['nom_complet'], // El nom complet
      'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
      'autenticat' => true // Definim que està autenticat

];

Codi Per últim, registrarem una acció nomenada “auto-login” i redirigirem a l’usuari a la seva pàgina privada

registrar_activitat($pdo, $usuari['id'], 'auto-login'); // Registrem acció "auto-login" header('Location: privada.php'); exit;

En cas que el valor de les cookies sigui erroni (el token no sigui correcte o la ID no existeixi), no només no permetrem l'inici de sessió sinó que a més a més esborrarem les cookies per evitar més intents

} else { // Si la consulta no retorna res // La ID o el token son erronis i no s'inicia sessió. Al ser erroni s'esborra la cookie per evitar nous intents

    setcookie('recordar_id', '', time() - 3600, '/');
    setcookie('recordar_token', '', time() - 3600, '/');

}

Mostra de missatges d'error o èxit

En cas que l'usuari introdueixi unes credencials errònies, es mostrarà un missatge d'error indicant-ho.

Un exemple de missatge es visualitzaria de la següent forma

Codi complet

<?php
date_default_timezone_set('Europe/Madrid'); 

session_start();
require 'funcions.php';
require './connexioBD/connexioRW.php';

$error = '';

// Redirigir si ja hi ha una sessió activa
if (esta_autenticat()) {
    header('Location: privada.php');
    exit;
}

try{
    // AUTO-LOGIN en cas d'haver-hi cookies
    if (!esta_autenticat() && isset($_COOKIE['recordar_id'], $_COOKIE['recordar_token'])) {
        $id = $_COOKIE['recordar_id']; // Recuperem la ID de l'usuari
        $token = $_COOKIE['recordar_token']; // Recuperem el token d'autenticació

        // Validar cookies amb la BD
        $stmt = $pdo->prepare("SELECT * FROM usuaris WHERE id = ? AND token_recordar = ?");
        $stmt->execute([$id, $token]);
        $usuari = $stmt->fetch();

        // Si hi ha un usuari amb la ID i el token que està guardat a la cookie
        if ($usuari) {
            session_regenerate_id(true); // Regenerem la ID de sessió
            $_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
                'id' => $usuari['id'], // El seu ID a la BD
                'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
                'nom_complet' => $usuari['nom_complet'], // El nom complet
                'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
                'autenticat' => true // Definim que està autenticat
            ];
            registrar_activitat($pdo, $usuari['id'], 'auto-login'); // Registrem acció "auto-login"
            header('Location: privada.php');
            exit;
        } else { // Si la consulta no retorna res
            // La ID o el token son erronis i no s'inicia sessió. Al ser erroni s'esborra la cookie per evitar nous intents
            setcookie('recordar_id', '', time() - 3600, '/');
            setcookie('recordar_token', '', time() - 3600, '/');
        }
    }
} catch (PDOException $e) {
    $error = "Error en iniciar sessió automàticament. Motiu " . $e->getMessage();
}

// Variables de bloqueig
$max_intents = 3;       // Màxim d'intents erronis
$bloqueig_minuts = 2;  // Temps de bloqueig (minuts)

try{
    // LOGIN manual
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        // Recuperem les dades del formulari
        $nom_usuari = trim($_POST['nom_usuari']);
        $contrasenya = $_POST['contrasenya'];
        $recordar = isset($_POST['recordar']); // Opció per crear o no galetes

        // Recuperem les dades de l'usuari amb el nom d'usuari especificat
        $stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?");
        $stmt->execute([$nom_usuari]);
        $usuari = $stmt->fetch();

        // Verifiquem que l'usuari existeixi
        if ($usuari) {

            // Comprovar si hi ha bloqueig
            if ($usuari['intents_login'] >= $max_intents 
                && strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) {
                // Calcular temps restant
                $ultim_intent_ts = strtotime($usuari['ultim_intent']); //Calculem el temps (l’hora) UNIX de l’últim intent 
                $final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60); //Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
                $segons_restants = $final_bloqueig_ts - time(); // Calculem els segons restants. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual

                $minuts_restants = floor($segons_restants / 60); // Convertim els segons restants a minuts
                $segons_restants = $segons_restants % 60; // Recuperem els segons restants després de calcular les hores

                $error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts_restants} minuts i {$segons_restants} segons.";
            } else { // Si l'usuari no està bloquejat

                if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) {
                    // Login correcte, resetajer intents
                    $stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?");
                    $stmt->execute([$usuari['id']]);

                    session_regenerate_id(true); // Protecció session fixation
                    $_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
                        'id' => $usuari['id'], // El seu ID a la BD
                        'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
                        'nom_complet' => $usuari['nom_complet'], // El nom complet
                        'rol'        => $usuari['rol'], // El seu rol (usuari, admin)
                        'autenticat' => true // Definim que està autenticat
                    ];

                    if ($recordar) { // En cas que volguem desar la sessió tot i tancar el navegador
                        // Generar un token únic i desar-lo a la BD
                        $token = bin2hex(random_bytes(16));
                        setcookie('recordar_id', $usuari['id'], time() + 30*24*60*60, '/'); // Recordem la ID de l'usuari
                        setcookie('recordar_token', $token, time() + 30*24*60*60, '/'); // Recordem el token desat a la BD

                        $stmt = $pdo->prepare("UPDATE usuaris SET token_recordar = ? WHERE id = ?");
                        $stmt->execute([$token, $usuari['id']]);
                    }

                    // Registrem l'acció "login"
                    registrar_activitat($pdo, $usuari['id'], 'login');
                    header('Location: privada.php');
                    exit;

                } else {
                    // Si la contrasenya és incorrecte, sumem un intent de login
                    $stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?");
                    $stmt->execute([$usuari['id']]);

                    // Registrar intent fallit
                    registrar_activitat($pdo, $usuari['id'], 'error-login');
                    $error = "Usuari o contrasenya incorrecte";
                }

            }

        } else {
            $error = "L'usuari no existeix";
        }
    }
} catch (PDOException $e) {
    $error = "Error en iniciar sessió. Motiu: " . $e->getMessage();
}
?>

<html>
<head>
    <meta lang="ca">
    <meta charset="UTF-8">
    <title>Inici de sessió</title>
    <link rel="stylesheet" href="./css/login.css">
</head>
<body>
    <div class="login-card">
        <h2>Login</h2>

        <?php if ($error): ?>
            <div class="error-container">
                <p style='color:red;'><?= $error ?></p>
            </div>
        <?php endif; ?>
        
        <form method="post">
            <div class="input-group">
                <label>Nom d'usuari:</label>
                <input type="text" name="nom_usuari" required>
                <a href="registre.php" class="helper-link">No tens usuari? Crea'n un</a>
            </div>

            <div class="input-group">
                <label>Contrasenya:</label>
                <input type="password" name="contrasenya" required>
                <a href="recuperacio.php" class="helper-link">He oblidat la contrasenya</a>
            </div>

            <div class="checkbox-group">
                <input type="checkbox" name="recordar" id="recordar">
                <label for="recordar">Recordar-me</label>
            </div>

            <input type="submit" value="Entra">
        </form>

        <div class="separator"><span>O també pots</span></div>

        <div class="mfa-section">
            <a href="mfa.php"><button type="button" class="btn-2fa">Iniciar sessió amb 2FA</button></a>
        </div>
    </div>
</body>
</html>