Komplex példa - Validálás - 4. rész: A szerver oldal

Attila | 2022. 07. 29. 12:38 | Olvasási idő: 5 perc

Címkék: #Blade #Érvényesítés (Validation) #Laravel #Nézet (View) #Támadás (Attacking) #Űrlap (Form) #Védekezés (Defence)

A kliens oldali utazás után visszatérek a szerver oldalra és megvizsgálom, hogy milyen lehetőségeket nyújt számunkra a Laravel, ha validálni szeretnénk az űrlapokról érkező adatokat.
laravel-validation-1-png

Bevezetés

Az elmúlt néhány bejegyzésben végigvettem a kliens oldali validáció lehetőségeit az űrlapok kitöltésénél:

  1. HTML5 attribútumok segítségével "terelgethetjük" a felhasználót a helyes kitöltés felé
  2. Javascript kódok segítségével ellenőrizhetjük a kitöltött adatok formátumát, intervallumait
  3. Egy kimondottan validációs célra létrehozott Javascript osztálykönyvtár telepítését, lehetőségeit és működtetését is végignéztünk

Éppen itt az ideje, hogy rátérjünk arra, amikor már az adatok a kliens oldalról átkerülnek a szerver oldalra. Itt sem úszhatjuk meg a validációt, ha érvényes adatokkal szeretnénk dolgozni a jövőben. Mivel szerver oldalon leszünk, simán használhatnánk a PHP nyelv lehetőségeit is az ellenőrzésre, azonban szerencsére a Laravel nyújt nekünk olyan lehetőségeket, amelyek megkönnyítik a munkánkat.

Ahogy azt megszokhattátok, gyakorlati szempontból fogom bemutatni a Laravel validációs lehetőségeit, így amikor megnézünk valamit, utána egyből ki is próbáljuk élesben. Viszont néhol kicsit "csalni" is fogok, mert azt szeretném, hogy a lényegre, vagyis most a szerver oldali validációra tudjunk koncentrálni, ezért amikor új vagy már meglévő űrlapokkal dolgozok, akkor nem teszek majd bele (vagy megszüntetem) kliens oldali validációt. Így egyszerűbb lesz tesztelni a szerver oldali validációnk helyes működését, de természetesen, ha meg vagyunk már elégedve a szerver oldali validációval, akkor be kell tenni (vagy vissza kell illeszteni) a kliens oldali védelmi mechanizmusokat is.


Poggyász (csomag) létrehozása

Kezdjük egy olyan folyamattal, ami még hiányzott a rendszerből, így legalább épül az alkalmazásunk. Az útvonalaink a web.php-ben megvannak. Ezután elindulunk a luggage/index nézettel, amibe elhelyezzük a weblinket, ami elvezet a létrehozó nézethez (és az azon belüli űrlaphoz).

Ez ugye a LuggageController create metódusára fog elvinni minket az útvonalán keresztül, ami bár létezik, de a metódus magja még üres. Töltsük ezt fel, de előtte emlékeztetőül: a luggage adattábla az adatbázisban két olyan mezőt tartalmaz, amit a felhasználó ad majd meg az űrlapon keresztül: 1. csomag száma, 2. utas azonosítója. A LuggageController create metódusában emiatt egy listát is át kell adnunk a create nézetnek, ami az utasok nevét és azonosítóját tartalmazza, hogy majd a felhasználó ki tudja ezt választani (megjegyzés: nem túl valószerű, hogy a felhasználó a csomag létrehozásakor tud böngészni az utasok között és kiválaszthatja a poggyászhoz tartozó utast, de most ettől tekintsünk el, hiszen mi a szerver oldali validációra szeretnénk koncentrálni).

public function create()
{
  return view('luggage.create', [
    'passengers' => Passenger::orderBy('name')->get()
  ]);
}

Ez ennyi, persze importálnunk is kell a fájl elején a Passenger Model osztályt a helyes működés miatt. Utána következhet a create nézet, aminél bár adná magát a luggage adattábla kényszerei miatt, hogy tegyük bele a required attribútumot a két bemeneti mezőnkhöz, de most tekintsünk el ettől, ahogy korábban erre utaltam is:

