Felhasználói hitelesítés (authentication) - 4. rész: Breeze: szerepkör alapú hitelesítés

Attila | 2022. 12. 06. 18:27 | Olvasási idő: 4 perc

Címkék: #Breeze #CoreConcept #Köztes réteg (Middleware) #Laravel #Laravel 9 #MySQL #Routing #SQLite

A Breeze lehetőséget nyújt nekünk a felhasználói hitelesítésre, azonban mi van akkor, ha mi szeretnénk szerepkör alapon (role-based) külön-külön kezelni a felhasználók csoportjait? Ennek járok utána ebben a bejegyzésben, így megalapozva a későbbi engedélyeztetési (authorize) lehetőségek feltárását.
multiple-user-roles-auth-laravel

Bevezetés

Mivel a Breeze-t már használjuk, ezért "akarva/akaratlanul" van is már két felhasználói csoportunk. Az egyik a látogatók csoportja, ők azok, akik nincsenek regisztrálva, ezáltal bejelentkezve sem lehetnek, illetve a másik csoport az a regisztrált felhasználóké, akik be is tudnak jelentkezni az alkalmazásunkba. Ezt a két csoportot fogjuk most kiegészíteni egy adminisztrátor csoporttal, vagy nevezhetjük szerepkörnek is a csoportokat.


Szerepkörök beépítése

Adatbázis bővítése

Hozzunk létre egy új migrációs fájlt:

php artisan make:migration add_role_column_to_users

Az up() metóduson belül ezzel bővítsük a users tábla módosítását:

$table->enum('role', ['user','admin'])->default('user');

Megjegyzés: ha komplexebb alkalmazást építünk, akkor érdemes lehet egy roles táblát létrehozni, amiben van id, name és a timestamp-ek, majd azt összekötni a users tábla módosításával, ott létrehozva egy role_id külső kulcsot erre a roles táblára. Azonban mi most egyszerűbben oldjuk ezt meg azáltal, hogy egy sima oszlopbővítést hajtottunk végre és enumerációval (felsorolással, ami kvázi egy lista a lehetséges értékeivel) hoztuk létre az új role oszlopot a users táblában.

A down() metóduson belül ezzel bővítsük a users tábla módosítását:

$table->dropColumn('role');

Ezután következhet a migráció végrehajtása:

php artisan migrate

Megjegyzés: a MySQL adatbáziskezelőben a táblákhoz tudunk enum típusú oszlopot hozzáadni. Az SQLite ezt egy kicsit "trükkösebben" oldja meg a háttérben, mivel ott egy sima varchar (szöveges) típusú oszlop jön létre, amihez egy ellenőrzést fűz hozzá: CHECK("role" IN ('user', 'admin')) Tehát csak user és admin értékek kerülhetnek bele ebbe a mezőbe.

Nekem most két felhasználóm van a users táblában és az egyiket manuálisan beállítom admin szerepkörűre. Ugye a migrációs fájlban beállított alapértelmezett (default) érték miatt mindkét felhasználó alapból user szerepkörű lett, ezt most az egyiknél megváltoztatom admin-ra.

User Model bővítése

Szerkesszük át egy picit a User Model fájlunkat. Először is a $fillable tömbhöz adjuk hozzá az új role mezőnket.

protected $fillable = [
  'name',
  'email',
  'password',
  'username',
  'role',
];

Adjunk hozzá továbbá egy új metódust, ami egy bizonyos szerepkör (user vagy admin) meglétét hivatott ellenőrizni majd:

/**
  * @param string $role
  * @return bool
*/
public function hasRole(string $role): bool
{
  return $this->getAttribute('role') === $role;
}

A hármas egyenlőség jel nem csak azt ellenőrzi, hogy megegyezik-e a paraméterként kapott $role változó típusa (string) megegyezik-e az alapértelmezetten létező getAttribute() metódus visszatérési értékének típusával.


Szerepkörök használata

Ahhoz, hogy tudjuk használni szerepköröket, érdemes átgondolni, átismételni, hogy hogyan is történik meg a felhasználói kérések kiszolgálása:

  1. A felhasználó vagy látogató beír egy URL-t a böngészőjébe, ami jobb esetben regisztrálva van útvonalként a mi Laravel rendszerünkben.
  2. Ha regisztrálva van az útvonal, akkor felhasználói kérés végigmegy a regisztrált middleware-eken, az alapértelmezetten létezőkön túl, az általunk regisztráltakon is végighalad, mint egy vöröshagyma héjain kintről a közepe felé haladva. A middleware-ek áttekintéséhez érdemes áttekinteni ezt a blogbejegyzésemet.
  3. Ha túljutott a felhasználói kérés a middleware-eken, akkor következhet az útvonalhoz tartozó Controller metóduson keresztül a feldolgozása (esetleg még a View megjelenítése és visszaadása a felhasználó böngészőjének).

