ugrás a tartalomhoz

Munkamenet kezelés alapjai

Hodicska Gergely · 2004. Május. 13. (Cs), 21.00
Munkamenet kezelés alapjai
Sokéves PHP levelezőlistás tapasztalatom alapján nyugodtan kijelenthetem, hogy a PHP nyelvvel ismerkedők számára az egyik legnagyobb misztérium a munkamenet kezelés. Pedig valójában egy nagyon egyszerű és gyakran nélkülözhetetlen eszközről van szó, mint ezt a következőkben reményeim szerint kiderül. Cikkem első részében a munkamenet kezelés alapjairól lesz szó, míg a következő részben a biztonság kérdését boncolgatjuk majd, megtudhatjuk, milyen fenyegetettségek léteznek a munkamenet kezeléssel kapcsolatban, és hogyan védekezhetünk ez ellen.

A sorozatban megjelent

Miért kell ez nekünk?

Egyből adódik a kérdés, hogy egyáltalán miért is van szükségünk munkamenet kezelésre. A kérdés megválaszolása előtt nézzük meg, hogy hogyan is zajlik a kommunikáció a böngészőnk és a programunkat futtató szerver között. Amikor megtekintünk egy weboldalt, egy párbeszéd játszódik le a két fél között, melynek a nyelve a HTTP protokoll. Ez a párbeszéd kérés/válasz formájában történik. Például ha beírjuk a böngészőnkbe, hogy http://felho.hu/cikkek.php, akkor az először kapcsolódik a felho.hu-t szolgáltató számítógép 80-as portjához és a következő kérést küldi:

GET /cikkek.php HTTP/1.1
HOST: felho.hu
Ennek hatására a webszerver a PHP értelmező segítségével lefuttatja a cikkek.php scriptet, majd ennek kimenetét visszaküldi a böngészőnek a következő formában:

HTTP/1.x 200 OK
Server: Apache/2.4.12 (Unix) Debian GNU/Linux PHP/6.0.0
Last-Modified: Sun, 25 Apr 2006 13:56:49 GMT
Content-Length: 177
Content-Type: text/html; charset=iso-8859-2

<html>
    <head>
        <title>Cikkek</title>
    </head>
    <body>
        <a href="/cikk.php?id=1">1. cikk</a>
        <a href="/cikk.php?id=2">2. cikk</a>
    </body>
</html>
Ezt követően a kapcsolat megszűnik a böngésző és a szerver között, majd ha az oldalon rákattintunk valamelyik linkre, akkor ugyanez a folyamat játszódik le újból. A két kérés között a webszerver képtelen bárminemű kapcsolatot teremteni, számára azok kiszolgálása egymástól teljesen függetlenül zajlik. Ezért hívják a HTTP protokollt állapotmentes protokollnak, ellentétben például az FTP protokollal, mely folyamatos kapcsolatot biztosít mindaddig, amíg valami hiba nem történik, vagy nem szakítjuk meg a kapcsolatot.

A valós életben ezzel szemben gyakorta fordulnak elő olyan helyzetek, amikor a felhasználók kényelmi, illetve alkalmazásunk funkcionális igényei megkövetelik, hogy valamilyen úton-módon a fenti működéssel ellentétben képesek legyünk kapcsolatot teremteni a látogató által megtekintett oldalak között

Megoldás?

Az ötlet rendkívül egyszerű: amikor a látogató először érkezik oldalunkhoz, generálunk egy azonosítót, amit eljuttatunk a klienshez oly módon, hogy azt a kliens a látogató következő oldal lekérésekor eljutassa a szerverre. Ha érkezik egy ilyen azonosító, akkor gondoskodunk róla, hogy az ismét eljusson a klienshez. Ezután egy ilyen azonosító (sessionId) egy adott felhasználó munkamenetét fogja jelképezni, és ezen azonosító alapján a szerveren állapotinformációkat, adatokat tárolhatunk. A sessionId tetszőleges karaktersorozatból állhat, de megfelelő generálása kulcsfontosságú lehet munkamenet kezelésünk biztonságosságának szempontjából, de erről majd később bővebben szó esik.

Hogyan továbbítsuk a sessionId-t?

