Middleware szerepe a Laravel keretrendszerben

Attila | 2022. 10. 23. 15:51 | Olvasási idő: 4 perc

Címkék: #Biztonság (Security) #composer #CoreConcept #Köztes réteg (Middleware) #Laravel #Laravel 9 #MVC #npm #Routing #Támadás (Attacking) #Telepítés (Installation) #Űrlap (Form) #Védekezés (Defence) #Windows

Rengeteg dolgot megismertünk már a Laravel keretrendszerről. Most egy központi koncepciót, a rendszer magját érintő területet fogunk körüljárni: a Middleware -ek szerepét vizsgáljuk meg közelebbről.
Middleware_in_Laravel

Előkövetelmény: szükséges eszközök, csomagok frissítése

Ahogy azt már korábban átvettük ebben a blogbejegyzésben, érdemes úgy kezdeni a munkát, ha már hosszabb ideje nem nyúltunk a projektjeinkhez, hogy felfrissítjük a szükséges eszközeink és a beépülő csomagjaink verzióit. Nézzük meg, hogyan állunk most az eszközeinkkel:

  • PHP verzió: jelenleg a 8.1.2 van használatban, de ha úgy érezzük, hogy a saját verziónk elavult, akkor érdemes frissíteni, amihez javaslom ezt a blogbejegyzést. A parancssoros PHP verziójának ellenőrzésére használhatjuk ezt a parancsot a terminal-ban: php -v
  • A Composert szintén a legutóbb hivatkozott blogbejegyzésben írtak szerint lehet frissíteni. composer -v paranccsal lekérhető, hogy nálunk melyik verzió van meg. A jelenlegi legfrissebb verzió: 2.4.3
  • A Laravel keretrendszer telepítőjét szintén, annak a legfrissebb verziószáma jelenleg: 4.2.17, ami nem a Laravel keretrendszer verziószámára utal, hanem csak a telepítőjének a verziójára.
  • A Node és az npm csomagkezelő verzióinak ellenőrzése a következő parancsokkal a legegyszerűbb a terminal-ban: node -v és npm -v
    • Ha azt tapasztaljuk a központi oldal alapján, hogy ez már elavult, akkor innen töltsük le a legfrissebb verzió telepítőjét. Jelenlegi legfrissebb verzió a node-ból: 16.18.0 az npm-ből pedig: 8.19.2
  • A Laravel legfrissebb verziója jelenleg: 9.36.4


Bevezetés

Mik is azok a Middleware-ek?

Ők vizsgálják meg és szűrik az alkalmazásunk felé érkező HTTP kéréseket, amelyek az alkalmazásunkhoz akarnak hozzáférni. Például, ha vannak regisztrált felhasználóink, akkor ennek segítségével könnyedén fogjuk tudni ellenőrizni, hogy adott látogató (még csak látogató!) bejelentkezett-e. Ha olyan tartalomhoz szeretne hozzáférni, amihez bejelentkezés szükséges és még nincsen bejelentkezve a látogató, akkor továbbirányítjuk a bejelentkezési felületre. Ha Middleware azt érzékeli a vizsgálat során, hogy a felhasználó már bejelentkezett, akkor engedni fogja neki a hozzáférést az adott tartalmakhoz és a kérésére válaszul meg is kapja a tartalmakat a böngészőjében. Egy másik alapból létező és működő Middleware az űrlapoknál használt @csrf Blade direktíva által generált CSRF token ellenőrzése, hogy az űrlap elküldése és adatainak fogadása után megegyezik-e.

Úgy is fogalmazhatnánk, hogy a Middleware-ek gyakorlatilag hidat képeznek a HTTP kérések és válaszok között, miközben megvizsgálják és megszűrik a kéréseket.

Léteznek alapértelmezetten a projekttel együtt létrejövő Middleware-ek a Laravel-ben, mint az a kettő is, amire az imént utaltam, de természetesen mi magunk is tudunk definiálni sajátokat, egyedieket.


