Tutorial PHP

Creare form di registrazione e login sicuro in PHP e MySQL

Creare form di registrazione e login sicuro in PHP e MySQL

Tra le funzionalità di un'applicazione web non può mancare l'accesso ad un'area protetta tramite login e password. L'implementazione di questo tipo di funzionalità porta con sè implicazioni riguardanti soprattutto la sicurezza.

In questo tutorial vedremo come creare un form di registrazione e login usando PHP e MySQL e lo faremo adottando le migliori pratiche in tema di sicurezza web.

Nota: Si consiglia di utilizzare il codice presente in questo tutorial solamente a scopo didattico, in quanto non rappresenta una soluzione completa da utilizzare in produzione, ma un modo per far comprendere il meccanismo e le logiche di sicurezza che stanno dietro ad un sistema di login in PHP

Il codice sorgente di questo tutorial è disponibile per il download a questo indirizzo.

La struttura dei file e delle cartelle è la seguente:

  • css
    • style.css
  • php
    • dashboard.php
    • database.php
    • login.php
    • logout.php
    • register.php
  • sql
    • users.sql
  • login.html
  • register.html

Modulo di registrazione

Il primo passo è la creazione del modulo di registrazione per consentire l'inserimento degli utenti. Il modulo che andremo a creare sarà molto semplice e avrà due soli campi: username e password.

Il nome utente deve essere univoco, ossia non devono esserci due utenti con lo stesso username, dunque faremo una verifica in fase d'inserimento per evitare di memorizzare utenti doppioni.

Di seguito è mostrato il codice HTML per il modulo di registrazione

<!DOCTYPE html>
<html>
    <head>
        <title>Registrazione</title>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap">
        <link rel="stylesheet" href="/css/style.css">
    </head>
    <body>
        <form method="post" action="/php/register.php">
            <h1>Registrazione</h1>
            <input type="text" id="username" placeholder="Username" name="username" maxlength="50" required>
            <input type="password" id="password" placeholder="Password" name="password" required>
            <button type="submit" name="register">Registrati</button>
        </form>
    </body>
</html>

Abbiamo utilizzato l'attributo HTML5 required per obbligare l'utente a compilare i dati del modulo, in quanto sono necessari per il funzionamento dell'applicazione.

Modulo di login

Allo stesso modo creiamo il modulo per il login utente. Anche in questo caso sono necessari solamente due campi: username e password.

Di seguito è mostrato il relativo codice HTML

<!DOCTYPE html>
<html>
    <head>
        <title>Login</title>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap">
        <link rel="stylesheet" href="/css/style.css">
    </head>
    <body>
        <form method="post" action="/php/login.php">
            <h1>Login</h1>
            <input type="text" id="username" placeholder="Username" name="username">
            <input type="password" id="password" placeholder="Password" name="password">
            <button type="submit" name="login">Accedi</button>
        </form>
    </body>
</html>

Aggiungiamo un pò di stile

Anche se non necessario per lo scopo del tutorial, con poche regole CSS possiamo rendere i nostri moduli esteticamente più gradevoli e accattivanti.

Il seguente codice è contenuto nel file style.css che viene richiamato alla riga 5 nei file di login e registrazione, tramite il tag <link>

html,body {
    padding: 0;
    margin: 0;
    font: 14px 'Open sans', sans-serif;
    color: #333;
}
h1 {
    margin: 0;
    margin-bottom: 20px;
	font-size: 24px;
}
form {
    padding: 40px;
    background: #DDD;
    border: 4px solid #AAA;
    width: 360px;
    margin: 30px auto;
}
input {
	font: 14px 'Open Sans', sans-serif;
    border: 2px solid #ccc;
    padding: 6px;
    display: block;
	margin-bottom: 10px;
    width: 96%;
}
button {
    font: 14px 'Open Sans', sans-serif;
    background-color: #4285f4;
    color: white;
    padding: 6px;
    cursor: pointer;
    border: none;
    margin-top: 10px;
	text-transform: uppercase;
}