A probléma kezelésére bármilyen mechanizmus alkalmas lehet, ami biztosítja azt, hogy a kliens számára eljuttatott azonosító visszakerüljön a szerverre. Először tekintsük meg a kézenfekvő lehetőségeket, azokat a módszereket, melyeket amúgy is arra használunk, hogy adatokat jutassunk el a klienstől a szervernek, majd később érdekességképpen bemutatok egy kicsit egzotikusabb lehetőséget is:
  • sessionId továbbítása URL-en keresztül (GET mód): az oldalunkon található minden egyes link végéhez hozzáfűzzük a munkamenet azonosítót:
    cikk.php?id=1&sessionId=krixkrax
  • sessionId továbbítása rejtett űrlap mezőn keresztül (POST mód): az oldalunkon található minden egyes űrlapot kiegészítünk egy plusz rejtett mezővel:
    <form method=”post”>
    ...eredeti tartalom...
    <input type=”hidden” name=”sessionId” value=”krixkrax”>
    </form>
  • sessionId továbbítása süti használatával (Cookie mód): a szerver az első oldal megtekintésekor beállít egy sütit, amit a böngésző minden egyes további kéréskor elküld a szervernek:
    1. kérés:
    
    GET /cikkek.html HTTP/1.1
    HOST: felho.hu
    
    Válasz:
    
    HTTP/1.x 200 OK
    Server: Apache/2.4.12 (Unix) Debian GNU/Linux PHP/6.0.0
    Last-Modified: Sun, 25 Apr 2006 13:57:03 GMT
    Set-Cookie: sessionId=krixkrax
    Content-Length: 177
    Content-Type: text/html; charset=iso-8859-2
    
    
    <html>
        <head>
            <title>Cikkek</title>
        </head>
        <body>
            <a href="/cikk.php?id=1">1. cikk</a>
            <a href="/cikk.php?id=2">2. cikk</a>
        </body>
    </html>
    
    2.kérés:
    
    GET /cikkek.html HTTP/1.1
    HOST: felho.hu
    Cookie: sessionId=krixkrax
    

Ismét adódik a kérdés, hogy ezen lehetőségek közül melyiket is válasszuk. Ha a biztonságosságot tartjuk szem előtt, akkor elmondható, hogy a különböző támadási módok általában alkalmazhatóak mindegyik módszer esetén, ezért érdemesebb praktikus szempontok alapján döntenünk.

Ha az azonosítót URL-ben tároljuk, akkor az könnyen kerülhet illetéktelen kezekbe, akár csak egy böngésző által küldött „REFERER” mezőben. Ezen kívül ez a tárolási mód a könyvjelzők „ellensége” is lehet. Például bejelentkezünk egy oldalra, ahol ezáltal egy új munkamenet nyitunk. Nagy nehezen megtaláljuk azt az információt, amire szükségünk van, örülünk (Vincent?), elmentjük a linket, benne a munkamenet azonosítóval. Ha a munkamenet érvényességének lejárta után próbálunk rákattintani a linkre, meglepve tapasztaljuk, hogy a kívánt tartalom helyett a bejelentkezési oldalra kerülünk.

Űrlapok használatára eleve nem mindig van lehetőségünk, így ez nem járható mód, így ha tehetjük érdemes a munkamenet azonosítót sütiben tárolni, és csak ha erre nincs lehetőségünk (kliens nem kezeli vagy csak le van tiltva) akkor használni az egyéb lehetőségeket. Sütiben való tárolás esetén például az előbb vázolt szituáció: bejelentkezünk, linket elmentjük. Ha legközelebb szükségünk van rá, akkor ismét bejelentkezünk, az új munkamenet azonosító bekerül a sütibe, majd a linkre kattintva a kéréssel együtt elküldésre kerül a szerver felé, és máris a kívánt oldalon vagyunk.

Munkamenet kezelés PHP-vel

A PHP 4-es verziója előtt a munkamenet kezelést a programozónak kellett megoldania, nem volt hozzá beépített támogatás. Mindenki saját igényeinek, lehetőségeinek függvényében írt saját munkamenet kezelőt, vagy használt egy már kész megoldást (például PHPlib).

