Auth-Umschalter: Session.php verzweigt auf AUTH_BACKEND (jwt = JWT-Access-Cookie + widerrufbare Redis-Session)
- teacher/admin Login/Read/Logout branchen auf AUTH_BACKEND; session-Pfad unveraendert (Live-Default, prod-sicher). - jwt: loginTeacher/loginAdmin legen Redis-Session (SessionStore) an + setzen kurzlebiges JWT-Cookie (ggs_t/ggs_a) + langes Sid-Cookie (ggs_ts/ggs_as). - Read: Fast-Path via signiertem JWT (zustandslos), Refresh-Path via Sid->Redis + frisches JWT; Widerruf = Redis DEL bei Logout. - Teacher/Admin getrennte Cookies (koexistieren), typ-Check gegen Rollenverwechslung. Lazy-require von Jwt/SessionStore (jeder Einstiegspfad). - Schueler unveraendert (UUID-Cookie + DB, schon pod-unabhaengig). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+123
-12
@@ -1,8 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* GeoGraSim — Session Management
|
||||
* Schueler: UUID-Cookie (anonym, kein Login)
|
||||
* Lehrkraefte: PHP-Session mit teacher_id
|
||||
* Schueler: UUID-Cookie (anonym, kein Login) — DB-gestuetzt, bereits pod-unabhaengig.
|
||||
* Lehrkraefte/Admin: je nach AUTH_BACKEND
|
||||
* 'session' (Default) → native PHP-Session ($_SESSION) wie bisher.
|
||||
* 'jwt' → JWT-Access-Cookie + widerrufbare Redis-Session (pod-unabhaengig).
|
||||
*
|
||||
* Der jwt-Zweig liegt VOLLSTAENDIG hinter AUTH_BACKEND. Solange 'session' gilt
|
||||
* (Live-Default), aendert sich nichts. Alle Endpoints sind ueber diese Klasse
|
||||
* kanalisiert, deshalb greift der Umschalter an EINER Stelle.
|
||||
*/
|
||||
|
||||
class Session {
|
||||
@@ -15,6 +21,108 @@ class Session {
|
||||
}
|
||||
}
|
||||
|
||||
// ── AUTH_BACKEND-Umschalter (JWT + Redis) ───────────────────────────
|
||||
private static function jwtMode(): bool {
|
||||
return defined('AUTH_BACKEND') && AUTH_BACKEND === 'jwt';
|
||||
}
|
||||
|
||||
/** Laedt die jwt-Bibliotheken lazy — nur wenn der jwt-Zweig aktiv ist.
|
||||
* So funktioniert jeder Einstiegspfad (Front-Controller ODER direkte
|
||||
* PHP-Datei), ohne dass der Bootstrap sie kennen muss. Die vendor-
|
||||
* Namespaces (Firebase\JWT, Predis) laedt der Composer-Autoloader,
|
||||
* den app.php ueberall einbindet. */
|
||||
private static function jwtBoot(): void {
|
||||
if (!class_exists('Jwt')) require_once __DIR__ . '/Jwt.php';
|
||||
if (!class_exists('SessionStore')) require_once __DIR__ . '/SessionStore.php';
|
||||
}
|
||||
|
||||
/** [Access-Cookie, Sid-Cookie, typ] je Rolle. Teacher und Admin sind
|
||||
* getrennt (wie bisher die getrennten $_SESSION-Keys), koennen also
|
||||
* im selben Browser koexistieren. */
|
||||
private static function roleCookies(string $role): array {
|
||||
return $role === 'admin'
|
||||
? ['ggs_a', 'ggs_as', 'admin']
|
||||
: ['ggs_t', 'ggs_ts', 'teacher'];
|
||||
}
|
||||
|
||||
private static function putCookie(string $name, string $val, int $expires): void {
|
||||
setcookie($name, $val, [
|
||||
'expires' => $expires,
|
||||
'path' => (defined('BASE_PATH') ? BASE_PATH : '') . '/',
|
||||
'samesite' => 'Lax',
|
||||
'secure' => defined('IS_PRODUCTION') ? IS_PRODUCTION : false,
|
||||
'httponly' => true,
|
||||
]);
|
||||
$_COOKIE[$name] = $val; // im selben Request sofort sichtbar
|
||||
}
|
||||
|
||||
private static function dropCookie(string $name): void {
|
||||
setcookie($name, '', [
|
||||
'expires' => 1,
|
||||
'path' => (defined('BASE_PATH') ? BASE_PATH : '') . '/',
|
||||
'samesite' => 'Lax',
|
||||
'secure' => defined('IS_PRODUCTION') ? IS_PRODUCTION : false,
|
||||
'httponly' => true,
|
||||
]);
|
||||
unset($_COOKIE[$name]);
|
||||
}
|
||||
|
||||
/** jwt-Login: Redis-Session anlegen + Sid-Cookie (lang) + Access-JWT (kurz). */
|
||||
private static function jwtLogin(string $role, int $sub): void {
|
||||
self::jwtBoot();
|
||||
[$acc, $sidC, $typ] = self::roleCookies($role);
|
||||
$sid = SessionStore::create(['typ' => $typ, 'sub' => $sub]);
|
||||
self::putCookie($sidC, $sid, time() + (defined('JWT_REFRESH_TTL') ? (int)JWT_REFRESH_TTL : 2592000));
|
||||
self::putCookie($acc, Jwt::issue(['sub' => $sub, 'typ' => $typ, 'sid' => $sid]),
|
||||
time() + (defined('JWT_TTL') ? (int)JWT_TTL : 900));
|
||||
}
|
||||
|
||||
/** jwt-Read: aktuelle User-ID fuer Rolle (oder null).
|
||||
* Fast-Path (gueltiges Access-Token) ist zustandslos ohne Redis-Hit;
|
||||
* erst wenn das kurzlebige Token abgelaufen ist, wird ueber das Sid-
|
||||
* Cookie die widerrufbare Redis-Session geprueft und ein frisches
|
||||
* Access-Token nachgelegt. */
|
||||
private static function jwtUserId(string $role): ?int {
|
||||
self::jwtBoot();
|
||||
[$acc, $sidC, $typ] = self::roleCookies($role);
|
||||
|
||||
// Fast-Path: gueltiges, signiertes Access-Token → kein Redis-Hit.
|
||||
$tok = $_COOKIE[$acc] ?? null;
|
||||
if ($tok) {
|
||||
$c = Jwt::verify($tok);
|
||||
if ($c && ($c['typ'] ?? '') === $typ && isset($c['sub'])) {
|
||||
return (int) $c['sub'];
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh-Path: Access-Token fehlt/abgelaufen → Sid-Cookie → Redis.
|
||||
$sid = $_COOKIE[$sidC] ?? null;
|
||||
if (!$sid) return null;
|
||||
try {
|
||||
$data = SessionStore::get($sid); // slidet TTL; null = widerrufen/abgelaufen
|
||||
} catch (\Throwable $e) {
|
||||
return null; // Redis nicht erreichbar → wie „nicht eingeloggt"
|
||||
}
|
||||
if (!$data || ($data['typ'] ?? '') !== $typ || !isset($data['sub'])) return null;
|
||||
|
||||
// frisches Access-Token nachlegen (nur solange keine Header raus sind)
|
||||
if (!headers_sent()) {
|
||||
self::putCookie($acc, Jwt::issue(['sub' => (int)$data['sub'], 'typ' => $typ, 'sid' => $sid]),
|
||||
time() + (defined('JWT_TTL') ? (int)JWT_TTL : 900));
|
||||
}
|
||||
return (int) $data['sub'];
|
||||
}
|
||||
|
||||
/** jwt-Logout: Redis-Session widerrufen + Cookies loeschen. */
|
||||
private static function jwtLogout(string $role): void {
|
||||
self::jwtBoot();
|
||||
[$acc, $sidC, ] = self::roleCookies($role);
|
||||
$sid = $_COOKIE[$sidC] ?? null;
|
||||
if ($sid) { try { SessionStore::revoke($sid); } catch (\Throwable $e) {} }
|
||||
self::dropCookie($acc);
|
||||
self::dropCookie($sidC);
|
||||
}
|
||||
|
||||
/** Schueler-Session-ID aus Cookie */
|
||||
public static function studentId(): ?string {
|
||||
return $_COOKIE[self::COOKIE_NAME] ?? null;
|
||||
@@ -69,12 +177,14 @@ class Session {
|
||||
|
||||
/** Lehrer eingeloggt? (startet die Session lazy — Aufrufer müssen nicht) */
|
||||
public static function teacherId(): ?int {
|
||||
if (self::jwtMode()) return self::jwtUserId('teacher');
|
||||
self::start();
|
||||
return $_SESSION['teacher_id'] ?? null;
|
||||
}
|
||||
|
||||
/** Lehrer-Login */
|
||||
public static function loginTeacher(int $teacherId): void {
|
||||
if (self::jwtMode()) { self::jwtLogin('teacher', $teacherId); return; }
|
||||
self::start();
|
||||
$_SESSION['teacher_id'] = $teacherId;
|
||||
}
|
||||
@@ -91,23 +201,26 @@ class Session {
|
||||
}
|
||||
|
||||
// ── Admin (Super-Admin nach 2FA) ────────────────────────────────────
|
||||
// Einheitlicher Zugang wie beim Lehrer, damit der spätere JWT+Redis-
|
||||
// Umschalter (AUTH_BACKEND) an EINER Stelle greift statt an jedem Endpoint.
|
||||
// Einheitlicher Zugang wie beim Lehrer, damit der JWT+Redis-Umschalter
|
||||
// (AUTH_BACKEND) an EINER Stelle greift statt an jedem Endpoint.
|
||||
|
||||
/** Admin eingeloggt (2FA abgeschlossen)? */
|
||||
public static function adminId(): ?int {
|
||||
if (self::jwtMode()) return self::jwtUserId('admin');
|
||||
self::start();
|
||||
return $_SESSION['admin_id'] ?? null;
|
||||
}
|
||||
|
||||
/** Admin-Login abschließen (nach erfolgreicher 2FA). */
|
||||
public static function loginAdmin(int $adminId): void {
|
||||
if (self::jwtMode()) { self::jwtLogin('admin', $adminId); return; }
|
||||
self::start();
|
||||
$_SESSION['admin_id'] = $adminId;
|
||||
}
|
||||
|
||||
/** Admin-Logout — inkl. offener 2FA-Phase (admin_pending). */
|
||||
/** Admin-Logout — inkl. offener 2FA-Phase (admin_pending liegt immer nativ). */
|
||||
public static function logoutAdmin(): void {
|
||||
if (self::jwtMode()) self::jwtLogout('admin');
|
||||
self::start();
|
||||
unset($_SESSION['admin_id'], $_SESSION['admin_pending']);
|
||||
}
|
||||
@@ -124,14 +237,12 @@ class Session {
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
if (self::jwtMode()) {
|
||||
self::jwtLogout('teacher');
|
||||
self::jwtLogout('admin');
|
||||
}
|
||||
self::start();
|
||||
session_destroy();
|
||||
setcookie(self::COOKIE_NAME, '', [
|
||||
'expires' => 1,
|
||||
'path' => BASE_PATH . '/',
|
||||
'samesite' => 'Lax',
|
||||
'secure' => IS_PRODUCTION,
|
||||
'httponly' => true,
|
||||
]);
|
||||
self::dropCookie(self::COOKIE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user