Ebben a cikkben igyekszem összefoglalni a Rails munkamenet kezelési megoldásának alapjait: hogyan működik, miként használhatjuk azt. Ha nem ismernéd pontosan mi az a munkamenet-kezelés és miért van rá szükségünk webalkalmazások fejlesztésekor a cikk elején összefoglaltam. Ha ezekkel tisztában vagy nyugodtan ugorj a Rails specifikus részekhez.
A HTTP protokollt állapotátmeneti protokollnak is nevezzük, mivel nem képes a kérések között egyértelmű kapcsolatot teremteni. A HTTP protokoll szabványa ezt nem írja elő, viszont sok esetben szükségünk van rá: ha egyértelműen meg tudnánk állapítani hogy ki intézte a HTTP kérést megtudnánk mondani, hogy korábban milyen műveleteket végzett(pl. egy webáruháznál milyen termékeket helyezett egy kosárba) vagy, hogy jogosult-e a művelet elvégzésére. Ennek leküzdésére az idők folyamán többféle megoldást is kitaláltak, amelyeket egységesen munkamenet kezelésként definiálunk.
A munkamenet-kezelések többsége valamilyen egyedi azonosító előállítására és továbbítására épül. Amikor a felhasználó belép vagy elindítja az alkalmazásunkat egy - annak lejáratáig mindenképp - egyedi azonosítót kap, amelyet valahogy a válaszokkal el kell küldenünk és minden kéréssel meg kell kapnunk.
Ennek a legegyszerűbb megoldása, ha a kérések URL-jében helyezzük el ezt az azonosítót. Mivel a felhasználó leggyakrabban linkeken keresztül kommunikál az alkalmazásunkkal gyakran találkozhatunk (korábban mindenképp) ilyen URL-kel:
http://szupertitkos-alkalamazas.com/index.php?PHPSESSID=04934454521c14300dd2faaa99deff
Ahol nyilvánvalóan az alkalmazás feladata, hogy az URL-t feldolgozva értelmezze azt, az előállított HTML válaszokban pedig a megfelelő azonosítókat elhelyezze a linkekben.
Ez a megoldás viszont több szempontból sem szerencsés. Egyrészt nem túl felhasználóbarát: zavaró, adott esetben túl hosszú URL-eket eredményez, amelyek ráadásul nem könyvjelzőzhetőek. Másrészt könnyebben kivannak téve rossz indulatú támadások célpontjának, tehát nem túl biztonságosak.
Arról nem is beszélve, hogy a felhasználó akár tudtán kívül is biztonságos vagy személyes információkat adhat át - akár ismerősének is - azzal, hogy a teljes URL-t elküldi neki, az ismerős pedig megnyitva azt belép az ő munkamenetébe. Ezért az egyedi azonosítót érdemesebb más módszerekkel átadni két kérés között.Az is nyilvánvaló, hogy ez a probléma csak a GET metódusú kéréseket érinti, ha POSTtal küldjük el a munkamenet-azonosítót akkor az nem fog megjelenni az URL-ekben.
A felhasználó böngészője viszont POST-olt adatokat csak űrlapelemekkel küldhet az alkalmazásunk számára úgy, hogy egy submit tipusú gombra kattint előtte - ekkor nyilvánvalóan célszerűbb a munkamenet-azonosítót rejtett űrlapelemként átadni a kérésekkel. De ez a tény nyilvánvalóan csak akkor segít, ha a felhasználónak egyébként is valamilyen űrlapot kell kitöltenie, máskülönben minden linkünket gomb elemre kellene cserélnünk, amit körbefogunk, egy a linkre mutató form elemmel.
De ne aggódjunk, létezik egy ennél sokkal jobb, transzparens megoldás.
A böngészők többsége ma már kivétel nélkül támogatja a sütiket (cookies), amelyek lehetővé teszik, hogy információt helyezzünk el a látogatónál, majd azt a következő kéréssel visszakapjuk.
Sütiket egyszerűen a HTTP válaszokkal küldhetünk a látogatónak. Például egy ilyen válasz fejléce a következőképpen nézhetne ki:
HTTP/1.1 200 OK Set-Cookie: azonositod=abcdef12345
Amikor a felhasználó böngészője értelmezi a HTTP válasz fejlécét és sütit talál benne eltárolja azt, semmi mást nem csinál vele.
Sokan vélik veszélyesnek a sütik használatát és blokkolják, de ez teljesen felesleges, mivel a szabvány pontosan előírja, hogy a böngészőknek csak tárolniuk kell azokat, majd a következő kérésekkel visszaadniuk a szervernek, nem kötelesek futtatni, így kártékony kódok elhelyezése is haszontalan bennük.A fentebb eltárolt sütit ekkor a böngésző visszaküldi a következő kérésével - összeveti, hogy a kért címhez tartozik-e eltárolt süti:
GET / HTTP/1.1 Cookie: azonositod=abcdef12345
A szerverünk számára mindez még ugyanúgy nem jelent semmilyen plusz információt, a mi dolgunk, hogy a süti adataiból valahogy munkamenetet varázsoljunk.
Ugyanakkor, mint láthattuk a sütik használatával két megoldás közül is válaszhatunk:
egyrészt akár elküldhetjük a teljes munkamenetet is, az összes adatával együtt. Nyilván ekkor érdemes figyelembe venni, hogy bizalmas adatokat csak kódolt formában továbbítsunk! Egyrészt a HTTP forgalmat bármikor lehallgathatják, így bizalmas információkhoz juthatnak hozzá illetéktelenek, ugyanakkor a sütik tartalmát a felhasználó is bármikor megtekintheti.
Sokszor veszélytelennek tűnő információk is okozhatnak gondokat például, ha egy webáruház kosárát szintén sütikben tároljuk egy leleményes felhasználó akár bele is piszkálhat a rendelési- végösszegbe elég komoly galibákat okozva.Mindkét megoldás rendelkezik előnyökkel és hátrányokkal, ezeket a Rails munkamenetkezelésének jellemzésekor bővebben is látni fogjuk.
A cikk első részében láthattuk, hogyan tudunk az állapotmentes HTTP protokollból azonosítható munkameneteket létrehozni. Mielőtt mélyebbre néznénk, hogy a Rails keretrendszer ezeket hogyan alkalmazza, érdemes a felszínen maradni és megnézni, hogyan rejti el mindezt a szemünk elől.
A Rails sok más keretrendszerhez hasonlóan egy központosított változót használ adatok elhelyezésére illetve kivételére a munkamenetek között. A session nevű hash tölti be ezt a szerepet - a PHP-ben például hasonló a $_SESSION nevű szuperglobális változó.
A session változót a hasheknek megfelelően tudjuk használni, így:
session[:user_id] = user.id
user_id = session[:user_id]
session[:user_id] = nil
Mindösszesen ennyit elegendő tudnunk, ha munkameneteket akarunk kezelni a Rails keretrendszerrel. Az ugyanis minden mást elintéz helyettünk, az alapértelmezett beállítások pedig megfelelnek a hatékony/biztonságos szűrőnek.
Mielőtt mélyebbre ásnánk a Rails munkamenet-kezelési beállításaiban érdemes tudnunk, hogy az teljes egészében a Ruby CGI moduljának Session osztályára épül.
A Rails alapértelmezettként - a 2.0-ás verzió óta - a munkamenetek-tartalmát sütiken keresztül továbbítja, így a szerver helyett a felhasználó böngészője tárolja az adatokat, azok a sütikben vándorolnak a kérések között.
Egy Rails alkalmazás HTTP válasza ez esetben a következőképpen nézhet ki, ha némi adatot helyezünk el a munkamenetben:
HTTP/1.1 200 OK Set-Cookie: _testapp_session=BAh7CDoMY3NyZl9pZCIlMmEyMzA5YmRjZmRiMjU4M2RkYzkxOWVhYm Y2ZGFj%0AMjE6DHVzZXJfaWQiBzIyIgpmbGFzaElDOidBY3Rpb25Db250cm9sbGVyOjpG%0AbGFzaDo6Rmxhc2h IYXNoewAGOgpAdXNlZHsA--5c40d34a7d2c084988efba2ba5fdfc17ed6f9244; path=/
Mint látható a munkamenetek biztonsági szempontból erősen titkosítva vannak. Vedd észre, hogy a karaktersorozat tartalmaz két elválasztó karaktert(--), az ez után álló karakterlánc a munkamenet-adatainak a hash összege, amit az alkalmazáshoz tartozó egyedi kulccsal képez. Ezt a config/environment.rb fájlban találjuk meg:
...
# Your secret key for verifying cookie session data integrity.
# If you change this key, all old sessions will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
config.action_controller.session = {
:session_key => '_webshop_session',
:secret => 'bba3ecbb125287724a7ed4107b5d467a653b85fc5a672b104f9237e00943c5658b169e3df97aaf322fe2bdb81a8b48fca27299c936232d0c1dfb4581bb965bee'
}
...
Ha szeretnénk megváltoztatni az alkalmazáshoz tartozó egyedi kulcsot, generálhatunk egyet a rake secret parancs segítségével.
A sütik titkosítása egy szintig elegendő biztonságot nyújt: a felhasználók nem tudják értelmezni, a munkamenetek feltörése és átírása a titkosítás erőssége miatt pedig szinte lehetetlen. Ugyanakkor nyilvánvaló hátrányokkal is rendelkezik: ha az alkalmazásban olyan változtatásokat végzünk, amelyek érintik a munkameneteket is, a felhasználóknál lévő elavult tartalmak miatt könnyen érvénytelen adatokat kaphatunk vissza, és a korábbiakat csak különböző trükkök bevetésével tudjuk megváltoztatni. Ráadásul a böngészők többsége mindösszesen 4KiByteban maximalizálja a sütikben tárolt adatok méretét, amit a CookieStore megoldás is korlátozásként alkalmaz.
A sütik idevágó szabványa (RFC2965) egyébiránt ajánlásként 20 db, egyenként minimum 4KiB-os sütit fogalmaz meg domainenként a böngészők számára.Nem kell azonban rögtön keresztet vetnünk, a Rails ugyanis biztosít számunkra többféle módot is a munkamenetek tartalmának tárolásához.
Az nyilvánvaló, hogy ha a munkameneteket nem a felhasználók, hanem a szerver oldalán szeretnénk tárolni akkor három lehetőségünk adódik:
A Railsben - legjobb tudomásom szerint - hat tárolási mód közül választhatunk. Ezek felét a Ruby CGI moduljának Session osztálya biztosítja a számunkra, úgymint:
A munkamenet-kezelés pontos működésről részletes információkhoz juthatunk, ha végigtanulmányozzuk a CGI modul Session osztályát.
A Rails három saját tárolási formával egészíti ki ezeket:
A CGI module Session osztálya ugyanakkor lehetővé teszi, hogy saját tárolási módokat is létrehozzunk.
A munkamenet-tárolási módok mindegyike nyilvánvalóan rendelkezik előnyökkel és hátrányokkal, közülük most az adatbázisban való tárolást szeretném kiemelni.
Ennek a tárolási módnak a használatához először létre kell hoznunk a munkamenet-adatok tárolására szolgáló táblát: a db:sessions:create rake taszk elvégzi helyettünk a szükséges séma előállítását, így csak futtatnunk kell a migrációt.
rake db:sessions:create rake db:migrate
A létrejövő táblázat egy nagyon egyszerű struktúrával rendelkezik:
create_table "sessions", :force => true do |t| t.string "session_id", :default => "", :null => false t.text "data" t.datetime "created_at" t.datetime "updated_at" end
A táblázat létrehozása után meg kell adnunk, hogy a keretrendszer az adatbázison keresztül kezelje a munkameneteket: a környezetünk konfigurációjában ( hogy az összesre érvényes legyen a config/enviroment.rb fájlban) a config.action_controller.session_store beállításnál az :active_record_store paramétert kell megadnunk:
# Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rake db:sessions:create") config.action_controller.session_store = :active_record_store
A szerverünk újraindítása után a sütikben már csak a munkamenetekhez rendelt azonosítók fognak utazni: ennek a segítségével fogja a keretrendszer a munkamenethez tartozó adatokat előszedni az adatbázisból. Ha törölni szeretnénk minden korábbi munkamenet adatát az adatbázisból futtassuk a db:session:clear rake taszkot.
Közvetlenül az adatbázisos munkamenet tárolásra való átállás után botlottam a fenti kivételbe. Az elsőre érthetetlennek tűnő probléma gyorsan orvosolható: az app/controllers/application.rb fájlban vegyük ki a komment jelet a protect_from_forgery metódus után álló secret szimbólum1 elől.
...
# See ActionController::RequestForgeryProtection for details
# Uncomment the :secret if you're not using the cookie session store
protect_from_forgery 1:secret => 'f62528d702c4231e586e9d688f824a8e'
...
A Rails ugyanis az ActiveRecordos munkamenet kezelésre való átállás után minden űrlapban elhelyez egy titkosított kódszót(Authenticity Token) rejtett űrlapmezőként, majd a kérések fogadásakor ellenőrzi azokat, ezzel megelőzve, hogy - a GET metódusos kérések kivételével - kéréseket lehessen küldeni az alkalmazáson kívülről is mintegy védekezésképp a CSRF (Cross-Site Request Forgery) támadások ellen. Ha konkrétabban is érdekel a probléma és magának a támadásnak a leírása az API ide vágó részét érdemes elolvasni.
Mint láthattuk a Rails munkamenet-kezelése a sütikre épül azonban ha a felhasználó böngészőjében a sütik-kezelése tiltva van ez nem működőképes megoldás. A keretrendszer ekkor az űrlapokban automatikusan minden válasszal generál egy rejtett form elemet is, amely így visszaküldheti a munkamenet-azonosítót vagy a munkamenet-adatokat a következő kéréssel a szervernek - a CGI modul valójában automatikusan elvégzi ezt a műveletet helyettünk. Ugyanakkor ez csak az űrlappal elküldött kéréseket érinti, ha URL-ben paraméterként szeretnénk továbbítani a munkamenet-azonosítót arról magunknak kell gondoskodnunk.
Szorosan a Rails munkameneteihez tartozik a flash hash használata. Bizonyos esetekben ugyanis szükségünk van arra, hogy üzeneteket tudjunk átadni két átirányított esemény között, például hogy hibaüzeneteket vagy információkat közöljünk a felhasználóval.
Azonban mivel minden átirányítás során a Rails új vezérlő objektumokat hoz létre és így az előzőleg létrehozott példányváltozók elvesznek, ezeket kénytelenek vagyunk a munkamenetben elhelyezni, majd minden lehíváskor törölni az eredeti üzenetet. A Rails ezt a folyamatot teszi automatikussá(szükségtelenné) a flash hash használatával.
A flash változót ugyanúgy használhatjuk, mint a session változót, azzal a különbséggel, hogy a benne tárolt adat mindösszesen egy válaszig őrzi meg tartalmát, utána automatikusan törlődik.
A flash rendelkezik továbbá két segéd metódussal: flash#new[] és flash#keep(). Az előbbi a flash tartalmát csak az éppen futó eseményig tárolja el, nem adja át azt egy következőnek a munkamenetben. Míg utóbbi megőrzi a már létező flash tartalmat a következő átirányításig is.
A Rails Wiki bejegyzése a munkamenetekről
Biztonsági kockázatok a CookieStore-al kapcsolatban.Főként linkekkelt teletűzdelt, de hasznos összefoglaló a Rails munkameneteiről.
Kicsit régi cikk, de teljesítménytesztekkel mutatja be a tárolási módokat