A jobb áttekinthetőség miatt az egyes HTML tag-ek új attribútumait új sorba tettem, amihez egy VSCode-os formázás (ALT + SHIFT + f) is a segítségemre volt. Azért pedig, hogy ne felejtsük el a kliens oldali validációt a későbbiekben, betettem a fájlba a két @push -os JS részt (head-scripts és scripts neveken).

Látható, hogy az űrlap action attribútumában, semennyire sem meglepő módon a következő állomás majd a store útvonalon keresztüli LuggageController-ben lévő store() metódus lesz. Ide fogjuk elhelyezni a szerver oldali validációnkat. De előtte még egyetlen gyors teendő: a Luggage Model osztályba helyezzük el a kitölthető mezők felsorolását:

protected $fillable = ['number', 'passenger_id'];

Ezután nézzük is meg, milyen lehetőségeink vannak a szerver oldali validációra!

1. típusú szerver oldali validáció és hibajelzés

A store() metódusunk, ugye alapból megkapja a Request objektumot, ami tartalmazza azokat az adatokat (egy gyűjteményben), amiket a felhasználó elküldött az űrlapon.

public function store(Request $request)
{
  Luggage::create(request()->all());
  return redirect(route('luggage.index'));
}

Ha kliens és szerver oldali validáció nélkül oldanánk meg a poggyász létrehozó feladatot, akkor nem megfelelő információk megadása esetén egy csúnya nagy adatbázissal kapcsolatos hibaüzenetet kapnánk például arról, hogy a csomag számának megadása kötelező volt (angolul persze), amitől az egyszeri felhasználó biztosan megijedne. Teszteljük is ezt le, küldjük el úgy az űrlapot, hogy nincs megadva a poggyász száma (ez azt eredményezi az alábbi INSERT INTO utasításban, hogy a VALUES részben ? kerül a number mező helyére - üres lesz -, ami a hibát okozza):

Meg is kaptuk, amit vártunk. Úgyhogy segítsük a felhasználót most szerver oldali validációval. Használjuk az imént említett $request objektumot most azoknak a szabályoknak a betartatására, amelyeket bár kliens oldalon a példánk miatt figyelmen kívül hagytunk, de a luggage adattábla elvárásként fogalmaz meg felénk:

  1. Mindkét bemeneti mező megadása kötelező.
  2. A csomagszámot tartalmazó szöveges mezőnek maximum 255 karakter hosszúnak szabad lennie.
  3. Az utas azonosítónak pedig egész számnak kell lennie.
  4. Az utas azonosítónak, mivel ez egy külső kulcs, ami hivatkozik a passengers tábla egy sorára, ezért itt csak olyan értéket szabad elfogadni, ami létezik a passengers táblában (tehát ha csak 200 sorunk van például a passengers táblában, 1-200-ig tartozó id azonosítókkal, akkor most a poggyászoknál ne lehessen 645-ös azonosítójú utashoz beállítani az új poggyászt).

Nézzük meg, hogy néz ki az iménti felsorolás a szerver oldali validációnál:

public function store(Request $request)
{
  $request->validate([
    'number' => 'required|max:255',
    'passenger_id' => 'required|integer|exists:passengers,id',
  ]);

  // Ha idáig eljutunk a feldolgozásban, akkor az érkező adatok már érvényesek

Talán a required, max:255, integer szabályokat nem kell magyaráznom, de az exists vonatkozik arra, hogy ennek a passenger_id mező értéknek szerepelnie kell a passengers tábla id oszlopában, csak ekkor lehet érvényes. Most itt én használtam négyféle (required, max, integer, exists) szabályozást, de ennél sokkal több van, amelyek itt érhetők el rövid magyarázattal és egy-egy példával együtt: Laravel - validációs szabályok

Ahogy az utolsó (megjegyzés) sor is írja a kódban, ha "idáig eljutunk", akkor következhet a feldolgozás, vagyis az adatok elmentése az adatbázisba, mert már érvényesek az adatok, megfelelnek a validate() metódusban megfogalmazott szabályainknak. Teszteljük is le ugyanúgy, ahogy korábban, tehát ne adjunk meg csomag számot és úgy küldjük el, majd figyeljük, hogy mi történik.

Nem tudom, hogy ki mennyire figyelt jól, de látszólag nem történik semmi... ami nem teljesen igaz, mert az oldalunk a gombnyomást érzékelte, töltődött is, majd visszaállt ugyanabba az állapotba (ha esetleg az utas nevét megváltoztatjuk, de a csomagszámot üresen hagyjuk, akkor ez jobban látható, mert az utasoknál visszaugrik alapállapotba a kiválasztott utas).

A szerver oldali validáció megtörténik (gyaníthatóan), csak mivel nem teljesül, ezért visszadobja a kliens oldalnak, hogy hiba van: jelen esetben nem töltöttük ki a csomag száma mezőt. Viszont a kliens oldal (a create nézet) nincs felkészítve még arra, hogy megmutassuk a visszadobott hibákat. Folytassuk ezzel a munkát, szúrjuk be a form nyitó tag-je elé ezt:

Ha most ráfrissítünk az oldalra és újraküldjük csomag szám nélkül az űrlapot, akkor most már mutatja nekünk, hogy valami hiba van:

Visszakaptuk, amit vártunk, vagyis azt, hogy a csomag száma mező kitöltése kötelező. Angolul van a hibaüzenet, mi viszont lehet, hogy magyarul szeretnénk kiírni a felhasználónak, nézzük meg, hogy mit kell ehhez tennünk. A store() metódusban lévő validate() metódus jelenleg csak egy paramétert tartalmaz, de hozzáadhatunk egy másodikat is, amivel az alapértelmezett hibaüzeneteket megváltoztathatjuk. Tegyük is ezt meg:

$request->validate([
  'number' => 'required|max:255',
  'passenger_id' => 'required|integer|exists:passengers,id',
], [
  'number.required' => 'A Csomag szám mező kitöltése kötelező.',
  'number.max' => 'A csomag száma maximum 255 karakter hosszú lehet.',
  'passenger_id.required' => 'Az Utas megadása kötelező.',
  'passenger_id.integer' => 'Az Utas azonosítójának számnak kell lennie.',
  'passenger_id.exists' => 'Csak létező utast adhat meg.',
]);

Láthatóak a saját hibaüzeneteim. Próbáljuk is ki, hogy működnek-e, de most egy másik szabályozást teszteljünk:

  1. Nálam nincsen 9999-es számú utas, úgyhogy belenyúlok a HTML kódjába valós időben egy vizsgálat után szerkesztéssel, és törlöm a select-et a DOM fából (jobb egérgomb a select kódsorán és "Csomópont törlése" menüpont Firefox-ban),
  2. majd lemásolom ("Csomópont kettőzése" menüpont) a meglévő input mezőt ott manuálisan, aminek ugyanazt a nevet adom, mint ami a select-é volt (passenger_id) a value attribútum értékét pedig beállítom 9999-re.
  3. A támadás helyes végrehajtásához még az id attribútum értékét átállítom passenger_id-ra, a type-ot pedig number-re.

Az alábbi képen láthatók a DOM fa szerkesztései bal alul, az eredménye pedig felette:

Utána a Mentés gomb megnyomásával meg tudjuk nézni, hogy valóban visszaadja-e a megfelelő hibaüzenetet a szerver oldali validáció:

Látható tehát, hogy megtámadtuk a webalkalmazásunkat: kliens oldalon próbáltuk kicselezni a szabályokat, átírtunk paramétereket hamisra, de a szerver oldali validációnk közbeszólt, és nem enged ilyen (nem létező utashoz tartozó) poggyászt felvenni az adatbázisba.

2. típusú szerver oldali validáció és hibajelzés

Ha nem szeretnénk használni a $request objektum validate() segédmetódusát, akkor lehetőségünk van arra is, hogy a Validator (facade-ot, erről majd a későbbiekben lesz szó) alkalmazzuk. A működés itt is nagyon hasonló lesz, csak itt a Validator osztály make metódusát használjuk. Ez a metódus négy paramétert kaphat, amiből az első és második kötelező, a harmadik és negyedik opcionális:

  1. paraméter (kötelező): a felhasználótól érkező bemeneti adatok, ezek ugyanúgy mint korábban, most is lehet őket kezelni a $request->all() segítségével.
  2. paraméter (kötelező): a validációs szabályokat tartalmazza.
  3. paraméter (opcionális): a validációs szabályokra adott alapértelmezett hibaüzeneteket tudjuk itt felüldefiniálni.
  4. paraméter (opcionális): további egyéb leíró adatot adhatunk át neki, ami segítheti a működést (ezt mi nem fogjuk használni).

Nézzük meg a korábbi példa átültetését erre a gondolatmenetre a három paraméterrel:

Validator::make($request->all(), [
  'number' => 'required|max:255',
  'passenger_id' => 'required|integer|exists:passengers,id',
], [
  'number.required' => 'A Csomag szám mező kitöltése kötelező.',
  'number.max' => 'A csomag száma maximum 255 karakter hosszú lehet.',
  'passenger_id.required' => 'Az Utas megadása kötelező.',
  'passenger_id.integer' => 'Az Utas azonosítójának számnak kell lennie.',
  'passenger_id.exists' => 'Csak létező utast adhat meg.',
]);

Ez tehát nagyon hasonlóan működik és ugyanúgy használhatjuk alapértelmezetten a $request->validate(...) helyett. Viszont ez így még nem teljesen jó, ami pedig a két módszer különbségéből adódik...

Szerver oldali validáció két típusának összevetése

Céljuk szempontjából azonosak, de amíg az elsőnél ($request->validate(...)), ha elbukik a validálás, dob egy kivételt (exception-t), amit akkor rögtön visszaküld hibajelzésként a kliens felé és megjelenít a felhasználónak egy hibaüzenetet. A Validator::make(...) visszatérése viszont egy objektum a szerver oldalon, amivel utána nekünk még dolgoznunk kell és eldönthetjük, hogy mit csináljunk hibakezelésként, ha bukik a validáció. Például: mi dönthetjük el, hogy milyen címre (útvonalra) küldjük tovább a felhasználót, ha elbukott a validációja.

$validator = Validator::make(request()->all(), [
    'number' => 'required|max:255',
    'passenger_id' => 'required|integer|exists:passengers,id',
  ], [
    'number.required' => 'A Csomag szám mező kitöltése kötelező.',
    'number.max' => 'A csomag száma maximum 255 karakter hosszú lehet.',
    'passenger_id.required' => 'Az Utas megadása kötelező.',
    'passenger_id.integer' => 'Az Utas azonosítójának számnak kell lennie.',
    'passenger_id.exists' => 'Csak létező utast adhat meg.',
]);

if($validator->fails()) {
  return redirect(route('luggage.create'))
    ->withErrors($validator);
}

Itt a példában számozottan (és más háttérszínnel) jeleztem azokat a sorokat, amik relevánsan változtak. Látható, hogy az 1. sorban az említett visszakapott objektumot utána egy feltételvizsgálatnak vetjük alá (12-15. sorok): visszaküldjük a poggyászt létrehozó útvonalra a hibákkal és az input értékekkel együtt. Ahogy említettem, küldhetnénk másik útvonalra is, de akkor azon az útvonalon kellene vizsgálni a nézetben, hogy van-e olyan kódblokk, ami a hibák megjelenítését végzi. Jelenleg például hiába küldenénk a 'luggage.index' útvonalra, a hiba ott lenne megjeleníthetően, viszont azon az útvonalon a nézetben nincs hibamegjelenítő kód.

A hibamegjelenítő kódnál is van még lehetőségünk másképpen megtenni. Nézzük meg ezt is a luggage / create.blade.php nézetben: most ugye az űrlap felett van ez a kódrészlet, ami ilyen rövid űrlapnál rendben van, de ha egy nagyobb űrlapról van szó, vagy csak a problémás mezőhöz akarjuk írni a hibát, akkor ezt is meg lehet tenni:

Ennek a kódrészletnek az utolsó három sorára szeretném felhívni a figyelmet, amit a csomag száma input mező alá (vagy akár felé is...) tudunk betenni. Ez megvizsgálja, hogy van-e a number mezőhöz kapcsolódó hiba és ha van, akkor azt egy div-be megjeleníti nekünk. Így:

És ha már ezt megnéztük, akkor próbáljuk ki azt a Blade direktívát (az @if-en kívül), amit a hibák létezésének vizsgálatára hoztak létre. Működése ugyanaz, mint az iménti kódnak, csak talán egy kicsit komplettebb:

Itt tehát megint csak az utolsó három sor a fontos: ha a passenger_id-val van gond, akkor egy div-ben megjeleníti a hibának az üzenetét ($message, ez beépített, alapértelmezetten használatos változó). Teszteljük úgy, hogy a select vizsgálata után a lenyitáskor legelső helyen lévő option value-ját átírjuk megint 9999-re és Mentsük el az űrlapot.

A képen bennhagytam, hogy alul hogyan írtam át az option value értékét, felette pedig már az eredmény látható: 1. az űrlap felett van a hibalista minden hibaüzenettel, a Csomag száma alatt is ott a hibaüzenet és az Utas kiválasztó alatt is megvan az üzenet, így tehát már majdnem teljes a validációnk!

Ne vesszenek el a felhasználó jól kitöltött adatai...

De mi van akkor, ha nem támadólag akart fellépni a felhasználónk, hanem csak elrontott valamit...? Ha sok bemeneti mezőnk volt és csak egyet rontott el belőlük, amit a kliens oldali validációnk (hibásan) nem fogott el, csak a szerver oldali, akkor jó nagy bosszúságot okozunk a felhasználónknak, mert minden bemeneti mező (jelen esetben ugye csak az input és a select) alaphelyzetbe áll és újra kell kezdenie a felhasználónak az űrlap kitöltését... Oldjuk meg ezt, hogy ne érhessen senkit ekkora csalódás. Kezdjük a sima szöveges beviteli mezővel:

Az input mező value attribútumában az old() segédmetódussal helyezzük el a number nevű változó értékét. Ha volt ilyen régi érték (validáció előtti), akkor azt rakja bele, ha nem volt, akkor üresen hagyja a mezőt.

A tesztelése pedig úgy történhet meg, ha megadunk valamit a "Csomag száma" mezőbe, majd a select-et "hackeljük meg":

Az eredmény:

Megkapjuk a hibaüzenetet a select-re és szerencsére nem veszett el a "Csomag száma" mezőbe beírt adatunk sem.

Nézzük meg ennek a fordítottját, vagyis a select-be helyezzük el (selected option) a validáció előtt kiválasztott utast:

Ezt már használtuk korábban is, most csak annyi a trükk, hogy az old() segédmetódust alkalmazzuk itt is az elbukott szerver oldali validáció előtti érték "kinyerésére". Tesztelése úgy történhet meg, ha most a "Csomag száma" mezőt hagyjuk üresen, míg egy alapértelmezetten eltérőtől utast választunk ki a select-ben, például így:

Majd Mentés és a kapott eredmény:

Tehát megmaradt a kiválasztott utas, "hiába" rontottuk el az űrlap kitöltését.


Összefoglalás

A bejegyzésben megismertük a szerver oldali validáció lehetőségeit. Gyakorlati példákon keresztül mutattam be, hogy mi történik, amikor elbukik a validáció. Egyéni hibaüzeneteket adtunk meg az egyedi szabályok megsértéséhez. Többféle módon hackeltük meg a kliens oldali szabályokat ahhoz, hogy hozzáférjünk a szerver oldali szabályainkhoz. Megnéztünk alternatív megoldásokat a validáció végrehajtására a Controller releváns action-jében (store), közben pedig a hibák kliens oldali megjelenítésére is háromféle módot kipróbáltunk. Hogy ne vesszenek el kitöltött mezők adatai, megnéztük azt is, hogy milyen módon tudjuk megtartani őket a felhasználók számára.

A validációs minisorozat zárásaként már csak egy rész van hátra, amikor megnézzük, hogy hogyan hozhatunk létre egyedi szerver oldali szabályozásokat. Ez vár még ránk, mielőtt majd új témába kezdenénk a Laravel hatalmas témakörében.

A bejegyzéshez tartozó kódok ebben a Github commit-ben érhetők el.