Először rátérhetünk az útvonal regisztrációs fájlunkra, a web.php-ra. A user szerepkörű felhasználónknak csak egy módosítást hajtsunk végre a /dashboard útvonalában:

Route::get('/dashboard', function () {
  return view('user_dashboard');
})->middleware(['auth', 'verified', 'user'])->name('dashboard');

A nézet nevét írtuk itt át dashboard-ról user_dashboard-ra, valamint a middleware-ek felsorolása közé felvettük a user middleware-t. Azért, hogy ez működjön és használható legyen, keressük meg a dashboard nézetünket és nevezzük át a fájlt erre: user_dashboard. Valamint a fájlon belül a (4. sorban lévő) címet írjuk át "Dashboard"-ról "User Dashboard"-ra. Magát az útvonal elérhetőségét (/dashboard) és nevét (dashboard) azért nem akartam megváltoztatni, mert az a rendszer több más részét is érintené, de itt most nem az a lényeg nekünk, úgyhogy emiatt ezt érintetlenül hagytam.

Következhet a másik útvonal a web.php fájlban, amelyet újként kell létrehoznunk, de lemásolhatjuk a fenti útvonalat és a megfelelő részeit módosíthatjuk így:

Route::get('/admin-dashboard', function () {
  return view('admin_dashboard');
})->middleware(['auth', 'verified', 'admin'])->name('admin_dashboard');

Ez a nézet viszont még nem létezik, viszont lemásolhatjuk hozzá az előbb módosított dashboard.blade.php -t és adjuk neki az admin_dashboard.blade.php nevet. A fájlban az imént átírt címet írjuk át "User Dashboard"-ról "Admin Dashboard"-ra.

Megjegyzés: vegyük észre, hogy túl sok háttérlogika itt még nincsen az útvonalak mögött, csak egy példát mutatok arra, hogy hogyan lehet middleware-ek segítségével más-más irányba küldeni az adott szerepkörrel rendelkező felhasználókat.

Most létre fogunk hozni két middleware-t, egyet-egyet szerepkörönként, tehát a user és az admin szerepkör is kapni fog egyet, amelyeket utána majd az útvonalak hozzáférésénél fogunk hasznosítani.

php artisan make:middleware UserAuthenticated

php artisan make:middleware AdminAuthenticated

Kezdjük a munkát az AdminAuthenticated osztály handle() metódusának magjával, alakítsuk át így:

if( auth()->check() )
{
  /** @var User $user */
  $user = auth()->user();
  
  if ( $user->hasRole('admin') ) {
    return $next($request);
  }
}

abort(403);  // permission denied error

A hibamentes működéshez a fájl tetején importáljuk be az App\Models\User osztályt. Megjegyzés: a php dokumentációs megjegyzésben (/** ... */) a @var, mint variable, tehát változó megjelöléssel segítjük a rendszert azzal, hogy megmondjuk ennek a $user változónak a típusát, ami így egy User osztályból példányosított objektum lesz.

Ha pedig nincs admin szerepköre a felhasználónak, akkor adjunk neki vissza egy 403-as HTTP hibakódot, ami a hozzáférés megtagadását jelenti.

Folytassuk a UserAuthenticated osztály handle() metódusának magjának átalakításával:

if( auth()->check() )
{
  /** @var User $user */
  $user = auth()->user();
  
  if ( $user->hasRole('admin') ) {
    return redirect(route('admin_dashboard'));
  }
  else if ( $user->hasRole('user') ) {
    return $next($request);
  }
}

abort(403);  // permission denied error

Ez egy kicsit annyival trükkösebb az előzőnél, hogy mivel a Breeze komponens részei automatikusan a felhasználói beléptetés után a /dashboard útvonalra akarja küldeni a felhasználót, itt most mi egy ellenőrzést hajtunk végre, és hogy ha az, aki bejelentkezett, nem egy sima felhasználó (user), hanem egy adminisztrátor (admin), akkor oda továbbítjuk az ő vezérlőpultjára. Ha egyik szerepkörrel sem rendelkezik, akkor itt is egy hozzáférés megtagadva üzenettel fogjuk ezt jelezni a látogatók felé.

Itt se felejtsük el importálni a User osztályunkat a fájl tetején.

Nincs más hátra, mint az, hogy beregisztráljuk a rendszerbe ezt a két útvonal middleware-t:

