`, auf der `cache`-Redis-Verbindung * (`config('database.redis.cache')`, standardmäßig Datenbank 1). phpredis legt * darüber transparent noch `REDIS_PREFIX` * (`config('database.redis.options.prefix')`, auf dieser Installation * `clupilot-database-`, aus `APP_NAME` abgeleitet) — unsichtbar für jeden * PHP-Aufruf über diese Verbindung, aber Teil des tatsächlichen Schlüssels für * jeden Client, der nicht über phpredis mit derselben Option spricht. Der * Container muss also denselben Wert voranstellen; er kann ihn aus dieser * Klasse allein nicht erraten, deshalb steht er hier. */ final class TicketStore { /** * Dreißig Sekunden, weil ein Ticket nur den Weg vom Klick zum offenen * Fenster überbrücken muss. Wer die Seite offen liegen lässt und später neu * lädt, bekommt ein frisches. */ public const TTL_SECONDS = 30; private const PREFIX = 'terminal:ticket:'; /** * Legt eine Sitzung ab und gibt den Schlüssel zurück, den der Browser * bekommt: zweiunddreißig Byte Zufall, sonst nichts. * * Alles, was die Brücke wirklich braucht — Adresse, Benutzer, privater * Schlüssel, gepinnter Fingerabdruck — liegt serverseitig daneben und wird * vom Container gelesen, nie vom Browser mitgegeben. Ein Ticket, das die * Verbindungsdaten selbst trüge, stünde in der Adresszeile, im Verlauf und * in jedem Protokoll dazwischen. * * @param array $payload */ public static function issue(array $payload): string { $ticket = bin2hex(random_bytes(32)); Redis::connection('cache')->setex( self::PREFIX.$ticket, self::TTL_SECONDS, json_encode($payload, JSON_THROW_ON_ERROR), ); return $ticket; } /** * Liest das Ticket und löscht es im selben Zug — mit Redis' eigenem * `GETDEL`, nicht mit zwei Aufrufen. Erst das macht „genau eine Einlösung" * zu einer Zusage, die auch unter zwei gleichzeitigen Versuchen hält. * * @return array|null */ public static function redeem(string $ticket): ?array { $raw = Redis::connection('cache')->getdel(self::PREFIX.$ticket); // phpredis meldet ein fehlendes/abgelaufenes/schon geholtes Ticket als // `false`, nicht als `null` — die Cache-Fassade glättete das vorher. return $raw === false || $raw === null ? null : json_decode((string) $raw, true, flags: JSON_THROW_ON_ERROR); } }