Αυθεντικοποίηση χρηστών

Οι ελάχιστες έως μηδαμινές εφαρμογές ιστού δεν χρειάζονται μηχανισμό για την είσοδο των χρηστών ή τον έλεγχο των προνομίων τους. Σε αυτό το κεφάλαιο, θα μιλήσουμε για:

  • την είσοδο και την έξοδο του χρήστη
  • προσαρμοσμένους αυθεντικοποιητές και εξουσιοδοτητές

Εγκατάσταση και απαιτήσεις

Στα παραδείγματα, θα χρησιμοποιήσουμε ένα αντικείμενο της κλάσης Nette\Security\User, το οποίο αντιπροσωπεύει τον τρέχοντα χρήστη και το οποίο λαμβάνετε περνώντας το με τη χρήση dependency injection. Στα presenters απλά καλέστε το $user = $this->getUser().

Αυθεντικοποίηση

Αυθεντικοποίηση σημαίνει σύνδεση χρήστη, δηλαδή η διαδικασία κατά την οποία επαληθεύεται η ταυτότητα ενός χρήστη. Ο χρήστης συνήθως ταυτοποιείται χρησιμοποιώντας όνομα χρήστη και κωδικό πρόσβασης. Η επαλήθευση πραγματοποιείται από τον λεγόμενο αυθεντικοποιητή. Εάν η σύνδεση αποτύχει, εκπέμπεται η εντολή Nette\Security\AuthenticationException.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('The username or password you entered is incorrect.');
}

Έτσι γίνεται η αποσύνδεση του χρήστη:

$user->logout();

Και ελέγχοντας αν ο χρήστης είναι συνδεδεμένος:

echo $user->isLoggedIn() ? 'yes' : 'no';

Απλό, έτσι δεν είναι; Και όλες οι πτυχές της ασφάλειας αντιμετωπίζονται από τη Nette για εσάς.

Στον παρουσιαστή, μπορείτε να επαληθεύσετε τη σύνδεση στη μέθοδο startup() και να ανακατευθύνετε έναν μη συνδεδεμένο χρήστη στη σελίδα σύνδεσης.

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

Λήξη

Η σύνδεση χρήστη λήγει μαζί με τη λήξη του αποθετηρίου, το οποίο συνήθως είναι μια σύνοδος (βλ. ρύθμιση λήξης συνόδου ). Ωστόσο, μπορείτε επίσης να ορίσετε ένα μικρότερο χρονικό διάστημα μετά το οποίο ο χρήστης αποσυνδέεται. Για το σκοπό αυτό χρησιμοποιείται η μέθοδος setExpiration(), η οποία καλείται πριν από τη διεύθυνση login(). Παρέχετε ως παράμετρο μια συμβολοσειρά με μια σχετική ώρα:

// η σύνδεση λήγει μετά από 30 λεπτά αδράνειας
$user->setExpiration('30 minutes');

// Ακύρωση ορισμού λήξης
$user->setExpiration(null);

Η μέθοδος $user->getLogoutReason() ενημερώνει αν ο χρήστης έχει αποσυνδεθεί επειδή έχει λήξει το χρονικό διάστημα. Επιστρέφει είτε τη σταθερά Nette\Security\UserStorage::LogoutInactivity εάν έχει λήξει το χρονικό διάστημα είτε UserStorage::LogoutManual όταν έχει κληθεί η μέθοδος logout().

Αυθεντικοποιητής

Είναι ένα αντικείμενο που επαληθεύει τα δεδομένα σύνδεσης, δηλαδή συνήθως το όνομα και τον κωδικό πρόσβασης. Η τετριμμένη υλοποίηση είναι η κλάση Nette\Security\SimpleAuthenticator, η οποία μπορεί να οριστεί στη διαμόρφωση:

security:
	users:
		# όνομα: κωδικός πρόσβασης
		johndoe: secret123
		kathy: evenmoresecretpassword

Αυτή η λύση είναι πιο κατάλληλη για σκοπούς δοκιμών. Θα σας δείξουμε πώς να δημιουργήσετε έναν επαληθευτή που θα επαληθεύει τα διαπιστευτήρια έναντι ενός πίνακα της βάσης δεδομένων.

Ένας αυθεντικοποιητής είναι ένα αντικείμενο που υλοποιεί τη διεπαφή 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 περιέχει το hash. Μετά την επαλήθευση του ονόματος και του κωδικού πρόσβασης, επιστρέφει την ταυτότητα με το αναγνωριστικό του χρήστη, το ρόλο (στήλη role στον πίνακα), τον οποίο θα αναφέρουμε αργότερα, και έναν πίνακα με πρόσθετα δεδομένα (στην περίπτωσή μας, το όνομα χρήστη).