A PHP 4-es verziójának egyik nagy újdonsága volt, hogy rendelkezik saját munkamenet kezeléssel, ami ráadásul elég sokrétűen konfigurálható. Bár ezeknek egy részéről szó esik majd a későbbiekben, de mindenkinek melegen ajánlom, hogy olvassa el a PHP kézikönyv ide vonatkozó részét.

A PHP alapbeállítások mellett a sessionId sütiben való tárolását használja (php.ini: session.use_cookies opció), de engedélyezhetjük az egyéb módokon történő sessionId továbbítást is (php.ini: session.use_trans_sid opció). Ehhez a PHP hathatós segítséget képes nyújtani, ha engedélyezzük számára, ugyanis képes a scriptek által generált HTML kód-ban a php.ini url_rewriter.tags opciójában meghatározott HTML elemek automatikus módosítására, azokban a sessionId elhelyezésére. Például a linkek végéhez hozzáfűzi azt, vagy formok esetén egy rejtett mezőben helyezi el (vigyázzunk, mert META elemek használatakor nekünk kell biztosítani az azonosító továbbítását). Ha mind a süti, mind az egyéb módokon történő továbbítás engedélyezve van, akkor a PHP a következők szerint jár el: ha a kéréssel érkezik sessionId süti, akkor minden rendben, ha nem, akkor működésbe lép az azonosító különböző HTML elemekben való automatikus elhelyezése, valamint a kérésre adott válasz HTTP fejlécei közé bekerül a sessionId süti beállító is.

Munkamenet kezelés használata

Amennyiben szükségünk van munkamenet kezelésre, csak annyit kell tegyünk, hogy a programunkban kiadjuk a session_start() parancsot, majd ezt követően lehetőségünk van munkamenet adatok tárolására. Ezzel kapcsolatban egyetlen dologra kell figyeljünk: a parancs kiadását megelőzően a programunk nem generálhat kimenetet (kivéve output buffering használata), ugyanis a sessionId süti beállítása a Set-Cookie HTTP fejléc használatával történik, azonban amint programunk kimenetet generál, erre már nincs lehetőség.

Adatok elhelyezése a munkamenetben rendkívül egyszerű: a session_start() parancsot követően létrejön a $_SESSION nevű super global tömb, ami egyrészt tartalmazza a munkamenet során már korábban elhelyezett adatokat, valamint újabbakat tehetünk bele.

<?php //szamlalo.php
	session_start();
	if (!isset($_SESSION[’szamlalo’])) {
		$_SESSION[’szamlalo’] = 0;
}
$_SESSION[’szamlalo’]++;
echo $_SESSION[’szamlalo’];

// változó törlése
if ($_SESSION[’szamlalo’] == 20) {
	unset($_SESSION[’szamlalo’]);
}
?>
Ezenkívül lehetőségünk lehetne még használni a session_register(), session_is_registered() illetve a session_unregister() függvényeket (változó beállítására, meglétének ellenőrzésére, illetve törlésére), de ezek használata egyrészt elavult, másrészt szükséges hozzájuk a PHP register_globals beállításának On-ra állítása, amit jóérzésű PHP programozó úgyse tenne. Itt jegyezném meg, hogy vigyázzunk, függvények használata esetén még véletlenül se használjuk a global kulcsszót a $_SESSION tömbbel, mert különben bár értékadásaink látszólag érvényre jutnak, nem kerülnek tárolásra.