Tabella utenti

Adesso che abbiamo l'HTML necessario per mostrare i due moduli, passiamo alla fase di creazione della tabella nel database che dovrà contenere gli utenti registrati.

Inanzitutto dobbiamo creare il database, che chiameremo test. Se abbiamo installato XAMPP possiamo aggiungere il database dall'apposita sezione "Database" di phpMyAdmin, come mostrato di seguito

Creare nuovo database in phpMyAdmin

Successivamente andiamo nella tab "SQL" ed eseguiamo il seguente codice SQL

CREATE TABLE `users` (
    `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `username` varchar(50) NOT NULL,
    `password` varchar(255) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci AUTO_INCREMENT=1;

Con questo codice abbiamo creato una tabella di nome users, coi seguenti campi:

  • id di tipo INT senza segno e di lunghezza 11
  • username di tipo VARCHAR e lunghezza 50
  • password di tipo VARCHAR e lunghezza 255

La lunghezza del campo password è motivata dal fatto che tale attributo non conterrà la password così come l'utente la digita in fase di registrazione ma, come vedremo più avanti, verrà salvato l'hash di tale password.

Tramite la keyword UNIQUE KEY diciamo a MySQL che il campo username deve essere univoco.

Tramite la keyword AUTO_INCREMENT diciamo a MySQL che il campo id deve incrementarsi in maniera automatica dopo ogni inserimento.

Adesso non ci resta che creare l'ultima parte del nostro sistema: il codice PHP che consentirà di effettuare registrazione e autenticazione all'area riservata.

Connessione al database

Per poter interagire con la base dati abbiamo bisogno di effettuare una connessione al database MySQL. Effettueremo questa operazione tramite la potente estensione PDO messa a disposizione dal linguaggio.

Il codice contenuto nel file database.php è il seguente

<?php
$config = [
    'db_engine' => 'mysql',
    'db_host' => '127.0.0.1',
    'db_name' => 'test',
    'db_user' => 'root',
    'db_password' => '',
];

$db_config = $config['db_engine'] . ":host=".$config['db_host'] . ";dbname=" . $config['db_name'];

try {
    $pdo = new PDO($db_config, $config['db_user'], $config['db_password'], [
        PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"
    ]);
        
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e) {
    exit("Impossibile connettersi al database: " . $e->getMessage());
}

Spiegazione del codice:

  • Alla riga 2 definiamo un array associativo contenente i dati per la connessione al database.
  • Alla riga 12 effettuiamo una connessione al database. Se questa non va a buon fine viene lanciata un eccezione di tipo PDOException.

Possiamo includere lo script in ciascuna pagina PHP che necessita di connessione al database tramite l'istruzione require_once(), come spiegato più avanti.

Nota: per comodità è stato utilizzato l'utente root senza password. È consigliabile creare un utente con password e con privilegi ristretti alle sole operazioni di INSERT e SELECT.

Registrazione utente

<?php
require_once('database.php');

if (isset($_POST['register'])) {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';
    $isUsernameValid = filter_var(
        $username,
        FILTER_VALIDATE_REGEXP, [
            "options" => [
                "regexp" => "/^[a-z\d_]{3,20}$/i"
            ]
        ]
    );
    $pwdLenght = mb_strlen($password);
    
    if (empty($username) || empty($password)) {
        $msg = 'Compila tutti i campi %s';
    } elseif (false === $isUsernameValid) {
        $msg = 'Lo username non è valido. Sono ammessi solamente caratteri 
                alfanumerici e l\'underscore. Lunghezza minina 3 caratteri.
                Lunghezza massima 20 caratteri';
    } elseif ($pwdLenght < 8 || $pwdLenght > 20) {
        $msg = 'Lunghezza minima password 8 caratteri.
                Lunghezza massima 20 caratteri';
    } else {
        $password_hash = password_hash($password, PASSWORD_BCRYPT);

        $query = "
            SELECT id
            FROM users
            WHERE username = :username
        ";
        
        $check = $pdo->prepare($query);
        $check->bindParam(':username', $username, PDO::PARAM_STR);
        $check->execute();
        
        $user = $check->fetchAll(PDO::FETCH_ASSOC);
        
        if (count($user) > 0) {
            $msg = 'Username già in uso %s';
        } else {
            $query = "
                INSERT INTO users
                VALUES (0, :username, :password)
            ";
        
            $check = $pdo->prepare($query);
            $check->bindParam(':username', $username, PDO::PARAM_STR);
            $check->bindParam(':password', $password_hash, PDO::PARAM_STR);
            $check->execute();
            
            if ($check->rowCount() > 0) {
                $msg = 'Registrazione eseguita con successo';
            } else {
                $msg = 'Problemi con l\'inserimento dei dati %s';
            }
        }
    }
    
    printf($msg, '<a href="../register.html">torna indietro</a>');
}

Dopo aver verificato che il modulo è stato compilato, vengono eseguiti diversi check per verificare la correttezza dei dati:

  1. Alla riga 19 verifichiamo tramite la funzione filter_var() la correttezza dello username. Sono ammessi caratteri alfanumerici e underscore. Inoltre verifichiamo che la lunghezza sia compresa tra 3 e 20 caratteri.
  2. Alla riga 23 facciamo un controllo sulla lunghezza della password: una password troppo corta infatti non andrebbe bene in quanto debole, una password troppo lunga causerebbe un eccessivo consumo di risorse nella generazione dell'hash (vedi nota importante).

Infine, alla riga 41 controlliamo che l'utente non sia già registrato.

Se il controllo ha esito negativo le informazioni inviate dall'utente vengono salvate nel database.

Alla riga 20 abbiamo utilizzato la funzione bindParam() di PDO, che consente di filtrare automaticamente i dati da passare alla query prima che venga processata da PDO.

In questo modo viene effettuato l'escape dell'input esterno evitando potenziali attacchi di tipo SQL injection.

Nelle righe 5 e 6 verifichiamo che le variabili contenute nell'array $_POST siano inizializzate tramite l'operatore di confronto null coalesce, introdotto con PHP 7.

Nota importante

Poiché non bisogna mai memorizzare una password in chiaro, alla riga 13 eseguiamo un hashing della password così com'è. Un hash non è altro che una stringa di lunghezza fissa ottenuta mediante l'applicazione di una funzione di hash alla stringa di partenza.

Una delle 3 caratteristiche fondamentali di una funzione di hash sicura è quella di saper generare hash dai quali risulta impossibile risalire all'originale, se non per tentativi (attacco a forza bruta).

Per generare l'hash, PHP mette a disposizione la funzione password_hash(), il cui secondo parametro indica l'algoritmo che si occuperà della generazione.

Tramite la costante PASSWORD_BCRYPT utilizziamo la funzione di hash bcrypt, che risulta molto resistente ad attacchi a forza bruta per via della sua "lentezza" nel trasformare una stringa nel suo hash corrispondente.

Login utente

Una volta che si è registrato, dobbiamo consentire all'utente di accedere all'area riservata.

Di seguito è mostrato il codice necessario alla verifica dell'autenticazione utente

<?php
session_start();
require_once('database.php');

if (isset($_SESSION['session_id'])) {
    header('Location: dashboard.php');
    exit;
}

if (isset($_POST['login'])) {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';
    
    if (empty($username) || empty($password)) {
        $msg = 'Inserisci username e password %s';
    } else {
        $query = "
            SELECT username, password
            FROM users
            WHERE username = :username
        ";
        
        $check = $pdo->prepare($query);
        $check->bindParam(':username', $username, PDO::PARAM_STR);
        $check->execute();
        
        $user = $check->fetch(PDO::FETCH_ASSOC);
        
        if (!$user || password_verify($password, $user['password']) === false) {
            $msg = 'Credenziali utente errate %s';
        } else {
            session_regenerate_id();
            $_SESSION['session_id'] = session_id();
            $_SESSION['session_user'] = $user['username'];
            
            header('Location: dashboard.php');
            exit;
        }
    }
    
    printf($msg, '<a href="../login.html">torna indietro</a>');
}

Spiegazione del codice:

  1. Alla riga 1 inizializziamo una sessione, poiché se il login darà esito positivo sarà necessario memorizzare alcune variabili di sessione.
  2. Alla riga 5 viene controllato se l'utente è già loggato, in caso positivo viene reindirizzato alla pagina protetta dashboard.php
  3. Nelle righe 10-30 viene verificata l'autenticazione utente.
  4. Alla riga 26 viene eseguita la query memorizzata nella variabile $query, che preleva i dati associati al nome utente inserito in fase di login.
  5. Alla riga 28 viene effettuato un doppio controllo: se l'utente non esiste oppure la password inserita è errata, il login fallisce.
  6. Se il login va buon fine, viene rigenerato il SESSION ID e memorizzate due variabili di sessione contenenti una lo username e l'altra l'id della sessione corrente.

Nota: Per la verifica delle password non possiamo utilizzare l'operatore di confronto ==, piuttosto è necessario utilizzare la funzione password_verify(), in quanto consente di confrontare due hash.

Pagine ad accesso riservato

Concludiamo questo tutorial sulla creazione di un sistema di registrazione e login sicuro, analizzando il codice PHP della pagina ad accesso riservato

<?php
session_start();

if (isset($_SESSION['session_id'])) {
    $session_user = htmlspecialchars($_SESSION['session_user'], ENT_QUOTES, 'UTF-8');
    $session_id = htmlspecialchars($_SESSION['session_id']);
    
    printf("Benvenuto %s, il tuo session ID è %s", $session_user, $session_id);
    echo "<br>";
    printf("%s", '<a href="logout.php">logout</a>');
} else {
    printf("Effettua il %s per accedere all'area riservata", '<a href="../login.html">login</a>');
}

Spiegazione del codice:

  • Lo script verifica (riga 4) che la variabile $_SESSION['session_id'] sia stata inizializzata. In caso positivo viene mostrato un messaggio di benvenuto.
  • Nelle righe 5-6 facciamo un escape delle variabili di sessione tramite la funzione PHP htmlspecialchars(), che consente di prevenire attacchi di tipo XSS convertendo eventuali tag HTML presenti nella stringa (lo username proviene da un input utente inserito nel form di registrazione).

Di seguito il codice che effettua la disconnessione dall'area riservata

<?php
session_start();
session_destroy();
header('Location: ../login.html');
exit;

Lo script distrugge semplicemente la sessione tramite la funzione session_destroy() ed effettua un redirect alla pagina di login.

Ricordiamoci che session_destroy() elimina tutti i dati memorizzati in una sessione collegati ad uno specifico PHPSESSID. Se avessimo avuto altre variabili di sessione, avremmo eliminato anche quelle.

Per evitare questo problema possiamo usare la funzione unset() che rimuove solamente l'elemento passato come argomento

<?php
session_start();

if (isset($_SESSION['session_id'])) {
    unset($_SESSION['session_id']);
}
header('Location: ../login.html');
exit;

Nota: tutte le pagine in cui vengono gestite variabili di sessione devono avere in testa allo scipt la funzione session_start()

Nota: Poiché durante le fasi di registrazione e login vengono scambiate informazioni sensibili come la password utente, è buona norma installare un certificato SSL/TLS per rendere la connessione sicura tra client e server.

Conclusioni

In questo tutorial abbiamo imparato a creare un sistema base di registrazione e accesso utente tramite PHP. Dopo aver compreso le basi di questo sistema, è possibile creare una logica più complessa che consente agli utenti di confermare la registrazione tramite verifica via email o di reimpostare la password.

Inoltre è possibile aggiungere alcuni controlli per rendere più robusta l'applicazione come ad esempio la verifica della robustezza della password o l'inserimento di un captcha per evitare registrazioni automatiche da parte dei bot.