úprava index.php - přihlášen / nepřihlášen

This commit is contained in:
stepan
2026-03-17 16:02:57 +01:00
parent a116b30df7
commit 79c2a13266
2 changed files with 266 additions and 248 deletions
+250 -241
View File
@@ -1,261 +1,270 @@
<?php <?php
// ============================================================ // ============================================================
// HLAVNÍ SOUBOR AUTENTIZACE // PŘIHLAŠOVACÍ STRÁNKA
// ============================================================
// Tento soubor vložíš na začátek každé stránky, kterou chceš
// chránit přihlášením:
//
// require_once 'auth/auth.php';
//
// Po jeho načtení máš k dispozici tyto proměnné:
//
// $auth_prihlasen ... true / false (vždy nastaveno)
// $auth_uzivatel['id'] ... ID přihlášeného uživatele (nebo null)
// $auth_uzivatel['email'] ... email přihlášeného uživatele (nebo null)
// $auth_uzivatel['admin'] ... true / false (vždy nastaveno)
// $auth_logout_html ... HTML formulář s tlačítkem odhlášení,
// nebo prázdný řetězec pokud není přihlášen
//
// Použití odhlašovacího tlačítka na chráněné stránce:
//
// echo $auth_logout_html;
//
// ============================================================ // ============================================================
// Zpracovává přihlášení z dvou zdrojů:
// 1) Vlastní formulář na této stránce (login.php)
// 2) Formulář $auth_login_html vložený na jiné stránce
// (v tom případě přijde v POST i redirect_url)
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php'; require_once __DIR__ . '/db.php';
// ------------------------------------------------------------
// SPUŠTĚNÍ SESSION
// ------------------------------------------------------------
// Nastavíme parametry session cookie ještě PŘED session_start().
// HttpOnly = JavaScript nemůže cookie číst (ochrana před XSS)
// SameSite = cookie se neposílá při požadavcích z jiných webů
// (ochrana před CSRF)
// Secure = cookie se posílá jen přes HTTPS (pokud web běží na HTTPS)
session_name(SESSION_NAZEV); session_name(SESSION_NAZEV);
session_set_cookie_params([ session_set_cookie_params([
'lifetime' => 0, // 0 = cookie platí do zavření prohlížeče 'lifetime' => 0,
// (trvalost zajišťuje remember me, ne session)
'path' => '/', 'path' => '/',
'httponly' => true, // JavaScript k cookie nemá přístup 'httponly' => true,
'samesite' => 'Strict', // ochrana před CSRF 'samesite' => 'Strict',
// 'secure' => true, // odkomentuj pokud web běží na HTTPS // 'secure' => true,
]); ]);
session_start(); session_start();
// ------------------------------------------------------------ // Pokud je uživatel již přihlášen, přesměrujeme ho rovnou dál
// VÝCHOZÍ STAV uživatel není přihlášen if (isset($_SESSION['uzivatel_id'])) {
// ------------------------------------------------------------ header('Location: ' . AUTH_REDIRECT_PO_PRIHLASENI);
// Tyto hodnoty jsou nastaveny vždy při každém načtení stránky.
// Teprve níže se případně přepíší na true / skutečné hodnoty.
$auth_prihlasen = false;
$auth_uzivatel = [
'id' => null,
'email' => null,
'admin' => false,
];
$auth_logout_html = '';
// ------------------------------------------------------------
// KROK 1: Existuje platná session?
// ------------------------------------------------------------
// Session je nejrychlejší způsob ověření nevyžaduje dotaz do DB.
// Session data jsou uložena na serveru, uživatel je nemůže zfalšovat.
if (isset($_SESSION['uzivatel_id']) && isset($_SESSION['email'])) {
// Kontrola expirace session (nečinnost)
if (isset($_SESSION['posledni_aktivita']) &&
(time() - $_SESSION['posledni_aktivita']) > SESSION_EXPIRACE) {
// Session vypršela zrušíme ji
$_SESSION = [];
session_destroy();
} else {
// Session je platná uživatel je přihlášen
$auth_prihlasen = true;
$auth_uzivatel = [
'id' => $_SESSION['uzivatel_id'],
'email' => $_SESSION['email'],
// Přetypování na bool ochrana pro případ, že by v session
// byla jiná hodnota než true/false
'admin' => (bool) $_SESSION['admin'],
];
// Aktualizujeme čas poslední aktivity
$_SESSION['posledni_aktivita'] = time();
}
}
// ------------------------------------------------------------
// KROK 2: Session neexistuje zkusíme remember me cookie
// ------------------------------------------------------------
// Cookie obsahuje pouze selector a token. Nikdy neobsahuje
// heslo, ID uživatele ani příznak admin. Vše citlivé je na serveru.
if (!$auth_prihlasen &&
isset($_COOKIE['auth_selector']) &&
isset($_COOKIE['auth_token'])) {
$cookie_selector = $_COOKIE['auth_selector'];
$cookie_token = $_COOKIE['auth_token'];
// Vyhledáme záznam v DB podle selectoru (přesná shoda, rychlé)
$stmt = $pdo->prepare("
SELECT
t.id AS token_id,
t.uzivatel_id,
t.token_hash,
t.expiruje,
u.email,
u.admin
FROM `" . DB_TABULKA_TOKENY . "` t
JOIN `" . DB_TABULKA_UZIVATELE . "` u ON u.id = t.uzivatel_id
WHERE t.selector = :selector
LIMIT 1
");
$stmt->execute([':selector' => $cookie_selector]);
$zaznam = $stmt->fetch();
$cookie_ok = false;
if ($zaznam) {
// Zkontrolujeme, zda token ještě nevypršel
if (time() < strtotime($zaznam['expiruje'])) {
// Ověříme tajný token pomocí password_verify().
// Tato funkce záměrně trvá stejně dlouho bez ohledu
// na výsledek chrání před timing útoky.
if (password_verify($cookie_token, $zaznam['token_hash'])) {
$cookie_ok = true;
}
}
}
if ($cookie_ok) {
// Cookie je platná obnovíme session
// Regenerujeme session ID (ochrana před session fixation)
session_regenerate_id(true);
$_SESSION['uzivatel_id'] = $zaznam['uzivatel_id'];
$_SESSION['email'] = $zaznam['email'];
$_SESSION['admin'] = (bool) $zaznam['admin'];
$_SESSION['posledni_aktivita'] = time();
// Vygenerujeme CSRF token pokud ještě neexistuje
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$auth_prihlasen = true;
$auth_uzivatel = [
'id' => $zaznam['uzivatel_id'],
'email' => $zaznam['email'],
'admin' => (bool) $zaznam['admin'],
];
// Prodloužíme platnost tokenu v DB i v cookie
$nova_expirace = time() + REMEMBER_EXPIRACE;
$nova_expirace_dt = date('Y-m-d H:i:s', $nova_expirace);
$stmt2 = $pdo->prepare("
UPDATE `" . DB_TABULKA_TOKENY . "`
SET `expiruje` = :expiruje
WHERE `id` = :id
");
$stmt2->execute([
':expiruje' => $nova_expirace_dt,
':id' => $zaznam['token_id'],
]);
// Prodloužíme cookie v prohlížeči
setcookie('auth_selector', $cookie_selector, [
'expires' => $nova_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
setcookie('auth_token', $cookie_token, [
'expires' => $nova_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
// Smažeme staré (expirované) tokeny z DB příležitostně
$stmt3 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_TOKENY . "`
WHERE `expiruje` < :ted
");
$stmt3->execute([':ted' => date('Y-m-d H:i:s')]);
} else {
// Cookie je neplatná nebo expirovaná smažeme ji
if ($zaznam) {
$stmt4 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_TOKENY . "`
WHERE `id` = :id
");
$stmt4->execute([':id' => $zaznam['token_id']]);
}
setcookie('auth_selector', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
]);
setcookie('auth_token', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
]);
}
}
// ------------------------------------------------------------
// KROK 3: Rozhodnutí přesměrovat nebo pokračovat?
// ------------------------------------------------------------
if (!$auth_prihlasen && VYZADOVAT_PRIHLASENI) {
// Uložíme URL aktuální stránky pro přesměrování po přihlášení
$aktualni_url = $_SERVER['REQUEST_URI'] ?? '';
if (!empty($aktualni_url)) {
$_SESSION['redirect_po_prihlaseni'] = $aktualni_url;
}
header('Location: ' . AUTH_LOGIN_URL);
exit; exit;
} }
// ------------------------------------------------------------ // ------------------------------------------------------------
// PŘIPRAVENÝ HTML KÓD PRO ODHLAŠOVACÍ TLAČÍTKO // CSRF TOKEN
// ------------------------------------------------------------ // ------------------------------------------------------------
// Na chráněné stránce stačí napsat:
//
// echo $auth_logout_html;
//
// Pokud uživatel není přihlášen, proměnná je prázdný řetězec
// a nevypíše se nic.
if ($auth_prihlasen) { if (empty($_SESSION['csrf_token'])) {
$auth_logout_html = $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
'<form method="POST" action="' . htmlspecialchars(AUTH_LOGOUT_URL) . '">'
. '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($_SESSION['csrf_token'] ?? '') . '">'
. '<button type="submit">Odhlásit se</button>'
. '</form>';
} }
$csrf_token = $_SESSION['csrf_token'];
// ------------------------------------------------------------
// ZPRACOVÁNÍ FORMULÁŘE
// ------------------------------------------------------------
$chyba = '';
$email_hodnota = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// -- Ověření CSRF tokenu ----------------------------------
$csrf_z_formulare = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrf_token, $csrf_z_formulare)) {
$chyba = 'Neplatný požadavek. Zkuste stránku obnovit a přihlásit se znovu.';
}
if (empty($chyba)) {
$email = trim($_POST['email'] ?? '');
$heslo = $_POST['heslo'] ?? '';
$zapamatovat = isset($_POST['zapamatovat']);
$email_hodnota = htmlspecialchars($email);
if (empty($email) || empty($heslo)) {
$chyba = 'Zadejte email a heslo.';
}
}
if (empty($chyba)) {
// -- Kontrola brute force -----------------------------
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$okno_od = date('Y-m-d H:i:s', time() - BRUTE_OKNO);
$stmt = $pdo->prepare("
SELECT COUNT(*) AS pocet
FROM `" . DB_TABULKA_BRUTE . "`
WHERE (`ip_adresa` = :ip OR `email` = :email)
AND `cas` > :okno_od
");
$stmt->execute([
':ip' => $ip,
':email' => $email,
':okno_od' => $okno_od,
]);
$pocet_pokusu = $stmt->fetch()['pocet'];
if ($pocet_pokusu >= BRUTE_MAX_POKUSU) {
$chyba = 'Příliš mnoho neúspěšných pokusů. Zkuste to prosím za chvíli.';
}
}
if (empty($chyba)) {
// -- Ověření emailu a hesla v DB ----------------------
$stmt = $pdo->prepare("
SELECT `id`, `email`, `heslo`, `admin`
FROM `" . DB_TABULKA_UZIVATELE . "`
WHERE `email` = :email
LIMIT 1
");
$stmt->execute([':email' => $email]);
$uzivatel = $stmt->fetch();
// I když uživatel neexistuje, zavoláme password_verify na fiktivní
// hash aby útočník nemohl podle doby odpovědi poznat, zda email existuje
$fiktivni_hash = '$2y$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ01234';
$hash_k_overeni = $uzivatel ? $uzivatel['heslo'] : $fiktivni_hash;
$heslo_ok = password_verify($heslo, $hash_k_overeni);
if (!$uzivatel || !$heslo_ok) {
// Zapíšeme neúspěšný pokus
$stmt2 = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_BRUTE . "`
(`ip_adresa`, `email`)
VALUES
(:ip, :email)
");
$stmt2->execute([
':ip' => $ip,
':email' => $email,
]);
$chyba = 'Nesprávný email nebo heslo.';
} else {
// -- Přihlášení úspěšné ---------------------------
session_regenerate_id(true);
$_SESSION['uzivatel_id'] = $uzivatel['id'];
$_SESSION['email'] = $uzivatel['email'];
$_SESSION['admin'] = (bool) $uzivatel['admin'];
$_SESSION['posledni_aktivita'] = time();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// -- Remember me ----------------------------------
if ($zapamatovat) {
$selector = bin2hex(random_bytes(16));
$token = bin2hex(random_bytes(32));
$token_hash = password_hash($token, PASSWORD_DEFAULT);
$expiruje = date('Y-m-d H:i:s', time() + REMEMBER_EXPIRACE);
$stmt3 = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_TOKENY . "`
(`uzivatel_id`, `selector`, `token_hash`, `expiruje`)
VALUES
(:uzivatel_id, :selector, :token_hash, :expiruje)
");
$stmt3->execute([
':uzivatel_id' => $uzivatel['id'],
':selector' => $selector,
':token_hash' => $token_hash,
':expiruje' => $expiruje,
]);
$cookie_expirace = time() + REMEMBER_EXPIRACE;
setcookie('auth_selector', $selector, [
'expires' => $cookie_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
setcookie('auth_token', $token, [
'expires' => $cookie_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
}
// -- Přesměrování po přihlášení -------------------
// Priorita zdrojů pro redirect URL:
// 1) redirect_url z POST (formulář byl na jiné stránce)
// 2) redirect_po_prihlaseni v session (uloženo v auth.php)
// 3) výchozí stránka z config.php
$redirect_z_post = trim($_POST['redirect_url'] ?? '');
$redirect_ze_session = $_SESSION['redirect_po_prihlaseni'] ?? '';
// Bezpečnostní kontrola redirect URL:
// Povolujeme pouze relativní URL začínající lomítkem,
// aby útočník nemohl přesměrovat na cizí web
// (tzv. open redirect útok)
if (!empty($redirect_z_post) && str_starts_with($redirect_z_post, '/')) {
$redirect = $redirect_z_post;
} elseif (!empty($redirect_ze_session) && str_starts_with($redirect_ze_session, '/')) {
$redirect = $redirect_ze_session;
} else {
$redirect = AUTH_REDIRECT_PO_PRIHLASENI;
}
unset($_SESSION['redirect_po_prihlaseni']);
header('Location: ' . $redirect);
exit;
}
}
}
// ------------------------------------------------------------
// HTML VÝSTUP
// ------------------------------------------------------------
?>
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Přihlášení <?php echo htmlspecialchars(PROJEKT_NAZEV); ?></title>
<?php if (AUTH_CSS !== ''): ?>
<link rel="stylesheet" href="<?php echo htmlspecialchars(AUTH_CSS); ?>">
<?php endif; ?>
</head>
<body>
<h1>Přihlášení</h1>
<h2><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h2>
<?php if (!empty($chyba)): ?>
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p>
<?php endif; ?>
<form method="POST" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<p>
<label for="email">Email:</label><br>
<input
type="email"
id="email"
name="email"
value="<?php echo $email_hodnota; ?>"
required
autocomplete="email"
>
</p>
<p>
<label for="heslo">Heslo:</label><br>
<input
type="password"
id="heslo"
name="heslo"
required
autocomplete="current-password"
>
</p>
<p>
<label>
<input type="checkbox" name="zapamatovat" value="1">
Zapamatovat si mě
</label>
</p>
<p>
<button type="submit">Přihlásit se</button>
</p>
</form>
<?php if (REGISTRACE_OTEVRENA): ?>
<p><a href="<?php echo htmlspecialchars(AUTH_REGISTRACE_URL); ?>">Nemáš účet? Zaregistruj se.</a></p>
<?php endif; ?>
<p><a href="<?php echo htmlspecialchars(AUTH_RESET_URL); ?>">Zapomněl jsi heslo?</a></p>
</body>
</html>
+15 -6
View File
@@ -1,12 +1,21 @@
<?php <?php require_once 'auth/auth.php'; ?>
require_once 'auth/auth.php';
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="cs"> <html lang="cs">
<head><meta charset="UTF-8"><title>Chráněná stránka</title></head> <head><meta charset="UTF-8"><title>Ukázka</title></head>
<body> <body>
<p>Přihlášen: <?php echo htmlspecialchars($auth_uzivatel['email']); ?></p>
<?php if ($auth_prihlasen): ?>
<p>Přihlášen: <strong><?php echo htmlspecialchars($auth_uzivatel['email']); ?></strong></p>
<?php echo $auth_logout_html; ?> <?php echo $auth_logout_html; ?>
<p>Zde je chráněný obsah.</p> <p>Toto vidí pouze přihlášený uživatel.</p>
<?php else: ?>
<p>Pro zobrazení obsahu se přihlas:</p>
<?php echo $auth_login_html; ?>
<?php endif; ?>
</body> </body>
</html> </html>