Nézzük meg a gyakorlatban!

Hozzunk létre egy új projektet, aminek már szándékosan adom azt a nevet, hogy authbreeze, mert gondolok a következő nagyobb témakörömre, ami a felhasználói azonosítás lesz, és ott is mindenképpen hasznát vesszük majd a Middleware-eknek. A következő paranccsal létrehozhatjuk a legfrissebb Laravel verziójú projektet:

laravel new authbreeze

Ebben fogjuk megvizsgálni a Middleware-ek kapcsán releváns részeket. Nézzük meg az app / Http / Middleware mappát.

Ebben megvannak az alapértelmezetten létrejövő Middleware -jeink. A legalsó fájl, a Kernel.php (ez a Http mappában van, nem a Middleware-ben), ahogy a neve is mutatja, a rendszer magját jelképezi HTTP kérések szempontjából: itt tudjuk majd regisztrálni/engedélyezni azokat a Middleware-eket, amelyek minden az alkalmazáshoz beérkező HTTP kérés során le kell, hogy fussanak. A Kernel.php-ban regisztrált Middleware-jeink:

Amelyik kommentelve van (itt most a TrustHosts), az nincsen figyelembe véve, nem kerül ellenőrzésre a végrehajtása.

Globális Middleware

Az ebben a $middleware tömbben lévő globálisan létező Middleware-ek reprezentálnak egy-egy réteget, amelyek lehetőséget biztosítanak arra például, hogy cache-eljünk, felhasználói engedélyeztetést vizsgáljunk, vagy éppen átirányítsuk valahova a felhasználót stb. Más szóval, elég sok mindent megtehetünk, amit csak akarunk a felhasználóktól érkező kérések és válaszok kezelésénél. Ezek a $middleware tömbben lévő rétegek azért globálisak, mert ezek minden egyes HTTP kérés során végrehajtódnak, ellenőrzésre kerülnek.

Ha megnézzük az ebben a mappában lévő Authenticate.php Middleware-t, akkor azt láthatjuk, hogy ebben mindössze egy metódus van, ami tovább irányítja a kérést a login oldalra, ha nincs még azonosítva a felhasználó. Érdemes megnézni az osztályt is, amit importál ez az osztály: Illuminate\Auth\Middleware\Authenticate , amiből azt szűrhetjük le, hogy ez az osztály azért nem csak ilyen egyszerű, hanem ez a rendszer magjában lévő felhasználói azonosítást egészíti ki ezzel a továbbirányítós metódussal. Akit érdekelnek a rendszer mélységei, az könnyen le tud "fúrni", ha rákattint erre az útvonalra, majd F12-t nyom a VSCode-ban, akkor rögtön látja is ennek a hivatkozott osztálynak a forráskódját és mivel a Laravel keretrendszer magja nyílt forráskódú, ezért bármilyen mélységig lefúrhatunk a részletekért. Minden Middleware tartalmaz egy metódust, aminek a neve: handle. Ezen belül bármilyen feladatot megcsinálhatunk (mint például, amiket az imént felsoroltunk), vagy éppen csak tovább dobjuk a következő rétegnek a kérést (return $next($request);). Ha megvizsgáljuk az ebben az osztályban lévő authenticate metódust, akkor abból könnyen világossá válhat számunkra, hogy egy azonosítást hajt végre, és ha az nem sikeres, akkor egy AuthenticationException-nel tér vissza nekünk (nem muszáj a függvény összes többi sorát értenünk most, mert nem azok a lényegesek a számunkra).

