Chapter 17 ยท Databases & Forms
Sessions & Login
HTTP has amnesia: every page load is a brand-new conversation, and PHP forgets you completely between requests. Yet somehow websites remember you're logged in as you click around. The trick is called a session โ and it's the final skill we need before the project.
Think of a cloakroom at an event. You hand in your coat and receive a
numbered ticket. The cloakroom keeps a shelf for each ticket number. Later, you show the
ticket, and they instantly find your shelf. A session works the same way: PHP keeps a
private shelf on the server for each visitor and gives the browser a
ticket (a cookie with a random session ID). On every request the browser shows
the ticket, and PHP fetches the right shelf into $_SESSION.
Session basics
page1.php<?php
session_start(); // ALWAYS the first line, before any HTML output
$_SESSION["favourite"] = "nshima"; // put something on my shelf
echo "Saved! Now visit page2.php";
page2.php<?php
session_start(); // reopen my shelf
echo "You told me earlier your favourite food is: " . $_SESSION["favourite"];
Two different pages, two different requests โ and the value survived. Rules to burn in:
session_start()must run on every page that touches$_SESSION, and before any HTML output (even a blank line before<?phpcan break it with the famous "headers already sent" error).$_SESSIONis a normal associative array โ read and write it freely.- Data lives on the server; the browser holds only the meaningless ticket number.
Storing passwords โ never, ever in plain text
Before building login we must store passwords correctly. If your database ever leaks (it happens to giants), plain-text passwords hand every account to the attacker โ including accounts reused on email and banking. The professional standard is hashing:
Hashing is like cooking. Turning eggs, flour and sugar into a cake is easy;
turning the cake back into eggs is impossible. password_hash() cooks the password
into an unrecognisable string. At login we don't "uncook" it โ we cook the newly typed password
the same way and check whether the two cakes match, using password_verify().
// When creating a user (registration):
$hash = password_hash("secret123", PASSWORD_DEFAULT);
echo $hash;
// Store $hash in the database โ never the real password
// When logging in:
$typed = "secret123";
if (password_verify($typed, $hash)) {
echo "<br>Correct password!";
} else {
echo "<br>Wrong password.";
}
Never store plain passwords. Never use md5() or sha1() for passwords
(old tutorials still show this โ they are crackable in seconds today). Only
password_hash() / password_verify(). The hash column in your table
should be VARCHAR(255) to fit current and future hash formats.
A users table
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert an admin user. Generate the hash with PHP first:
-- echo password_hash("admin123", PASSWORD_DEFAULT);
INSERT INTO users (username, password_hash)
VALUES ('admin', '$2y$10$PASTE_THE_HASH_YOU_GENERATED_HERE');
The complete login flow
login.php<?php
session_start();
require "Database.php";
$error = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$username = trim($_POST["username"] ?? "");
$password = $_POST["password"] ?? "";
$pdo = Database::connect();
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(); // one row, or false if no such user
if ($user && password_verify($password, $user["password_hash"])) {
session_regenerate_id(true); // fresh ticket after login (security habit)
$_SESSION["user_id"] = $user["id"]; // THE moment of logging in:
$_SESSION["username"] = $user["username"];// remember who this shelf belongs to
header("Location: dashboard.php"); // send the browser onward
exit; // always exit after a redirect
} else {
$error = "Wrong username or password."; // same message for both cases โ on purpose!
}
}
?>
<!DOCTYPE html>
<html>
<body>
<h1>Login</h1>
<?php if ($error): ?><p style="color:red"><?= $error ?></p><?php endif; ?>
<form method="post">
<label>Username: <input type="text" name="username"></label><br>
<label>Password: <input type="password" name="password"></label><br>
<button type="submit">Log in</button>
</form>
</body>
</html>
If the message said "no such username", attackers could probe which usernames exist and then attack only the passwords. One vague message reveals nothing. Small detail, professional habit.
Protecting pages
Being "logged in" simply means your shelf contains a user_id. So a protected page just checks the shelf and evicts strangers. Put the check in one small file and require it at the top of every private page:
auth-check.php<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: login.php");
exit;
}
dashboard.php<?php
require "auth-check.php"; // strangers never get past this line
echo "Welcome back, " . htmlspecialchars($_SESSION["username"]) . "!";
Logging out
Logging out = destroying the shelf and the ticket:
logout.php<?php
session_start();
$_SESSION = []; // empty the shelf
session_destroy(); // throw the shelf away
header("Location: login.php");
exit;
Build a tiny page counter: a page that increments $_SESSION["visits"] on every
refresh and prints "You have visited this page N times." (Hint: ?? gives a starting
value of 0 on the first visit.)
Show solution
<?php
session_start();
$_SESSION["visits"] = ($_SESSION["visits"] ?? 0) + 1;
echo "You have visited this page " . $_SESSION["visits"] . " times.";
Where must session_start() appear?
How does password_verify() check a password if hashes can't be reversed?
- Sessions defeat HTTP's amnesia: a server-side shelf per visitor, found via a ticket cookie.
session_start()first line, every session page; then$_SESSIONis a normal array.- Passwords:
password_hash()to store,password_verify()to check. Never plain text, never md5. - Login = verify, then store
user_idin the session and redirect (withexit). - Protect pages with one shared
auth-check.php; logout = empty + destroy the session. - Use one vague "wrong username or password" message โ don't help attackers probe.