protected $routeMiddleware = [
  'auth' => \App\Http\Middleware\Authenticate::class,
  'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
  'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
  'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
  'can' => \Illuminate\Auth\Middleware\Authorize::class,
  'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
  'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
  'signed' => \App\Http\Middleware\ValidateSignature::class,
  'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
  'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
  'check-hour' => \App\Http\Middleware\CheckHour::class,
  'user' => \App\Http\Middleware\UserAuthenticated::class,
  'admin' => \App\Http\Middleware\AdminAuthenticated::class,
];

A users adatbázistáblám így néz ki:

Úgyhogy ezzel a két felhasználóval fogom tesztelni, hogy megfelelő vezérlőpultot (dashboard felületet) kapok-e meg. Elsőként itt látható, hogy Attila nevű felhasználómmal a "User Dashboard"-ot kapom meg bejelentkezés után:

Teszt Elek nevű felhasználómmal pedig az "Admin Dashboard"-ot és a hozzá tartozó útvonalat kapom meg a böngészőmben:

Ha pedig user szerepkörű felhasználóként szeretném elérni az /admin-dashboard útvonalat, akkor megkapom a 403-as hozzáférés megtagadva hibaüzenetet.

Látogatóként (nem bejelentkezett felhasználóként) sem a /dashboard sem az /admin-dashboard útvonalat nem tudom elérni, ha ezeket írnám be, akkor automatikusan a bejelentkezési űrlapra irányítana rá a rendszer.


Összegzés és kiegészítő információk az útvonalakról: csoportosítás (group) és előtag (prefix)

Ebben a bejegyzésben a felhasználói azonosításhoz kapcsolódó tudásanyagunkat kiegészítettem a szerepkör alapú lehetőségek bemutatásával. Mindehhez a felhasználói kérések (requests) feldolgozását használtuk fel, mivel a kiszolgáláshoz különböző middleware-eken kell keresztül mennie a kérésnek, itt tudtuk a felhasználók adott szerepkörének meglétét ellenőrizni. Ha több útvonalat is szeretnénk ilyen módon ellenőrizni, akkor lehetőségünk van csoportosítani az útvonalainkat, például azokat, amelyek a sima regisztrált felhasználókhoz vagy esetleg az adminisztrátorokhoz tartoznak. Ehhez példaként nézzük meg a web.php-ban már szereplő "profile" útvonalakhoz tartozó csoportosítást. A felhasználói profil szerkesztéséhez elegendő az "auth" middleware-nek való "megfelelés". Ha viszont a user és admin middleware-jeinkhez szeretnénk útvonalakat létrehozni, akkor azt így tudnánk megtenni:

// user middleware-hez tartozó útvonalaink
Route::group(['middleware' => ['auth', 'user']], function () {
  // 1. user útvonal regisztrációja
  // 2. user útvonal regisztrációja
  // ...
  // utolsó user útvonal regisztrációja
});

// admin middleware-hez tartozó útvonalaink
Route::group(['middleware' => ['auth', 'admin']], function () {
  // 1. admin útvonal regisztrációja
  // 2. admin útvonal regisztrációja
  // ...
  // utolsó admin útvonal regisztrációja
});

A megjegyzések helyére most konkrét útvonalat én nem regisztráltam, csak szemléltetni szerettem volna, hogy hogyan is kellene ennek működnie.

Még egy legutolsó kiegészítés: ha ezeket a user és admin specifikus útvonalakat szeretnénk ellátni egy előtaggal, egy prefixszel, akkor azt még a group metódus első tömb paraméterén belül jelezni kell:

// user middleware-hez tartozó útvonalaink
Route::group(['middleware' => ['auth', 'user'], 'prefix' => 'user'], function () {
  // 1. user útvonal regisztrációja
  // 2. user útvonal regisztrációja
  // ...
  // utolsó user útvonal regisztrációja
});

// admin middleware-hez tartozó útvonalaink
Route::group(['middleware' => ['auth', 'admin'], 'prefix' => 'admin'], function () {
  // 1. admin útvonal regisztrációja
  // 2. admin útvonal regisztrációja
  // ...
  // utolsó admin útvonal regisztrációja
});

Ilyenkor egy user-specifikus útvonal így nézhet ki: /user/utvonal-1

Míg egy admin-specifikus útvonal így: /admin/utvonal-1

Tehát a két útvonal regisztrációja lehet egyforma is és ez nem fog gondot okozni, mivel a prefix-ben különbözni fognak egymástól.

A bejegyzéshez kapcsolódó módosításokat tartalmazó Github commit itt érhető el.