Az imént a réteg szót használtam és ez nem véletlen: ugyanis a Kernel.php-ban lévő $middleware tömbelemek gyakorlatilag egy-egy réteget képviselnek, amelyeken át kell jutnia a felhasználótól/látogatótól érkező HTTP kéréseknek. Ha valamelyik rétegen nem jut át a kérés, mert például nincsen bejelentkezve a látogató, akkor nem kerül tovább a kérés, hanem elirányítjuk őt egy bejelentkezési oldalra, tehát nem fog olyan választ kapni, aminek a segítségével ő hozzáférhetne bizonyos webalkalmazás tartalmakhoz. Erre egy nagyon jó szemléltetés a hagyma ábrája (most itt igazából a Middleware-ek neve nem is érdekes, a lényeg, hogy legbelül van az alkalmazás, amihez a kérésnek Middleware rétegeken kell keresztül jutniuk):

Mikor intézkedjünk a Middleware segítségével?

Természetesen, a Middleware-ek segítségével lefutásuk (ellenőrzésük) előtt és után is végrehajthatunk különböző feladatokat, ezt jól szemlélteti a következő ábra:

Hozzunk létre két Middleware-t, amelyek ezt képesek személtetni:

php artisan make:middleware BeforeMiddleware

public function handle(Request $request, Closure $next)
{
  // Feladatok végrehajtása a továbbadás előtt

  return $next($request);
}


php artisan make:middleware AfterMiddleware

public function handle(Request $request, Closure $next)
{
  $response = $next($request);

  // Feladatok végrehajtása a továbbadás után

  return $response;
}

Most még ezek a Middleware-ek nem csinálnak igazából semmit, sőt, ez a "semmittevésük" igazából végrehajtásra sem kerül, hiszen, nem regisztráltuk be őket a Kernel.php globális $middleware elemei közé. Pótolhatjuk ezt a hiányosságot:

protected $middleware = [
  // \App\Http\Middleware\TrustHosts::class,
  \App\Http\Middleware\TrustProxies::class,
  \Illuminate\Http\Middleware\HandleCors::class,
  \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
  \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
  \App\Http\Middleware\TrimStrings::class,
  \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
  \App\Http\Middleware\BeforeMiddleware::class,
  \App\Http\Middleware\AfterMiddleware::class,
];

Indítsuk el az alkalmazásunk kiszolgálását:

php artisan serve

Ha a böngészőben megnézzük az alkalmazásunkat, akkor megfelelően betöltődik a weboldalunk. Viszont, ha most mondjuk a BeforeMiddleware -be a "Feladatok végrehajtása a továbbadás előtt" komment után ütünk egy entert és beszúrjuk ezt a sort:

dd("feladat");

Majd elmentjük és frissítjük a böngészőben az oldalunkat (F5), akkor már meg is kapjuk az üzenetet:

Ennek a kiíratásnak persze túl sok értelme nincsen, de ha például a kiíratás helyett egy naplót (log-ot) szeretnénk építeni, akkor itt ezt meg tudjuk tenni, hogy a látogatók milyen lekérdezéseket hajtottak végre. Persze a naplózásra a Laravel-ben van erre jobb megoldás is, csak egy példát akartam írni arról, hogy miket is tudunk megtenni itt.

Útvonalakhoz tartozó Middleware-ek

Lehetőségünk van arra is, hogy bizonyos útvonalakhoz rendeljünk hozzá Middleware-eket. Ennek az lehet a célja, hogy bizonyos útvonalakat csak regisztrált és bejelentkezett felhasználóknak engedünk elérni, vagy épp fordítva, csak sima látogatóknak engedhetünk elérni. De ezek segítségével tudunk az útvonalakhoz különböző engedélyeztetéseket definiálni.

Az imént felsorolt útvonalakhoz tartozó alapértelmezetten létrejövő és létező Middleware-eket szintén a Kernel.php tartalmazza a $routeMiddleware asszociatív tömbben:

Ezeket alapból használhatjuk: ha például egy útvonalat, mondjuk az aktuálisan bejelentkezett felhasználónak a profil oldalának elérését szeretnénk felhasználói azonosítós ('auth') ellenőrzéssel ellátni, akkor azt így tehetjük meg az útvonalak regisztrálásánál (mondjuk a web.php-ban):