A $_SESSION tömben lévő adatokat a PHP alapbeállítás szerint a session.save_path opció által meghatározott könyvtárban tárolja: a sessionId értékének megfelelő nevű file-ba kerül a $_SESSION tömb serializált formája. Ez nem túl biztonságos megoldás, ezért ha a használt szerveren más felhasználók is vannak, célszerű egy külön könyvtárat megadni erre a célra, melyhez másnak nincs hozzáférése. Ebben az esetben is tartsuk szem előtt, hogy ezek a fájlok a webszerver jogosultságával jönnek létre, így bármely a szerveren futó más által írt, a webszerver által futtatott program képes ezek olvasására. Ha szükséges nagyobb biztonság biztosítása, akkor a PHP lehetőséget biztosít a munkamenet adatok tárolási mechanizmusának átdefiniálására. Segítségével például az érintett adatokat tárolhatjuk adatbázisban is, aminek a fokozott biztonság mellett másik előnye, hogy alakalmas lehet elosztott környezetben történő transzparens munkamenet kezelés megvalósítására. Erre a PHP kézikönyvben találunk példát, ezért ezzel kapcsolatban csak egy dolgot emelnék ki, ha adatbázis alapú munkamenet kezelés mellett döntünk, akkor a programunk befejezésekor hívjuk meg a session_write_close() függvényt, ezzel biztosan elkerüljük, hogy a munkamenet adatok még azelőtt mentésre kerülnek, mire a következő oldal kiszolgálása során a programunk újból használni szeretné azokat, ami az adatok inkonzisztens állapotba való kerülését eredményezheti.

Egy érdekes megközelítés

Cikkem első részének végén pedig következzen az ígért nem triviális lehetőség munkamenet kezelésre, felhasználók nyomon követésére. Mint az elején szó volt róla, bármely olyan mechanizmus jó lehet számunkra, amely biztosítja hogy egy a szerver által generált azonosítót a kliens visszaküldjön a következő kérés alkalmával. A bemutatandó módszer a HTTP protokoll cache-control fejléceit használja e cél megvalósítása érdekében. Ezek arra lennének hivatottak, hogy a böngésző meg tudja kérdezni a szervertől, hogy egy adott dokumentum megváltozott-e a szerveren, annak eldöntésére, hogy lekérje-e újból a szerverről, vagy használhatja a cache-ben tárolt verziót. Ezek a fejlécek a Last-Modified és az Etag, az előbbi elvileg a dokumentum utolsó változásának dátumát tárolja, míg az utóbbi egy tetszőleges szöveget. A kettő közül inkább az első támogatott a kliensek széles körében. Amikor először nézünk meg egy oldalt, a szerver elküldi ezeket a fejléceket a dokumentummal együtt, amit a böngésző eltárol a saját cache-ben. Ha újból meglátogatjuk az oldalt, akkor a böngésző (ha a felhasználó ezt nem írja elő másképp) mielőtt újból lekéri a teljes dokumentumot, elküldi ezen fejléceket a szervernek, ami ezen adatok alapján eldönti, hogy a dokumentum változott-e, és jelzi a böngészőnek, hogy az általa tárolt változat friss-e. Ennek a módszernek munkamenet kezelésre való használatát azonban több tényező nehezíti: felhasználói beállítás, kliens és szerver közötti esetleges proxy cache beállítás; de kitűnően alkalmas lehet a felhasználók webezési szokásainak titokban(!) történő megfigyelésére. További információk: Google :-).

Ezzel vége is az első résznek. A következőben, mint említettem, a munkamenet kezelés és a biztonság kérdése kerül terítékre, és már a haladók számára is fog érdekességeket rejteni.
 
1

eszrevetel, kerdes?

kmm · 2004. Május. 14. (P), 16.53
valahol hallottam, hogy ezt:
if( ! isset( $_SESSION[’szamlalo’] ) )
inkabb igy:
if( ! array_key_exists( "szamlalo", $_SESSION ) )
igaz ez?
ha igen miert, ha nem miert?
// tudom hogy phplistan volt rola szo, de nem mindenki olvassa a listat aki esetleg olvassa az oldalt.


--
üdv: kmm...
2

Sebesség?

Hojtsy Gábor · 2004. Május. 14. (P), 17.03
Én még nem hallottam ilyet, de szerintem sebesség oka lehet, és nem kapcsolódik a session-höz egyáltalán. Az első esetben van egy tömb elem keresés és egy 'függvény' hívás, míg a második esetben egy függvényhívás és az keres a tömbben. Lehet, hogy gyorsabb a második (mert optimalizált a tömb index keresésre, és nem veszi elő a tömbelem értékét), de az is lehet, hogy az első a gyorsabb, mert az isset() nyelvi szerkezet és nem olyan lassú, mint egy függvényhívás. Bár erre bbalint tudna válaszolni, ha olvassa. Majd megmondja, hogy miért nincs igazam.

Különben le lehet mérni :)
3

