Logga in med sessioner och cookies

By . Latest revision .

Vi ska titta närmare på sessioner och cookies och hur man kan skapa login-funktionalitet med hjälp av dem tillsammans med en databas.

#Förutsättning

Du kan grunderna i php och du vet vad variabler, typer och funktioner innebär.
Du har kännedom om SQL och databashantering.
Du har gått igenom sessioner och cookies i artikeln “Kom i gång med PHP på 20 steg”.
Du har gjort övningen klasser för $_SESSION och $_COOKIE.

#En klass för sessionen

Vi återanvänder sessions-klassen vi skapade tidigare, i övningen klasser för $_SESSION och $_COOKIE.

Snabbt och lätt. Vad behöver vi mer? Jo, en databas…

#Databasen

Vi kör på en SQLite-databas för enkelhetens skull. Jag väljer att skapa databasen med hjälp av SQLite manager. Tabellen heter users och kolumnerna blir användarnamn och lösenord (name, pass).

Här är tanken att användarens namn ska lagras tillsammans med dennes krypterade lösenord. Vi ska sikta på att inte behöva hantera lösenordet i klartext, utan hela tiden jämföra den krypterade versionen med hjälp av php’s inbyggda funktioner.

Jag sparar databasen i mappen db som oophp.sqlite.

#Hantera lösenord

Php har bra inbyggt stöd för att hantera lösenord och kryptering. I en labb i kursen htmlphp testar vi på lite kryptering. Det är dags att öppna locket och damma av den kunskapen nu. Ett test-script kan se ut så här:

<?php 

$pass = "test123";

// Kryptera lösenordet. PASSWORD_DEFAULT använder den starkaste algoritmen.
$crypt_pass = password_hash($pass, PASSWORD_DEFAULT);

// Hur ser hashen ut då? Den ser lite olika ut varje gång man kör den. 
echo "hashed password: " . $crypt_pass;

// Utskriften blir på ett ungefär: hashed password: $2y$10$.f7Occu3KqJgOEY12VSsIOHMVdCx632Efa9hQQQnAoLwEWfpPgtnK

Det är alltså informationen i $crypt_pass som vi sparar i databasen. När vi sedan ska jämföra värdet, gör vi det på följande sätt:

$user_pass = "test123";

password_verify($user_pass, $crypt_pass); // Returnerar true om de är likadana, annars false.

Du kan läsa mer om password_hash här.

Puh…Det var det! Resten kvar.

#En klass för databaskopplingen

Vi kikar på en klass som hanterar databaskopplingen. Vi kör vidare med PDO som vi lärde oss i htmlphp. Om vi letar upp koden för anslutningen kan vi i stort sett stoppa den rakt in i en klass. På rätt plats förstås. En första ansats kan bli som följer:

<?php 

class Connect
{
    private $db;
    
    /**
     * Constructor
     * @param $dsn string The dsn to the database-file
     * @return void
     */
    public function __construct($dsn)
    {
        try {
            $db = new PDO($dsn);
            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->db = $db;
        } catch (PDOException $e) {
            echo "Failed to connect to the database using DSN:<br>$dsn<br>";
        }
    }
    
    /**
     * Adds user to the database
     * @param $user string The name of the user
     * @param $pass string The user's password
     * @return void
     */
    public function addUser($user, $pass)
    {
        $stmt = $this->db->prepare("INSERT into users (name, pass) VALUES ('$user', '$pass')");
        $stmt->execute();
    }
    
    /**
     * Gets the hashed password from the database
     * @param $user string The user to get password from/for
     * @return string The hashed password
     */
    public function getHash($user)
    {
        $stmt = $this->db->prepare("SELECT pass FROM users WHERE name='$user'");
        $stmt->execute();
        
        $res = $stmt->fetch(PDO::FETCH_ASSOC);
        
        return $res["pass"];
    }
    
    /**
     * Changes the password for a user
     * @param $user string The usr to change the password for
     * @param $pass string The password to change to
     * @return void
     */
    public function changePassword($user, $pass)
    {
        $stmt = $this->db->prepare("UPDATE users SET pass='$pass' WHERE name='$user'");
        $stmt->execute();
    }
    