Θα προσθέσουμε τον authenticator στη διαμόρφωση ως υπηρεσία του DI container:

services:
	- MyAuthenticator

$onLoggedIn, $onLoggedOut Γεγονότα

Το αντικείμενο Nette\Security\User διαθέτει συμβάντα $onLoggedIn και $onLoggedOut, ώστε να μπορείτε να προσθέσετε ανακλήσεις που ενεργοποιούνται μετά την επιτυχή είσοδο ή μετά την έξοδο του χρήστη.

$user->onLoggedIn[] = function () {
	// ο χρήστης έχει μόλις συνδεθεί
};

Ταυτότητα

Η ταυτότητα είναι ένα σύνολο πληροφοριών σχετικά με έναν χρήστη που επιστρέφονται από τον αυθεντικοποιητή και οι οποίες στη συνέχεια αποθηκεύονται σε μια συνεδρία και ανακτώνται με τη χρήση του $user->getIdentity(). Έτσι μπορούμε να πάρουμε το id, τους ρόλους και άλλα δεδομένα του χρήστη όπως τα περάσαμε στον authenticator:

$user->getIdentity()->getId();
// λειτουργεί επίσης η συντόμευση $user->getId(),

$user->getIdentity()->getRoles();

// τα δεδομένα του χρήστη μπορούν να είναι προσβάσιμα ως ιδιότητες
// το όνομα που περάσαμε στο MyAuthenticator
$user->getIdentity()->name;

Είναι σημαντικό ότι όταν ο χρήστης αποσυνδεθεί χρησιμοποιώντας το $user->logout(), η ταυτότητα δεν διαγράφεται και εξακολουθεί να είναι διαθέσιμη. Έτσι, αν η ταυτότητα υπάρχει, από μόνη της δεν εξασφαλίζει ότι ο χρήστης είναι επίσης συνδεδεμένος. Αν θέλουμε να διαγράψουμε ρητά την ταυτότητα, αποσυνδέουμε τον χρήστη με το logout(true).

Χάρη σε αυτό, μπορείτε ακόμα να υποθέσετε ποιος χρήστης βρίσκεται στον υπολογιστή και, για παράδειγμα, να εμφανίσετε εξατομικευμένες προσφορές στο ηλεκτρονικό κατάστημα, ωστόσο, μπορείτε να εμφανίσετε τα προσωπικά του δεδομένα μόνο μετά τη σύνδεση.

Η ταυτότητα είναι ένα αντικείμενο που υλοποιεί τη διεπαφή Nette\Security\IIdentity, η προεπιλεγμένη υλοποίηση είναι η Nette\Security\SimpleIdentity. Και όπως αναφέρθηκε, η ταυτότητα αποθηκεύεται στη σύνοδο, οπότε αν, για παράδειγμα, αλλάξουμε το ρόλο κάποιου από τους συνδεδεμένους χρήστες, τα παλιά δεδομένα θα διατηρηθούν στην ταυτότητα μέχρι να συνδεθεί ξανά.

Αποθήκευση για τον συνδεδεμένο χρήστη

Οι δύο βασικές πληροφορίες σχετικά με τον χρήστη, δηλαδή το αν είναι συνδεδεμένος και η ταυτότητά του, μεταφέρονται συνήθως στη συνεδρία. Η οποία μπορεί να αλλάξει. Για την αποθήκευση αυτών των πληροφοριών είναι υπεύθυνο ένα αντικείμενο που υλοποιεί τη διεπαφή Nette\Security\UserStorage. Υπάρχουν δύο τυπικές υλοποιήσεις, η πρώτη μεταφέρει τα δεδομένα σε μια σύνοδο και η δεύτερη σε ένα cookie. Πρόκειται για τις κλάσεις Nette\Bridges\SecurityHttp\SessionStorage και CookieStorage. Μπορείτε να επιλέξετε την αποθήκευση και να τη ρυθμίσετε πολύ εύκολα στη διαμόρφωση security › authentication.

Μπορείτε επίσης να ελέγξετε τον ακριβή τρόπο με τον οποίο θα γίνεται η αποθήκευση (sleep) και η επαναφορά (wakeup) της ταυτότητας. Το μόνο που χρειάζεστε είναι να υλοποιεί ο αυθεντικοποιητής τη διεπαφή Nette\Security\IdentityHandler. Αυτή έχει δύο μεθόδους: η sleepIdentity() καλείται πριν από την εγγραφή της ταυτότητας στον αποθηκευτικό χώρο και η wakeupIdentity() καλείται μετά την ανάγνωση της ταυτότητας. Οι μέθοδοι μπορούν να τροποποιήσουν τα περιεχόμενα της ταυτότητας ή να την αντικαταστήσουν με ένα νέο αντικείμενο που επιστρέφει. Η μέθοδος wakeupIdentity() μπορεί ακόμη και να επιστρέψει το null, το οποίο αποσυνδέει τον χρήστη.

