Benutzeranmeldung (Authentifizierung)
Kaum eine Webanwendung kommt ohne einen Mechanismus zur Benutzeranmeldung und Überprüfung von Benutzerberechtigungen aus. In diesem Kapitel werden wir über Folgendes sprechen:
- An- und Abmelden von Benutzern
- Eigene Authentifikatoren
→ Installation und Anforderungen
In den Beispielen verwenden wir das Objekt der Klasse Nette\Security\User, das den aktuellen Benutzer
repräsentiert und auf das Sie zugreifen können, indem Sie es sich mittels Dependency Injection übergeben lassen. In
Presentern genügt es, $user = $this->getUser()
aufzurufen.
Authentifizierung
Authentifizierung bedeutet Benutzeranmeldung, also der Prozess, bei dem überprüft wird, ob der Benutzer wirklich
derjenige ist, für den er sich ausgibt. Üblicherweise weist er sich mit Benutzernamen und Passwort aus. Die Überprüfung führt
der sogenannte autentikátor durch. Schlägt die Anmeldung fehl, wird eine
Nette\Security\AuthenticationException
geworfen.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Benutzername oder Passwort ist falsch');
}
Auf diese Weise melden Sie den Benutzer ab:
$user->logout();
Und die Feststellung, ob er angemeldet ist:
echo $user->isLoggedIn() ? 'ja' : 'nein';
Sehr einfach, nicht wahr? Und alle Sicherheitsaspekte übernimmt Nette für Sie.
In Presentern können Sie die Anmeldung in der Methode startup()
überprüfen und einen nicht angemeldeten
Benutzer zur Anmeldeseite weiterleiten.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Ablauf (Expiration)
Die Benutzeranmeldung läuft zusammen mit dem Ablauf des Speichers ab, der
normalerweise die Session ist (siehe Einstellung Session-Ablauf). Es kann jedoch auch ein kürzeres
Zeitintervall festgelegt werden, nach dessen Ablauf der Benutzer abgemeldet wird. Dazu dient die Methode
setExpiration()
, die vor login()
aufgerufen wird. Geben Sie als Parameter eine Zeichenkette mit einer
relativen Zeit an:
// Anmeldung läuft nach 30 Minuten Inaktivität ab
$user->setExpiration('30 minutes');
// Aufhebung der eingestellten Expiration
$user->setExpiration(null);
Ob der Benutzer aufgrund des Ablaufs des Zeitintervalls abgemeldet wurde, verrät die Methode
$user->getLogoutReason()
, die entweder die Konstante Nette\Security\UserStorage::LogoutInactivity
(Zeitlimit abgelaufen) oder UserStorage::LogoutManual
(mit der Methode logout()
abgemeldet)
zurückgibt.
Authenticator
Dies ist ein Objekt, das die Anmeldedaten überprüft, also normalerweise Name und Passwort. Eine triviale Form ist die Klasse Nette\Security\SimpleAuthenticator, die wir in der Konfiguration definieren können:
security:
users:
# Benutzername: Passwort
frantisek: geheimespasswort
katka: nochgeheimerespasswort
Diese Lösung eignet sich eher für Testzwecke. Wir zeigen Ihnen, wie Sie einen Authenticator erstellen, der Anmeldedaten anhand einer Datenbanktabelle überprüft.
Ein Authenticator ist ein Objekt, das das Interface Nette\Security\Authenticator mit der Methode
authenticate()
implementiert. Ihre Aufgabe ist es, entweder eine sogenannte Identität
zurückzugeben oder eine Ausnahme Nette\Security\AuthenticationException
zu werfen. Es wäre auch möglich, einen
Fehlercode anzugeben, um die entstandene Situation genauer zu unterscheiden: Authenticator::IdentityNotFound
und
Authenticator::InvalidCredential
.
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
public function __construct(
private Nette\Database\Explorer $database,
private Nette\Security\Passwords $passwords,
) {
}
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('Benutzer nicht gefunden.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Ungültiges Passwort.');
}
return new SimpleIdentity(
$row->id,
$row->role, // oder ein Array mehrerer Rollen
['name' => $row->username],
);
}
}
Die Klasse MyAuthenticator kommuniziert mit der Datenbank über Nette
Database Explorer und arbeitet mit der Tabelle users
, wobei in der Spalte username
der Anmeldename
des Benutzers und in der Spalte password
der Passwort-Hash
gespeichert ist. Nach Überprüfung von Name und Passwort gibt sie die Identität zurück, die die ID des Benutzers, seine Rolle
(Spalte role
in der Tabelle), über die wir später mehr sprechen werden, und ein Array mit
weiteren Daten (in unserem Fall der Benutzername) enthält.
Den Authenticator fügen wir noch zur Konfiguration als Dienst des DI-Containers hinzu:
services:
- MyAuthenticator
Ereignisse $onLoggedIn, $onLoggedOut
Das Objekt Nette\Security\User
hat die Ereignisse
$onLoggedIn
und $onLoggedOut
. Sie können also Callbacks hinzufügen, die nach erfolgreicher Anmeldung
bzw. nach Abmeldung des Benutzers aufgerufen werden.
$user->onLoggedIn[] = function () {
// Benutzer wurde gerade angemeldet
};
Identität
Die Identität repräsentiert eine Sammlung von Informationen über den Benutzer, die vom Authenticator zurückgegeben wird und
anschließend in der Session gespeichert und mit $user->getIdentity()
abgerufen wird. Wir können also die ID,
Rollen und weitere Benutzerdaten abrufen, so wie wir sie im Authenticator übergeben haben:
$user->getIdentity()->getId();
// funktioniert auch als Abkürzung $user->getId();
$user->getIdentity()->getRoles();
// Benutzerdaten sind als Eigenschaften verfügbar
// Name, den wir in MyAuthenticator übergeben haben
$user->getIdentity()->name;
Wichtig ist, dass bei der Abmeldung mit $user->logout()
die Identität nicht gelöscht wird und
weiterhin verfügbar ist. Auch wenn der Benutzer eine Identität hat, muss er also nicht angemeldet sein. Wenn wir die Identität
explizit löschen möchten, melden wir den Benutzer mit dem Aufruf logout(true)
ab.
Dadurch können Sie weiterhin davon ausgehen, welcher Benutzer am Computer ist, und ihm beispielsweise im E-Shop personalisierte Angebote anzeigen. Seine persönlichen Daten können Sie ihm jedoch erst nach der Anmeldung anzeigen.
Die Identität ist ein Objekt, das das Interface Nette\Security\IIdentity implementiert, die Standardimplementierung ist Nette\Security\SimpleIdentity. Und wie erwähnt, wird sie in der Session gehalten. Wenn wir also beispielsweise die Rolle eines angemeldeten Benutzers ändern, bleiben die alten Daten in seiner Identität bis zu seiner erneuten Anmeldung erhalten.
Speicher für angemeldete Benutzer
Die beiden grundlegenden Informationen über den Benutzer, nämlich ob er angemeldet ist und seine identita, werden in der Regel in der Session übertragen. Dies kann jedoch geändert werden. Für die
Speicherung dieser Informationen ist ein Objekt verantwortlich, das das Interface Nette\Security\UserStorage
implementiert. Es stehen zwei Standardimplementierungen zur Verfügung: die erste überträgt Daten in der Session und die zweite
in einem Cookie. Dies sind die Klassen Nette\Bridges\SecurityHttp\SessionStorage
und CookieStorage
. Sie
können den Speicher auswählen und ihn sehr bequem in der Konfiguration security › authentication konfigurieren.
Weiterhin können Sie beeinflussen, wie genau das Speichern der Identität (sleep) und das Wiederherstellen
(wakeup) ablaufen soll. Es genügt, wenn der Authenticator das Interface Nette\Security\IdentityHandler
implementiert. Dieses hat zwei Methoden: sleepIdentity()
wird vor dem Schreiben der Identität in den Speicher
aufgerufen und wakeupIdentity()
nach dem Lesen daraus. Die Methoden können den Inhalt der Identität ändern oder
sie durch ein neues Objekt ersetzen, das sie zurückgeben. Die Methode wakeupIdentity()
kann sogar null
zurückgeben, wodurch der Benutzer abgemeldet wird.
Als Beispiel zeigen wir die Lösung einer häufigen Frage, wie die Rollen in der Identität sofort nach dem Laden aus der
Session aktualisiert werden können. In der Methode wakeupIdentity()
übergeben wir der Identität die aktuellen
Rollen, z. B. aus der Datenbank:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// hier kann die Identität vor dem Schreiben in den Speicher nach der Anmeldung geändert werden,
// aber das brauchen wir jetzt nicht
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// Aktualisierung der Rollen in der Identität
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
Und nun kehren wir zum Cookie-basierten Speicher zurück. Er ermöglicht es Ihnen, eine Website zu erstellen, auf der sich
Benutzer anmelden können, ohne Sessions zu benötigen. Das heißt, es muss nicht auf die Festplatte geschrieben werden. Übrigens
funktioniert auch die Website, die Sie gerade lesen, einschließlich des Forums, auf diese Weise. In diesem Fall ist die
Implementierung von IdentityHandler
eine Notwendigkeit. Im Cookie speichern wir nämlich nur ein zufälliges Token,
das den angemeldeten Benutzer repräsentiert.
Zuerst stellen wir also in der Konfiguration den gewünschten Speicher mit
security › authentication › storage: cookie
ein.
In der Datenbank erstellen wir eine Spalte authtoken
, in der jeder Benutzer eine völlig zufällige, einzigartige und nicht erratbare Zeichenkette ausreichender
Länge (mindestens 13 Zeichen) hat. Der CookieStorage
-Speicher überträgt im Cookie nur den Wert
$identity->getId()
, sodass wir in sleepIdentity()
die ursprüngliche Identität durch eine
Platzhalter-Identität mit authtoken
in der ID ersetzen. Umgekehrt lesen wir in der Methode
wakeupIdentity()
anhand des Authtokens die gesamte Identität aus der Datenbank:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
// Passwort überprüfen
...
// Identität mit allen Daten aus der Datenbank zurückgeben
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// Platzhalter-Identität zurückgeben, wobei die ID das Authtoken ist
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// Platzhalter-Identität durch die vollständige Identität ersetzen, wie in authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Mehrere unabhängige Anmeldungen
Es ist möglich, innerhalb einer Website und einer Session mehrere unabhängige angemeldete Benutzer gleichzeitig zu haben. Wenn wir beispielsweise auf einer Website eine getrennte Authentifizierung für die Administration und den öffentlichen Bereich haben möchten, reicht es aus, jeder einen eigenen Namen zuzuweisen:
$user->getStorage()->setNamespace('backend');
Es ist wichtig zu bedenken, dass wir den Namespace immer an allen Stellen festlegen, die zu dem jeweiligen Teil gehören. Wenn wir Presenter verwenden, legen wir den Namespace im gemeinsamen Vorfahren für den jeweiligen Teil fest – normalerweise im BasePresenter. Wir tun dies, indem wir die Methode checkRequirements() erweitern:
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Mehrere Authentifikatoren
Die Aufteilung der Anwendung in Teile mit unabhängiger Anmeldung erfordert meistens auch unterschiedliche Authentifikatoren.
Sobald wir jedoch in der Dienstkonfiguration zwei Klassen registrieren, die Authenticator implementieren, wüsste Nette nicht,
welche davon dem Objekt Nette\Security\User
automatisch zugewiesen werden soll, und würde einen Fehler anzeigen.
Daher müssen wir für Authentifikatoren das Autowiring
einschränken, sodass es nur funktioniert, wenn jemand eine bestimmte Klasse anfordert, z. B. FrontAuthenticator, was wir durch
die Wahl autowired: self
erreichen:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Den Authenticator des User-Objekts legen wir vor dem Aufruf der Methode login() fest, also normalerweise im Code des Formulars, das ihn anmeldet:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};