Σύνδεση χρηστών (Αυθεντικοποίηση)
Σχεδόν καμία web εφαρμογή δεν μπορεί να λειτουργήσει χωρίς μηχανισμό σύνδεσης χρηστών και επαλήθευσης των δικαιωμάτων τους. Σε αυτό το κεφάλαιο θα μιλήσουμε για:
- σύνδεση και αποσύνδεση χρηστών
- προσαρμοσμένους αυθεντικοποιητές
Στα παραδείγματα θα χρησιμοποιούμε το αντικείμενο της κλάσης Nette\Security\User, το οποίο
αντιπροσωπεύει τον τρέχοντα χρήστη και στο οποίο μπορείτε να
αποκτήσετε πρόσβαση ζητώντας να σας περαστεί μέσω dependency injection. Στους presenters
αρκεί απλώς να καλέσετε $user = $this->getUser()
.
Αυθεντικοποίηση
Με τον όρο αυθεντικοποίηση εννοείται η σύνδεση χρηστών, δηλαδή η
διαδικασία κατά την οποία επαληθεύεται αν ο χρήστης είναι όντως αυτός
που ισχυρίζεται ότι είναι. Συνήθως αποδεικνύεται με όνομα χρήστη και
κωδικό πρόσβασης. Η επαλήθευση γίνεται από τον λεγόμενο Αυθεντικοποιητή. Εάν η σύνδεση αποτύχει,
δημιουργείται μια εξαίρεση Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Το όνομα χρήστη ή ο κωδικός πρόσβασης είναι λανθασμένος');
}
Με αυτόν τον τρόπο αποσυνδέετε τον χρήστη:
$user->logout();
Και η διαπίστωση ότι είναι συνδεδεμένος:
echo $user->isLoggedIn() ? 'ναι' : 'όχι';
Πολύ απλό, έτσι δεν είναι; Και όλες τις πτυχές ασφαλείας τις διαχειρίζεται το Nette για εσάς.
Στους presenters μπορείτε να επαληθεύσετε τη σύνδεση στη μέθοδο
startup()
και να ανακατευθύνετε τον μη συνδεδεμένο χρήστη στη
σελίδα σύνδεσης.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Λήξη
Η σύνδεση του χρήστη λήγει μαζί με τη λήξη του αποθηκευτικού χώρου, ο οποίος
είναι συνήθως η session (βλ. ρύθμιση λήξης session). Ωστόσο, μπορεί να
οριστεί και ένα μικρότερο χρονικό διάστημα, μετά την παρέλευση του
οποίου ο χρήστης αποσυνδέεται. Γι' αυτό χρησιμεύει η μέθοδος
setExpiration()
, η οποία καλείται πριν από την login()
. Ως παράμετρο
δώστε μια συμβολοσειρά με σχετικό χρόνο:
// η σύνδεση λήγει μετά από 30 λεπτά αδράνειας
$user->setExpiration('30 minutes');
// ακύρωση της καθορισμένης λήξης
$user->setExpiration(null);
Το αν ο χρήστης αποσυνδέθηκε λόγω λήξης του χρονικού διαστήματος
αποκαλύπτει η μέθοδος $user->getLogoutReason()
, η οποία επιστρέφει είτε
τη σταθερά Nette\Security\UserStorage::LogoutInactivity
(έληξε το χρονικό όριο) είτε
UserStorage::LogoutManual
(αποσυνδέθηκε με τη μέθοδο logout()
).
Αυθεντικοποιητής
Πρόκειται για ένα αντικείμενο που επαληθεύει τα διαπιστευτήρια σύνδεσης, δηλαδή συνήθως το όνομα και τον κωδικό πρόσβασης. Μια απλή μορφή είναι η κλάση Nette\Security\SimpleAuthenticator, την οποία μπορούμε να ορίσουμε στη διαμόρφωση:
security:
users:
# όνομα: κωδικός πρόσβασης
frantisek: tajneheslo
katka: jestetajnejsiheslo
Αυτή η λύση είναι κατάλληλη κυρίως για δοκιμαστικούς σκοπούς. Θα δείξουμε πώς να δημιουργήσετε έναν αυθεντικοποιητή που θα επαληθεύει τα διαπιστευτήρια σύνδεσης έναντι ενός πίνακα βάσης δεδομένων.
Ο αυθεντικοποιητής είναι ένα αντικείμενο που υλοποιεί τη διεπαφή Nette\Security\Authenticator με τη
μέθοδο authenticate()
. Ο ρόλος της είναι είτε να επιστρέψει τη λεγόμενη
ταυτότητα είτε να δημιουργήσει μια εξαίρεση
Nette\Security\AuthenticationException
. Θα ήταν επίσης δυνατό να αναφερθεί ένας
κωδικός σφάλματος για λεπτομερέστερη διάκριση της κατάστασης που
προέκυψε: Authenticator::IdentityNotFound
και 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('User not found.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Invalid password.');
}
return new SimpleIdentity(
$row->id,
$row->role, // ή ένας πίνακας με περισσότερους ρόλους
['name' => $row->username],
);
}
}
Η κλάση MyAuthenticator επικοινωνεί με τη βάση δεδομένων μέσω του Nette Database Explorer και εργάζεται με τον πίνακα
users
, όπου στη στήλη username
βρίσκεται το όνομα σύνδεσης του
χρήστη και στη στήλη password
το αποτύπωμα του κωδικού πρόσβασης. Μετά
την επαλήθευση του ονόματος και του κωδικού πρόσβασης, επιστρέφει την
ταυτότητα, η οποία φέρει το ID του χρήστη, τον ρόλο του (στήλη role
στον πίνακα), για τον οποίο θα πούμε περισσότερα αργότερα, και έναν πίνακα με άλλα
δεδομένα (στην περίπτωσή μας το όνομα χρήστη).
Θα προσθέσουμε τον αυθεντικοποιητή στη διαμόρφωση ως υπηρεσία του DI container:
services:
- MyAuthenticator
Γεγονότα $onLoggedIn, $onLoggedOut
Το αντικείμενο Nette\Security\User
έχει γεγονότα $onLoggedIn
και
$onLoggedOut
, οπότε μπορείτε να προσθέσετε callbacks που θα καλούνται μετά
την επιτυχή σύνδεση ή την αποσύνδεση του χρήστη αντίστοιχα.
$user->onLoggedIn[] = function () {
// ο χρήστης μόλις συνδέθηκε
};
Ταυτότητα
Η ταυτότητα αντιπροσωπεύει ένα σύνολο πληροφοριών για τον χρήστη, το
οποίο επιστρέφει ο αυθεντικοποιητής και το οποίο στη συνέχεια
διατηρείται στη session και το λαμβάνουμε χρησιμοποιώντας το
$user->getIdentity()
. Μπορούμε λοιπόν να λάβουμε το id, τους ρόλους και
άλλα δεδομένα χρήστη, όπως τα περάσαμε στον αυθεντικοποιητή:
$user->getIdentity()->getId();
// λειτουργεί και η συντομογραφία $user->getId();
$user->getIdentity()->getRoles();
// τα δεδομένα χρήστη είναι διαθέσιμα ως properties
// το όνομα που περάσαμε στο MyAuthenticator
$user->getIdentity()->name;
Αυτό που είναι σημαντικό είναι ότι κατά την αποσύνδεση με τη χρήση
του $user->logout()
η ταυτότητα δεν διαγράφεται και παραμένει
διαθέσιμη. Έτσι, παρόλο που ο χρήστης έχει ταυτότητα, μπορεί να μην
είναι συνδεδεμένος. Αν θέλαμε να διαγράψουμε ρητά την ταυτότητα, θα
αποσυνδέαμε τον χρήστη καλώντας logout(true)
.
Χάρη σε αυτό, μπορείτε να συνεχίσετε να υποθέτετε ποιος χρήστης βρίσκεται στον υπολογιστή και, για παράδειγμα, να του εμφανίζετε εξατομικευμένες προσφορές στο e-shop, αλλά μπορείτε να του εμφανίσετε τα προσωπικά του δεδομένα μόνο μετά τη σύνδεση.
Η ταυτότητα είναι ένα αντικείμενο που υλοποιεί τη διεπαφή Nette\Security\IIdentity, η προεπιλεγμένη υλοποίηση είναι η Nette\Security\SimpleIdentity. Και όπως αναφέρθηκε, διατηρείται στη session, οπότε αν, για παράδειγμα, αλλάξουμε τον ρόλο κάποιου από τους συνδεδεμένους χρήστες, τα παλιά δεδομένα θα παραμείνουν στην ταυτότητά του μέχρι την επόμενη σύνδεσή του.
Αποθηκευτικός χώρος συνδεδεμένου χρήστη
Οι δύο βασικές πληροφορίες για τον χρήστη, δηλαδή αν είναι
συνδεδεμένος και η ταυτότητά του, συνήθως μεταφέρονται
στη session. Κάτι που μπορεί να αλλάξει. Για την αποθήκευση αυτών των
πληροφοριών είναι υπεύθυνο ένα αντικείμενο που υλοποιεί τη διεπαφή
Nette\Security\UserStorage
. Διατίθενται δύο τυπικές υλοποιήσεις, η πρώτη
μεταφέρει δεδομένα στη session και η δεύτερη σε cookie. Πρόκειται για τις
κλάσεις Nette\Bridges\SecurityHttp\SessionStorage
και CookieStorage
. Μπορείτε να
επιλέξετε τον αποθηκευτικό χώρο και να τον διαμορφώσετε πολύ άνετα στη
διαμόρφωση security ›
authentication.
Επιπλέον, μπορείτε να επηρεάσετε πώς ακριβώς θα γίνεται η αποθήκευση
της ταυτότητας (sleep) και η επαναφορά (wakeup). Αρκεί ο
αυθεντικοποιητής να υλοποιεί τη διεπαφή Nette\Security\IdentityHandler
. Αυτό
έχει δύο μεθόδους: η sleepIdentity()
καλείται πριν από την εγγραφή της
ταυτότητας στον αποθηκευτικό χώρο και η wakeupIdentity()
μετά την
ανάγνωσή της. Οι μέθοδοι μπορούν να τροποποιήσουν το περιεχόμενο της
ταυτότητας, ή να την αντικαταστήσουν με ένα νέο αντικείμενο που
επιστρέφουν. Η μέθοδος wakeupIdentity()
μπορεί ακόμη και να επιστρέψει
null
, αποσυνδέοντας έτσι τον χρήστη.
Ως παράδειγμα, θα δείξουμε μια λύση στη συχνή ερώτηση, πώς να
ενημερώσετε τους ρόλους στην ταυτότητα αμέσως μετά τη φόρτωση από τη
session. Στη μέθοδο wakeupIdentity()
θα περάσουμε στην ταυτότητα τους
τρέχοντες ρόλους π.χ. από τη βάση δεδομένων:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// εδώ μπορεί να τροποποιηθεί η ταυτότητα πριν από την εγγραφή στον αποθηκευτικό χώρο μετά τη σύνδεση,
// αλλά αυτό δεν το χρειαζόμαστε τώρα
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// ενημέρωση των ρόλων στην ταυτότητα
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
Και τώρα θα επιστρέψουμε στον αποθηκευτικό χώρο που βασίζεται σε
cookies. Σας επιτρέπει να δημιουργήσετε έναν ιστότοπο όπου οι χρήστες
μπορούν να συνδεθούν χωρίς να χρειάζονται sessions. Δηλαδή, δεν χρειάζεται
να γράφει στον δίσκο. Άλλωστε, έτσι λειτουργεί και ο ιστότοπος που
διαβάζετε αυτή τη στιγμή, συμπεριλαμβανομένου του φόρουμ. Σε αυτήν την
περίπτωση, η υλοποίηση του IdentityHandler
είναι απαραίτητη. Στο cookie θα
αποθηκεύουμε μόνο ένα τυχαίο token που αντιπροσωπεύει τον συνδεδεμένο
χρήστη.
Πρώτα λοιπόν, στη διαμόρφωση θα ορίσουμε τον επιθυμητό αποθηκευτικό
χώρο χρησιμοποιώντας security › authentication › storage: cookie
.
Στη βάση δεδομένων θα δημιουργήσουμε μια στήλη authtoken
, στην
οποία κάθε χρήστης θα έχει μια εντελώς
τυχαία, μοναδική και μη μαντέψιμη συμβολοσειρά επαρκούς μήκους
(τουλάχιστον 13 χαρακτήρες). Ο αποθηκευτικός χώρος CookieStorage
μεταφέρει στο cookie μόνο την τιμή $identity->getId()
, οπότε στο
sleepIdentity()
θα αντικαταστήσουμε την αρχική ταυτότητα με μια
αναπληρωματική με το authtoken
στο ID, αντίθετα στη μέθοδο
wakeupIdentity()
με βάση το authtoken θα διαβάσουμε ολόκληρη την ταυτότητα
από τη βάση δεδομένων:
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);
// επαληθεύουμε τον κωδικό πρόσβασης
...
// επιστρέφουμε την ταυτότητα με όλα τα δεδομένα από τη βάση δεδομένων
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// επιστρέφουμε μια αναπληρωματική ταυτότητα, όπου στο ID θα βρίσκεται το authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// αντικαθιστούμε την αναπληρωματική ταυτότητα με την πλήρη ταυτότητα, όπως στο authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Πολλαπλές ανεξάρτητες συνδέσεις
Είναι δυνατόν να έχουμε ταυτόχρονα πολλούς ανεξάρτητους συνδεδεμένους χρήστες στο πλαίσιο ενός ιστότοπου και μιας session. Εάν, για παράδειγμα, θέλουμε να έχουμε ξεχωριστή αυθεντικοποίηση για τη διαχείριση και το δημόσιο τμήμα στον ιστότοπο, αρκεί να ορίσουμε σε καθένα από αυτά το δικό του όνομα:
$user->getStorage()->setNamespace('backend');
Είναι σημαντικό να θυμόμαστε να ορίζουμε πάντα το namespace σε όλα τα σημεία που ανήκουν στο συγκεκριμένο τμήμα. Εάν χρησιμοποιούμε presenters, θα ορίσουμε το namespace στον κοινό πρόγονο για το συγκεκριμένο τμήμα – συνήθως τον BasePresenter. Θα το κάνουμε επεκτείνοντας τη μέθοδο checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Πολλαπλοί αυθεντικοποιητές
Η διαίρεση της εφαρμογής σε τμήματα με ανεξάρτητη σύνδεση συνήθως
απαιτεί επίσης διαφορετικούς αυθεντικοποιητές. Ωστόσο, εάν
καταχωρούσαμε δύο κλάσεις που υλοποιούν το Authenticator στη διαμόρφωση των
υπηρεσιών, το Nette δεν θα ήξερε ποιον από αυτούς να αντιστοιχίσει
αυτόματα στο αντικείμενο Nette\Security\User
και θα εμφάνιζε σφάλμα. Γι'
αυτό πρέπει να περιορίσουμε το autowiring για τους αυθεντικοποιητές
έτσι ώστε να λειτουργεί μόνο όταν κάποιος ζητήσει μια συγκεκριμένη
κλάση, π.χ. FrontAuthenticator, κάτι που επιτυγχάνεται με την επιλογή
autowired: self
:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Θα ορίσουμε τον αυθεντικοποιητή του αντικειμένου User πριν καλέσουμε τη μέθοδο login(), οπότε συνήθως στον κώδικα της φόρμας, που τον συνδέει:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};