Ως παράδειγμα, θα δείξουμε μια λύση σε μια συνηθισμένη ερώτηση σχετικά με τον τρόπο ενημέρωσης των ρόλων ταυτότητας αμέσως μετά την αποκατάσταση από μια συνεδρία. Στη μέθοδο 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. Σας επιτρέπει να δημιουργήσετε έναν ιστότοπο όπου οι χρήστες μπορούν να συνδεθούν χωρίς να χρειάζεται να χρησιμοποιούν συνεδρίες. Έτσι, δεν χρειάζεται να γράφει στο δίσκο. Εξάλλου, έτσι λειτουργεί ο ιστότοπος που διαβάζετε τώρα, συμπεριλαμβανομένου του φόρουμ. Σε αυτή την περίπτωση, η υλοποίηση του IdentityHandler είναι απαραίτητη. Θα αποθηκεύσουμε μόνο ένα τυχαίο token που αντιπροσωπεύει τον συνδεδεμένο χρήστη στο cookie.

Έτσι, πρώτα ορίζουμε την επιθυμητή αποθήκευση στη διαμόρφωση χρησιμοποιώντας το security › authentication › storage: cookie.

Θα προσθέσουμε μια στήλη authtoken στη βάση δεδομένων, στην οποία ο κάθε χρήστης θα έχει μια εντελώς τυχαία, μοναδική και μη-αναγνωρίσιμη συμβολοσειρά επαρκούς μήκους (τουλάχιστον 13 χαρακτήρες). Το αποθετήριο CookieStorage αποθηκεύει μόνο την τιμή $identity->getId() στο cookie, οπότε στο sleepIdentity() αντικαθιστούμε την αρχική ταυτότητα με ένα proxy με 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;
	}
}

Πολλαπλές ανεξάρτητες πιστοποιήσεις

Είναι δυνατό να έχετε πολλούς ανεξάρτητους συνδεδεμένους χρήστες σε έναν ιστότοπο και σε μία συνεδρία κάθε φορά. Για παράδειγμα, αν θέλουμε να έχουμε ξεχωριστό έλεγχο ταυτότητας για το frontend και το backend, απλά θα ορίσουμε ένα μοναδικό χώρο ονομάτων συνόδου για κάθε ένα από αυτά:

$user->getStorage()->setNamespace('backend');

Είναι απαραίτητο να έχετε κατά νου ότι αυτό πρέπει να οριστεί σε όλες τις θέσεις που ανήκουν στο ίδιο τμήμα. Όταν χρησιμοποιούμε παρουσιαστές, θα ορίσουμε το namespace στον κοινό πρόγονο – συνήθως το BasePresenter. Για να το κάνουμε αυτό θα επεκτείνουμε τη μέθοδο checkRequirements():

public function checkRequirements($element): void
{
	$this->getUser()->getStorage()->setNamespace('backend');
	parent::checkRequirements($element);
}

Πολλαπλοί επαληθευτές

Ο διαχωρισμός μιας εφαρμογής σε τμήματα με ανεξάρτητη αυθεντικοποίηση απαιτεί γενικά διαφορετικούς αυθεντικοποιητές. Ωστόσο, η καταχώρηση δύο κλάσεων που υλοποιούν Authenticator σε υπηρεσίες config θα προκαλούσε σφάλμα επειδή η Nette δεν θα ήξερε ποια από αυτές θα έπρεπε να συνδεθεί αυτόματα με το αντικείμενο Nette\Security\User. Γι' αυτό πρέπει να περιορίσουμε την αυτόματη σύνδεση για αυτούς με το autowired: self, ώστε να ενεργοποιείται μόνο όταν ζητείται συγκεκριμένα η κλάση τους:

services:
	-
		create: FrontAuthenticator
		autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FrontAuthenticator $authenticator,
	) {
	}
}

Πρέπει να ορίσουμε τον authenticator μας στο αντικείμενο User μόνο πριν από την κλήση της μεθόδου login(), που τυπικά σημαίνει στο callback της φόρμας σύνδεσης:

$form->onSuccess[] = function (Form $form, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};
έκδοση: 4.0