    /**
     * Check if user exists in the database
     * @param $user string The user to search for
     * @return bool true if user exists, otherwise false
     */
    public function exists($user)
    {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE name='$user'");
        $stmt->execute();
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return !$row ? false : true;
    }
}

Inget nytt och inga konstigheter. Ta en extra minut och se över vad som händer i metoderna.

…58…59…60…klar? Bra, då går vi vidare.

#Filerna

Nu har vi många pusselbitar på plats för att skapa ett egen variant av login-system, som även blir hyfsat säkert. Nästa steg blir att skapa de nödvändiga filerna. Vi ställer oss i roten.

# Skapa filerna vi ska använda
$ touch login.php config.php validate.php create_user.php handle_new_user.php logout.php welcome.php 

Vad ska vi ha filerna till?

Fil Funktion
login.php Sidan som hanterar login-formuläret.
config.php Konfigurationsfil. Inkluderas i alla sidor.
validate.php Processing-sida för inloggningsformuläret.
create_user.php Formulär för skapande av användare
handle_new_user.php Processing-sida för ny användare.
logout.php Sidan som hanterar utloggningen.
welcome.php Sidan man enbart kan nå om man loggar in.

#login.php

Vi behöver ett formulär som användaren kan använda för inloggning. Formulärets metod kan med fördel vara POST då vi inte vill visa lösenordet i webbläsarens adressfält.

<!doctype html>
<head>
    <meta charet="utf-8">
    <title>Login</title>
</head>
<body>
    <form action="validate.php" method="POST">
        <table>
            <legend><h3>Login form</h3></legend>
            <tr>
                <td>Enter name:</td><td><input type="text" name="name"></td>
            </tr>
            <tr>
                <td>Enter pass:</td><td><input type="password" name="pass"></td>
            </tr>
            <tr>
                <td><input type="submit" name="submitForm" value="Login"></td>
            </tr>
        </table>
    </form>
</body>
</html>

#config.php

Nu är det ett bra läge att tänka på DRY kod. Vi kommer behöva ansluta till databsen samt starta sessionen ett flertal gånger. Vi lägger till det i config.php, så kan vi bara inkludera den i övriga filer. Smart.

<?php
// Include Session
require_once("Session.php");
require_once("Connect.php");

$fileName = __DIR__ . "/db/oophp.sqlite";
$db = new Connect("sqlite:$fileName");

// Start the session
$session = new Session("MYSESSION");

Nu har vi tillgång till $session och $db om vi vill. Vi går tillbaka till login.php och lägger till lite funktionalitet:

<?php
// Include config
require_once("config.php");

$user_loggedin = "";

// Make sure no one is logged in
if ($session->has("name")) {
    echo "<p>You are already logged in as " . $session->get('name') . "</p>";
    echo "<p><a href='logout.php'>Logout</a></p>";
    $user_loggedin = "disabled";
}

?>

Vi inkluderar sessionen och startar den. Vi gör sedan en kontroll om någon redan är inloggad och stänger då möjligheten att logga in igen. Lägg till $user_loggedin som attribut på input-elementen samt knappen:

...
<!-- $user_loggedin holds 'disabled' if someone is logged in -->
<td>Enter name:</td><td><input type="text" name="name" <?=$user_loggedin?>></td>
...
<td>Enter pass:</td><td><input type="password" name="pass" <?=$user_loggedin?>></td>
...
<td><input type="submit" name="submitForm" value="Login" <?=$user_loggedin?>></td>
...

Vi skickar formuläret till validate.php för vidare hantering.

#validate.php

Formuläret skickas hit, till validate.php för hantering av de inkomna variablerna. Här behöver vi vår databasklass och sessionsklassen.

<?php
// Include config
require_once("config.php");

// Handle incoming POST variables
$user_name = isset($_POST["name"]) ? htmlentities($_POST["name"]) : null;
$user_pass = isset($_POST["pass"]) ? htmlentities($_POST["pass"]) : null;