Route::get('/profile', function () {
  //
})->middleware('auth');

Itt csak egy Middleware-t adtunk hozzá az útvonalhoz, de lehetőségünk van arra is, hogy több Middleware-t állítsunk be egy útvonalhoz: ekkor a sima szöveges paraméterátadás ('auth') helyett, egy tömböt adjunk át a middleware metódusnak, például így: middleware(['guest', 'throttle']). Sőt, nem csak a fenti listából választhatunk, hanem a saját útvonal Middleware-ünket is hozzá tudjuk rendelni az útvonalakhoz: middleware(\App\Http\Middleware\BeforeMiddleware::class)

Próbáljuk is ki az útvonal Middleware-t!

Paraméterátadás a Middleware-nek

A feladat, amit megvalósítunk: regisztrálunk egy Middleware-t, ami ha a jelenlegi óra (idő) több, mint amit paraméterként megkap, akkor írjuk ki, hogy"Jó éjszakát!", ellenkező esetben irányítsuk át a főoldalra a látogatót.

Hozzunk létre egy új Middleware-t:

php artisan make:middleware CheckHour

Megjegyzés: a Middleware-ekre nem vonatkozik névkonvenció, tehát nem muszáj nekünk odaírni a fájlok/osztályok végére, hogy "Middleware", anélkül is jól fognak tudni működni.

A CheckHour osztály handle metódusa így néz ki:

public function handle(Request $request, Closure $next, $hour)
{
  if($hour < date('H')){
    return $next($request);
  }

  return redirect('/');
}

A metódus kapott egy harmadik paramétert, ami az $hour, a magjában pedig azt fogjuk ellenőrizni, hogy amikor majd el akarom érni az ehhez rendelt útvonalat, akkor elmúlt-e már a kapott óra, mert akkor kívánjon jó éjszakát, ellenkező esetben átirányít a főoldalra.

Egy fontos kiegészítő módosítás: a pontos óra megkapásához: a config / app.php fájlban az időzóna alapértelmezetten UTC, amely a közép-európai időhöz képest (óraátállítástól függően) 1-2 órával kevesebb, így írjuk át a következő beállítást a fájlban erre és mentsük utána:

'timezone' => 'Europe/Budapest',

Regisztráljuk be az új útvonal Middleware-ünket az app / Http / Kernel.php fájl $routeMiddleware asszociatív tömbjében vegyünk fel egy új sort:

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,
];

Ezután következhet az útvonal és a hozzá tartozó Middleware regisztrálása a routes / web.php-ban:

Route::get('/goodnight', function () {
  return "Jó éjszakát!";
})->middleware(['check-hour:20']);

Mentés után tesztelhetjük is. Amikor írom ezt a bejegyzést elmúlt 22 óra, így ha én megnyitom most a http://127.0.0.1:8000/goodnight URL-t, akkor kiírja nekem a böngésző, hogy jó éjszakát. Ha itt az útvonal regisztrációnál 'check-hour:23'-t adok meg, akkor el fog vinni a főoldalra, mivel még nincs túl késő, ezért a CheckHour Middleware-ünk elküld inkább minket a kezdőoldalra.

A Middleware-nek a paraméterátadás az útvonal regisztrációnál a regisztrált Middleware név utáni kettőspont után történik meg (az átadott $hour változó értéke 20 vagy 23 az én példámban).


Zárszó

Természetesen, a Middleware-ekkel még foglalkozunk, számos téma során érinteni fogjuk őket (rögtön a következő bejegyzésben, felhasználói azonosításnál is), úgyhogy aki még nem érezné teljesen biztosnak a tudását ezen a területen, az se essen kétségbe.

Az új projektünk Github projektje itt található, míg a bejegyzéshez tartozó változások commit-je itt.