Ez: [code]if( ! isset( $_SES

tibcsi · 2004. Május. 14. (P), 18.52
Ez:
if( ! isset( $_SESSION['szamlalo'] ) )
kétszer gyorsabb, mint ez:
if( ! array_key_exists( "szamlalo", $_SESSION ) )

Lehet, hogy csak elgépelted, de vigyázz, mert a nagyon nem ugyanaz, mint a ' . Tehát ez (te ezt írtad):
if( ! isset( $_SESSION[’szamlalo’] ) )
lassabb, mint a fenti kettő összesen. :)
9

isset - array_key_exists

Anonymous · 2004. Május. 16. (V), 12.02
Szia!

Jelen esetben nincs semmi ertelme nem isset-et használni. És pont mivel egy nyelvi elem, szinte biztos, hogy gyorsabb is, mint egy függvényhívás. Az array_key_exists-nek akkor van létjogosultsága, ha olyan tömbindex létezeset szeretned vizsgalni, aminek az erteke null. Ilyenkor az isset false-t add vissza akkor is ha létezik ilyen indexu tombelem. Igaz, hogy ez talan kicsit furcsa mukodes, hiszen a null-t adni ertekul egy valtozonak, az nagyjabol egyenerteku az unset-tel, de ezek szerint nem teljesen.

Felho

Felho
4

Content-Length??

tibcsi · 2004. Május. 14. (P), 19.03
Nem kötekszem, csak egy kis javítás.
Content-Length: 177
Meglepne ha ilyet látnék egy php fájl elküldése előtt. Jó, persze simán lehetséges, de nem a legnormálisabb dolog. Amikor a PHP script indul még nem tudni, hogy mekkora lesz a kimenete.
5

Generált válaszban vannak

Hojtsy Gábor · 2004. Május. 14. (P), 19.16
Én úgy látom hogy ez két helyen van a cikkben, és mindkettő generált válasz, amit a böngésző kap meg, tehát nem a PHP futása előtt áll elő a tartalom hossza. Ezért nem értem, hogy mi a problémád.
6

Ha nem érted, mutatok egy p

tibcsi · 2004. Május. 14. (P), 20.41
Ha nem érted, mutatok egy példát: képzeld el ennek a scriptnek a futását:
Hány bájt lesz? <? echo date("s")%2==0 ? "Huszonöt!" : "19!";?>
Először ki kell írnia, hogy "Hány bájt lesz? ". Mielőtt ezt elküldené, el kell küldenie a headert. De ha most elküldi a headert, még nem tudja, hogy mennyi lesz a Content-Length. Így érthető?

Ha nem hiszed, akkor tegyél a szerver gyökerébe egy cikkek.php fájlt, azzal a tartalommal, ami a példában van és küld el a "lekérdezést", ami a példában van. (Nem az én példám, hanem ami a cikkben van.) Ráadásul az még nem is lesz olyan valós, mert a cikkek.php-ben valószínűleg dinamikusan van generálva a cikklista, tehát még valószínűtlenebb, hogy a méret meglesz előre.
7

Szerver beállítás

Hojtsy Gábor · 2004. Május. 14. (P), 20.55
Ez szerver beállítás kérdése. Lehet bufferelni a kimenetet. Nem feltétlenül szükséges, hogy rögtön elküldjön mindent a PHP az Apachenak és az Apache a böngészőnek.
8

Gondoltam rá, hogy tegyek eg

tibcsi · 2004. Május. 14. (P), 21.30
Gondoltam rá, hogy tegyek egy flush-t a példába. :) (tudom, attól még az apache bufferelhet)
Megoldható, hogy buffereljen az apache vagy a php, de nem túl áltanos. Mindenesetre nem ér az az egy sor már rég ennyit. :)
10

Nyomtatás?

romleo · 2004. Május. 27. (Cs), 08.14
Elnézést, de lehet itt egy nyomtatóbarát verziót kapni a cikkekből?
Mert nem találom...
(Pedig még regisztráltami is magam.)
11

Sajnos még nincs...

Bártházi András · 2004. Május. 27. (Cs), 09.04
Sajnos még nincs, amint időt tudunk rá szakítani, lesz.

-boogie-