// Correspond according to input
// Check if both fields are filled
if ($user_name != null && $user_pass != null) {
    // Check if username exists
    if ($db->exists($user_name)) {
        $get_hash = $db->getHash($user_name);
        // Verify user password
        if (password_verify($user_pass, $get_hash)) {
            $session->set("name", $user_name);
            header("Location: welcome.php");
        } else {
            // Redirect to login.php
            echo "User name or password is incorrect. <a href='login.php'>Try again.</a>";
        }
    } else {
        // Redirect to login.php
        echo "No such user. <a href='login.php'>Try again.</a>";
    }
}
?>

Kika igenom koden och se hur klasserna underlättar hanteringen.

Bra. Nu kan vi logga in. Men vi måste ju ha någon användare i databasen. Vi ordnar en möjlighet att skapa en användare.

#create_user.php

Här passar det bra med ett formulär till. Vi skickar det till en egen processing-sida, handle_new_user.php. Man kan såklart trycka ihop viss kod och då minska antalet filer. Välj själv vad du tycker passar bäst.

Om vi återgår till användarskapandet kan filen se ut som följer. Ett enkelt formulär:

<!doctype html>
<head>
    <meta charet="utf-8">
    <title>Create user</title>
</head>
<body>

    <form action="handle_new_user.php" method="POST">
        <table>
            <legend><h3>Create user</h3></legend>
            <tr>
                <td>Enter name:</td><td><input type="text" name="new_name"></td>
            </tr>
            <tr>
                <td>Choose pass:</td><td><input type="password" name="new_pass"></td>
            </tr>
            <tr>
                <td>Re-enter pass:</td><td><input type="password" name="re_pass"></td>
            </tr>
            <tr>
                <td><input type="submit" name="submitCreateForm" value="Create"></td>
            </tr>
        </table>
    </form>
    <p><a href='login.php'>Back to login</a></p>
    
</body>
</html>

Vi ska skicka lösenordet så vi använder återigen metoden POST. Användaren måste även skriva in sitt lösenord två gånger. Sedan skickar vi informationen till handle_new_user.php.

#handle_new_user.php

Precis som i validate.php så använder vi här databasklassen och sessionsklassen för att styra upp hanteringen.

<?php
/// Include config
require_once("config.php");

// Handle incoming POST variables
$user_name = isset($_POST["new_name"]) ? htmlentities($_POST["new_name"]) : null;
$user_pass = isset($_POST["new_pass"]) ? htmlentities($_POST["new_pass"]) : null;
$re_user_pass = isset($_POST["re_pass"]) ? htmlentities($_POST["re_pass"]) : null;

// Check if username exists
if (!$db->exists($user_name)) {
    // Check passwords match
    if ($user_pass != $re_user_pass) {
        echo "Passwords do not match!";
        header("Refresh:2; create_user.php");
    } else {
        // Make a hash of the password
        $crypt_pass = password_hash($user_pass, PASSWORD_DEFAULT);
        
        // Add user to database
        $db->addUser($user_name, $crypt_pass);
        
        echo "<p>Successfully added " . $user_name . "!</p><p><a href='login.php'>Login</a></p>";
    }
} else {
    echo "User already exists! Choose another username.";
    header("Refresh:2; create_user.php");
}

Som du märker har vi en del kod återupprepas och som med fördel kan flyttas till en konfigurationsfil.

Man kan även hasha namn och lösenord tillsammans, $crypt_pass = password_hash($user_name . $user_pass, PASSWORD_DEFAULT);. Det blir säkrare och mer bundet till användaren. För stunden kan du göra som du vill.

Nu kan vi skapa ett par användare.

Vi kikar på hur tabellen kan se ut:

Nu har vi några användare med krypterade lösenord. Vi har även undvikit att hantera lösenordet i klartext. Om du någon gång klickar på “Glömt lösenordet?” och det skickas till dig i klartext, ska du fundera på hur säkerheten ser ut hos dem.

Tillbaka till ämnet! Vi måste ju kunna logga ut också.

#logout.php

Vi använder våra metoder i sessionsklassen. Vi gör några kontroller på om en användare är inloggad och om sessionen verkligen finns.

<?php
// Include config
require_once("config.php");

