Authentifizierung von Benutzern
Wenig bis gar keine Webanwendungen benötigen keinen Mechanismus zur Benutzeranmeldung oder zur Überprüfung der Benutzerrechte. In diesem Kapitel werden wir darüber sprechen:
- Benutzeranmeldung und -abmeldung
- benutzerdefinierte Authentifikatoren und Autorisierer
→ Installation und Anforderungen
In den Beispielen verwenden wir ein Objekt der Klasse Nette\Security\User, das den aktuellen Benutzer
repräsentiert und das Sie durch Übergabe mittels Dependency
Injection erhalten. In Präsentatoren rufen Sie einfach $user = $this->getUser()
auf.
Authentifizierung
Authentifizierung bedeutet Benutzeranmeldung, d. h. der Vorgang, bei dem die Identität eines Benutzers überprüft
wird. Der Benutzer identifiziert sich in der Regel mit einem Benutzernamen und einem Passwort. Die Verifizierung erfolgt durch den
so genannten Authenticator. Wenn die Anmeldung fehlschlägt, wird
Nette\Security\AuthenticationException
ausgegeben.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
So wird der Benutzer abgemeldet:
$user->logout();
Und prüfen, ob der Benutzer angemeldet ist:
echo $user->isLoggedIn() ? 'yes' : 'no';
Ganz einfach, oder? Und alle Sicherheitsaspekte werden von Nette für Sie erledigt.
Im Presenter können Sie die Anmeldung mit der Methode startup()
überprüfen und einen nicht angemeldeten
Benutzer auf die Anmeldeseite umleiten.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Ablauf
Die Benutzeranmeldung läuft zusammen mit dem Ablauf des Repositorys ab, bei dem es
sich in der Regel um eine Session handelt (siehe die Einstellung für den Ablauf der
Session ). Sie können jedoch auch ein kürzeres Zeitintervall festlegen, nach dem der Benutzer abgemeldet wird. Zu diesem
Zweck wird die Methode setExpiration()
verwendet, die vor login()
aufgerufen wird. Geben Sie einen
String mit einer relativen Zeit als Parameter an:
// Die Anmeldung läuft nach 30 Minuten Inaktivität ab
$user->setExpiration('30 minutes');
// Abbruch des eingestellten Ablaufs
$user->setExpiration(null);
Die Methode $user->getLogoutReason()
gibt an, ob der Benutzer abgemeldet wurde, weil das Zeitintervall
abgelaufen ist. Sie gibt entweder die Konstante Nette\Security\UserStorage::LogoutInactivity
zurück, wenn die Zeit
abgelaufen ist, oder UserStorage::LogoutManual
, wenn die Methode logout()
aufgerufen wurde.
Authentifikator
Es handelt sich um ein Objekt, das die Anmeldedaten, d.h. in der Regel den Namen und das Passwort, überprüft. Die triviale Implementierung ist die Klasse Nette\Security\SimpleAuthenticator, die in der Konfiguration definiert werden kann:
security:
users:
# name: password
johndoe: secret123
kathy: evenmoresecretpassword
Diese Lösung ist eher für Testzwecke geeignet. Wir zeigen Ihnen, wie Sie einen Authentifikator erstellen, der die Anmeldedaten anhand einer Datenbanktabelle überprüft.
Ein Authenticator ist ein Objekt, das die Schnittstelle Nette\Security\Authenticator mit der Methode
authenticate()
implementiert. Seine Aufgabe ist es, entweder die so genannte Identität
zurückzugeben oder eine Exception Nette\Security\AuthenticationException
auszulösen. Es wäre auch möglich, einen
feinkörnigen Fehlercode Authenticator::IdentityNotFound
oder Authenticator::InvalidCredential
bereitzustellen.
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 Array von Rollen
['name' => $row->username],
);
}
}
Die Klasse MyAuthenticator kommuniziert mit der Datenbank über den Nette
Database Explorer und arbeitet mit der Tabelle users
, wobei die Spalte username
den Anmeldenamen des
Benutzers und die Spalte password
den Hash enthält. Nach der Überprüfung des Namens
und des Passworts gibt sie die Identität mit der ID des Benutzers, der Rolle (Spalte role
in der Tabelle), die wir
später erwähnen werden, und einem Array mit zusätzlichen Daten (in unserem Fall der Benutzername)
zurück.
Wir werden den Authenticator als Dienst des DI-Containers zur Konfiguration hinzufügen:
services:
- MyAuthenticator
$onLoggedIn, $onLoggedOut Ereignisse
Das Objekt Nette\Security\User
hat die Ereignisse
$onLoggedIn
und $onLoggedOut
, so dass Sie Rückrufe hinzufügen können, die nach einer erfolgreichen
Anmeldung oder nach der Abmeldung des Benutzers ausgelöst werden.
$user->onLoggedIn[] = function () {
// Benutzer hat sich gerade angemeldet
};
Identität
Eine Identität ist ein Satz von Informationen über einen Benutzer, der vom Authentifikator zurückgegeben wird und der dann
in einer Session gespeichert und mit $user->getIdentity()
abgerufen wird. Wir können also die ID, die Rollen und
andere Benutzerdaten so abrufen, wie wir sie im Authentifikator übergeben haben:
$user->getIdentity()->getId();
// funktioniert auch als Abkürzung $user->getId();
$user->getIdentity()->getRoles();
// Benutzerdaten können als Eigenschaften abgerufen werden
// der Name, den wir in MyAuthenticator übergeben haben
$user->getIdentity()->name;
Wichtig ist, dass beim Abmelden des Benutzers mit $user->logout()
die Identität nicht gelöscht wird
und weiterhin verfügbar ist. Wenn also die Identität vorhanden ist, garantiert sie allein nicht, dass der Benutzer auch
angemeldet ist. Wenn wir die Identität ausdrücklich löschen wollen, melden wir den Benutzer mit
logout(true)
ab.
Dadurch kann man zwar immer noch davon ausgehen, welcher Benutzer am Computer sitzt und z.B. personalisierte Angebote im E-Shop anzeigen, aber man kann seine persönlichen Daten erst nach dem Einloggen anzeigen.
Identity ist ein Objekt, das die Schnittstelle Nette\Security\IIdentity implementiert, die Standardimplementierung ist Nette\Security\SimpleIdentity. Wie bereits erwähnt, wird die Identität in der Session gespeichert. Wenn wir also zum Beispiel die Rolle eines angemeldeten Benutzers ändern, bleiben die alten Daten in der Identität erhalten, bis er sich erneut anmeldet.
Speicherung für angemeldete Benutzer
Die beiden grundlegenden Informationen über den Benutzer, d.h. ob er angemeldet ist und seine Identität, werden normalerweise in der Session gespeichert. Diese kann geändert werden. Für die
Speicherung dieser Informationen ist ein Objekt zuständig, das die Schnittstelle Nette\Security\UserStorage
implementiert. Es gibt zwei Standardimplementierungen, von denen die erste die Daten in einer Session und die zweite in einem
Cookie überträgt. Dies sind die Klassen Nette\Bridges\SecurityHttp\SessionStorage
und CookieStorage
.
Sie können die Speicherung auswählen und sie sehr bequem in der Konfiguration Sicherheit ›
Authentifizierung konfigurieren.
Sie können auch genau steuern, wie das Speichern (sleep) und Wiederherstellen (wakeup) der Identität erfolgen
soll. Alles, was Sie brauchen, ist, dass der Authentifikator die Schnittstelle Nette\Security\IdentityHandler
implementiert. Diese hat zwei Methoden: sleepIdentity()
wird aufgerufen, bevor die Identität in den Speicher
geschrieben wird, und wakeupIdentity()
wird aufgerufen, nachdem die Identität gelesen wurde. Die Methoden können
den Inhalt der Identität ändern oder sie durch ein neues Objekt ersetzen, das zurückgegeben wird. Die Methode
wakeupIdentity()
kann sogar null
zurückgeben, wodurch der Benutzer abgemeldet wird.
Als Beispiel zeigen wir eine Lösung für eine häufige Frage, wie man Identitätsrollen direkt nach der Wiederherstellung aus
einer Session aktualisiert. In der Methode wakeupIdentity()
übergeben wir die aktuellen Rollen an die Identität, z.
B. aus der Datenbank:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// hier können Sie die Identität vor dem Speichern nach dem Einloggen ändern,
// aber das brauchen wir jetzt nicht
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// Aktualisieren der Rollen in der Identität
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
Und nun kehren wir zur Cookie-basierten Speicherung zurück. Sie ermöglicht es, eine Website zu erstellen, auf der sich die
Benutzer anmelden können, ohne dass sie Sessionen verwenden müssen. Es muss also nicht auf die Festplatte geschrieben werden. So
funktioniert ja auch die Website, die Sie gerade lesen, einschließlich des Forums. In diesem Fall ist die Implementierung von
IdentityHandler
eine Notwendigkeit. Wir werden nur ein zufälliges Token, das den angemeldeten Benutzer
repräsentiert, im Cookie speichern.
Also stellen wir zunächst in der Konfiguration mit security › authentication › storage: cookie
den
gewünschten Speicherplatz ein.
Wir fügen eine Spalte authtoken
in der Datenbank hinzu, in der jeder Benutzer eine völlig zufällige, eindeutige und nicht ermittelbare Zeichenfolge von ausreichender
Länge (mindestens 13 Zeichen) hat. Das Repository CookieStorage
speichert nur den Wert
$identity->getId()
im Cookie, so dass wir in sleepIdentity()
die ursprüngliche Identität durch
einen Proxy mit authtoken
in der ID ersetzen, im Gegensatz dazu stellen wir in der Methode
wakeupIdentity()
die gesamte Identität aus der Datenbank gemäß authtoken wieder her:
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 prüfen
...
// wir geben die Identität mit allen Daten aus der Datenbank zurück
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// wir geben eine Proxy-Identität zurück, bei der die ID authtoken ist
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// Ersetzen Sie die Proxy-Identität durch eine vollständige Identität, 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 Authentifizierungen
Es ist möglich, mehrere unabhängige angemeldete Benutzer innerhalb einer Site und jeweils eine Session zu haben. Wenn wir z. B. eine getrennte Authentifizierung für Frontend und Backend haben wollen, legen wir einfach einen eindeutigen Sessionsnamensraum für jeden von ihnen fest:
$user->getStorage()->setNamespace('backend');
Es ist zu beachten, dass dieser an allen Stellen, die zum selben Segment gehören, gesetzt werden muss. Wenn wir Presenter verwenden, setzen wir den Namespace im gemeinsamen Vorfahren – normalerweise dem BasePresenter. Zu diesem Zweck erweitern wir die Methode checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Mehrere Authentifikatoren
Die Aufteilung einer Anwendung in Segmente mit unabhängiger Authentifizierung erfordert in der Regel unterschiedliche
Authenticators. Die Registrierung von zwei Klassen, die Authenticator implementieren, in Konfigurationsdiensten würde jedoch
einen Fehler auslösen, da Nette nicht wüsste, welche von ihnen mit dem Nette\Security\User
Objekt autowired werden sollte. Deshalb müssen wir das Autowiring für diese Klassen mit
autowired: self
einschränken, so dass es nur aktiviert wird, wenn die Klasse speziell angefordert wird:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Wir müssen unseren Authenticator nur auf das User-Objekt setzen, bevor wir die Methode login() aufrufen, was typischerweise im Callback des Login-Formulars bedeutet:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};