csiszarattila.com / Rubysztán

A Rails munkamenet kezelése

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.

Mi az a munkamenet-kezelés?

„Amikor egy asztali (desktop) alkalmazással dolgozunk, megnyitjuk, változtatunk valamit, majd bezárjuk. A számítógép tudja, hogy ki végzi a műveletet. Tudja, amikor elindítjuk az alkalmazást, és amikor bezárjuk. De az interneten ez nem így van. A webszerver nem tudja, hogy kik vagyunk és mit csinálunk, mert a HTTP protokoll nem tartja meg a kérések közt az állapotát. Nagy Gusztáv: Web programozás jegyzet 0.7 - 8.2.6. fejezet, 171 .oldal

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.

Milyen munkamenet-kezelési megoldások vannak?

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.

Mutasd az URL-ed, megmondom ki vagy!

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.

Süssünk sütit!

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:

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 Rails munkamenet-kezelése

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.

Csacsog a felszín, hallgat a mély

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:

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.

A Rails alapértelmezett beállításai

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.

Munkamenet tárolási módok

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 három lehetőség közül nyilvánvalóan a memóriában való tárolás lesz a leggyorsabb - nincs felesleges I/O írás illetve olvasás.

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:

FileStore
A munkamenet-adatok szövegként való tárolását jelenti - a HTTP fejlécben formázottnak megfelelően tárolódnak.
PStore
Szintén fájlban való tárolást jelent, de bináris formában. Így lehetőségünk van objektumok, állapot alapú lementésére is (ha jól tudom ezt nevezik marshalling-nak).
Memory Store
Mint a neve is mutatja a munkamenetek adatai teljes egészében a memóriában tárolódnak el.

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:

CookieStore
A fentebb már említett, teljes egészében a sütikben való adattárolást jelenti.
DRbStore
Az ActionPack modul definiálja, gyors és ideális több processz között megosztandó adatok tárolására.
ActiveRecordStore
Adatbázisban való tárolást jelent. Ennek segítségével az ActiveRecord-os modelljeinkhez hasonlóan fognak a munkamenet-adatok kezelődni - számunkra ez teljesen transzparens lesz, hiszen a munkamenet-adatokat a fentieknek megfelelően továbbra is a session hash-el érhetjük el.

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.

ActiveRecordStore

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.

Az ActionController::InvalidAuthenticityToken kivétel feloldása

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.

Mit tegyünk, ha tiltottak a sütik

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.

Flash hash - a Rails egy speciális munkamenet eljárása

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.

További olvasnivalók

A Rails Wiki bejegyzése a munkamenetekről

A CookieStore megvalósítása

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