// Check if someone is logged in
if ($session->has("name")) {
    $session->destroy();
} else {
    echo "<p>No active user.</p>";
    echo "<p><a href='login.php'>Login again.</a></p>";
    die();
}

// Check if session is active
$has_session = session_status() == PHP_SESSION_ACTIVE;

if (!$has_session) {
    echo "<p>The session no longer exists. You have successfully logged out!</p>";
}

echo "<p>Destroyed session.</p>";

echo "<a href='login.php'>Login again.</a>";
?>

session_status() kan returnera tre saker.

Returvärde Betydelse
PHP_SESSION_DISABLED Sessioner är avstängda.
PHP_SESSION_NONE Sessioner är tillgängliga, men det finns ingen.
PHP_SESSION_ACTIVE Sessioner är tillgängliga och det finns en.

Bra, nu har vi koll på det. Nu återstår bara welcome.php. Tanken är att man inte ska kunna nå den om man inte är inloggad. Vi tar en titt på hur det kan se ut.

#welcome.php

Det är egentligen en kontroll som behövs. “Finns det en användare aktiv? Om inte - gör en redirect.”:

if (!$session->has("name")) {
    header("Location: login.php");
}

// Resten av sidan

Ett försök till en testsida kan se ut så här:

<?php
// Include config
require_once("config.php");

if (!$session->has("name")) {
    header("Location: login.php");
}

echo "<h1>Welcome!</h1>";

echo "<p>You are logged in as " . $session->get('name') . "</p>";

echo "<p><a href='info.php'>View session</a></p>";

echo "<p><a href='logout.php'>Logout</a></p>";

echo "<p><a href='change_password.php'>Change password</a></p>";

?>

Filen info.php skriver ut variabeln $_SESSION. Inga konstigheter.

Det finns även en möjlighet att byta lösenord om man är inloggad. Det rör den sista delen av artikeln.

#change_password.php

Vi har redan metoden klar i klassen Connect. Vi skapar filen change_password.php och tittar på hur det kan se ut i den:

<?php 
// Include config
require_once("config.php");

$user = $session->get("name");
$status = "Change password";

// Handle incoming POST variables
$old_pass = isset($_POST["old_pass"]) ? htmlentities($_POST["old_pass"]) : null;
$new_pass = isset($_POST["new_pass"]) ? htmlentities($_POST["new_pass"]) : null;
$re_pass = isset($_POST["re_pass"]) ? htmlentities($_POST["re_pass"]) : null;

// Check if all fields are filled
if ($old_pass != null && $new_pass != null && $re_pass != null) {
    // Check if old password is correct
    if (password_verify($old_pass, $db->getHash($user))) {
        // Check if new password matches
        if ($new_pass == $re_pass) {
                $crypt_pass = password_hash($new_pass, PASSWORD_DEFAULT);
                $db->changePassword($user, $crypt_pass);
                $status = "Password changed.";
        } else {
            $status = "The passwords do not match.";
        }
    } else {
        $status = "Old password is incorrect.";
    }
} else {
    $status = "All fields must be filled.";
}

?>

<!doctype html>
<head>
    <meta charet="utf-8">
    <title>Change password</title>
</head>
<body>
    <form action="change_password.php" method="POST">
        <table>
            <legend><h3><?=$status?></h3></legend>
            <tr>
                <td>Old pass:</td><td><input type="password" name="old_pass"></td>
            </tr>
            <tr>
                <td>New pass:</td><td><input type="password" name="new_pass"></td>
            </tr>
            <tr>
                <td>Re-enter pass:</td><td><input type="password" name="re_pass"></td>
            </tr>
            <tr>
                <td><input type="submit" name="submitForm" value="Change password"></td>
            </tr>
        </table>
    </form>
    <p><a href='login.php'>Back to login</a></p>
    
</body>
</html>

Här valde jag att ha allt i samma fil, men återigen - strukturera på ett sätt som fungerar för dig.

#Avslutningsvis

Tillslut har vi ett litet fungerande inloggningssystem. Om du följer artikeln - sikta på att göra koden DRY och återanvänd koden om möjligt.

#Revision history

  • 2017-03-13: (A, lew) First version.

Document source.

Category: oophp.