Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Session handler
Forum PHP.pl > Forum > PHP > Pro > Archiwum Pro
Prph
Witam,

W PHP5 zaawansowane programowanie autorzy przedstawili mechanizm obslugi sesji, ktory trzymal zmienne w osobnej tabeli. Dzieki tezmu wydajnosc miala nieco wzrosnac.

Takie rozwiazanie jednak ma pewna wade - zmienne pobierane za z sesji poprzez metody __get i __set, nie zaś $_SESSION['zmienna'];

W chwili obecnie przysparza mi to więcej problemów niż mogłem to sobie wyobrazić.

Jak Wy organizujecie session handlera? Jest sens dzielenia sesji na zmienne i info o sesji?

Pozdrawiam,
Adrian.
aleksander
poszukaj na tym forum o phiendzie2, sciagnij kod (w temacie w moim poscie jest link) i zobacz jak tam wygląda session handler (w HttpContext)

pozdrawiam
splatch
Mi się podoba idea scope'ów. Polega ona na tym, że zależnie od rodzaju scope'a dane są utrwalane na określony czas - Application Scope = na stałe, Session Scope, Page Scope = sesja w obrębie strony oraz Request Scope.
Prph
Jasne, mow mi wiecej winksmiley.jpg
matid
Cytat(Prph @ 2006-03-22 21:14:12)
Jasne, mow mi wiecej winksmiley.jpg

Polecam więcej literatury traktującej o teorii programowania, wzorcach projektowych, aplikacjach biznesowych, itd.
Może się tak wydawać, ale programowanie to nie tylko klepanie kodu na klawiaturze winksmiley.jpg
Prph
Doskonale Cię rozumiem, ale ja ostatnio non stop siedze i czytam winksmiley.jpg
Nie miałbym problemów, jakbym sie nie uczepił rozwiązania przedstawionego w książce PHP5, Zaawansowane programowanie. Nadal uważam, że jest ono dobre, ale troche zakręcone.

Napisałem więc raczej prosty session handler. Lekkie modyfikacje, zeby był wygodniejszy. Kod poniżej:

  1. <?php
  2.  
  3. class NotLoggedInSessionException extends Exception
  4. {
  5. }
  6.  
  7. class Session
  8. {
  9. private $_oDatabase;
  10.  
  11. private static $_oInstance;
  12.  
  13. private $_sPHPSessionId;
  14. private $_iNativeSessionId;
  15. private $_iSessionLifespan = 3600;
  16. private $_iSessionTimeout = 60;
  17.  
  18. private $_iUserId;
  19. private $_bIsLoggedIn;
  20.  
  21. private function __construct()
  22. {
  23. $this->_oDatabase = Database::getInstance();
  24. $this->_bIsLoggedIn = false;
  25.  
  26. (
  27. array(&$this, '_sessionOpen'),
  28. array(&$this, '_sessionClose'),
  29. array(&$this, '_sessionRead'),
  30. array(&$this, '_sessionWrite'),
  31. array(&$this, '_sessionDestroy'),
  32. array(&$this, '_sessionGC')
  33. );
  34.  
  35. try
  36. {
  37. $this->_sessionGC();
  38. $this->_checkForActiveSession();
  39. }
  40. catch (Exception $oException)
  41. {
  42. throw $oException;
  43. }
  44.  
  45. session_set_cookie_params($this->_iSessionLifespan);
  46. }
  47.  
  48. public static function getInstance()
  49. {
  50. if(!isset(self::$_oInstance))
  51. {
  52. $sClassName = __CLASS__;
  53. self::$_oInstance = new $sClassName;
  54. }
  55.  
  56. return self::$_oInstance;
  57. }
  58.  
  59. public function _sessionOpen($sSavePath, $sSessionName)
  60. {
  61. return true;
  62. }
  63.  
  64. public function _sessionRead($sPHPSessionId)
  65. {
  66. try
  67. {
  68. if(!$this->_isSession())
  69. $this->_createNewSession($sPHPSessionId);
  70.  
  71. $sQuery = 'SELECT id, logged_in, user_id, vars
  72.  FROM session
  73.  WHERE phpsessid = "'.$sPHPSessionId.'"';
  74.  
  75. $this->_oDatabase->query($sQuery);
  76.  
  77. $aRow = $this->_oDatabase->fetch();
  78.  
  79. $this->_iNativeSessionId = $aRow['id'];
  80. $this->_bLoggedIn = $aRow['logged_in'];
  81. $this->_iUserId = $aRow['user_id'];
  82. }
  83. catch (Exception $oException)
  84. {
  85. throw $oException;
  86. }
  87.  
  88. return $aRow['vars'];
  89. }
  90.  
  91. public function _sessionWrite($sPHPSessionId, $sData)
  92. {
  93. $aToUpdate[] = array('vars', $sData);
  94.  
  95. try
  96. {
  97. $this->_oDatabase->update('session', $aToUpdate, 'phpsessid = "'.$this->_sPHPSessionId.'"');
  98. }
  99. catch (Exception $oException)
  100. {
  101. return false;
  102. }
  103.  
  104. return true;
  105. }
  106.  
  107. public function _sessionDestroy($sPHPSessionId)
  108. {
  109. $sQuery = 'DELETE FROM session
  110.  WHERE phpsessid = "'.$sPHPSessionId.'"';
  111.  
  112. try
  113. {
  114. $this->_oDatabase->query($sQuery);
  115. }
  116. catch (Exception $oException)
  117. {
  118.  throw $oException;
  119. }
  120.  
  121. return true;
  122. }
  123.  
  124. public function _sessionGC($iMaxLifeTime = null)
  125. {
  126.  
  127. $sQuery = 'DELETE FROM session
  128.  WHERE
  129.  ((now() - start) > '.$this->_iSessionLifespan.') OR
  130.  ((now() - last_activity) > '.$this->_iSessionTimeout.')';
  131.  
  132. try
  133. {
  134. $this->_oDatabase->query($sQuery);
  135. }
  136. catch (Exception $oException)
  137. {
  138.  throw $oException;
  139. }
  140.  
  141. return true;
  142. }
  143.  
  144. public function _sessionClose()
  145. {
  146. return true;
  147. }
  148.  
  149. private function _updateSession()
  150. {
  151. if(isset($this->_iNativeSessionId))
  152. {
  153. $aToUpdate[] = array('last_activity', 'now()', false);
  154.  
  155. try
  156. {
  157. $this->_oDatabase->update('session', $aToUpdate, 'id = "'.$this->_iNativeSessionId.'"');
  158. }
  159. catch (Exception $oException)
  160. {
  161. throw $oException;
  162. }
  163. }
  164. }
  165.  
  166. private function _isSession()
  167. {
  168. if(isset($this->_iNativeSessionId))
  169. return true;
  170. else
  171. return false;
  172. }
  173.  
  174. private function _createNewSession($sPHPSessionId)
  175. {
  176. $sUserAgent = $_SERVER['HTTP_USER_AGENT'];
  177.  
  178. $aToInsert[] = array('phpsessid', $sPHPSessionId);
  179. $aToInsert[] = array('start', 'now()', false);
  180. $aToInsert[] = array('user_agent', $sUserAgent);
  181.  
  182. try
  183. {
  184. $this->_oDatabase->insert('session', $aToInsert);
  185.  
  186. $sQuery = 'SELECT id FROM session WHERE phpsessid = "'.$this->_sPHPSessionId.'"';
  187. $this->_oDatabase->query($sQuery);
  188.  
  189. $aRow = $this->_oDatabase->fetch();
  190. $this->_iNativeSessionId = $aRow['id'];
  191.  
  192. $this->_sPHPSessionId = $sPHPSessionId;
  193. }
  194. catch (Exception $oException)
  195. {
  196.  throw $oException;
  197. }
  198. }
  199.  
  200. private function _checkForActiveSession()
  201. {
  202. $sUserAgent = $_SERVER['HTTP_USER_AGENT'];
  203.  
  204. if(!empty($_COOKIE['PHPSESSID']))
  205. {
  206. $this->_sPHPSessionId = $_COOKIE['PHPSESSID'];
  207.  
  208. $sQuery = 'SELECT id
  209.  FROM session
  210.  WHERE
  211.  phpsessid = "'.$this->_sPHPSessionId.'" AND
  212.  (
  213.  (now() - start) < '.$this->_iSessionLifespan.' AND
  214.  user_agent = "'.$sUserAgent.'" AND
  215.  (now() - last_activity) <= '.$this->_iSessionTimeout.'
  216.  ) OR
  217.  last_activity IS NULL';
  218.  
  219. try
  220. {
  221. $this->_oDatabase->query($sQuery);
  222.  
  223. if($this->_oDatabase->numRows() === 0)
  224. {
  225. unset($_COOKIE['PHPSESSID']); // session is not valid or actual
  226. }
  227. else
  228. {
  229. $aRow = $this->_oDatabase->fetch();
  230. $this->_iNativeSessionId = $aRow['id'];
  231. $this->_updateSession();
  232. }
  233. }
  234. catch (Exception $oException)
  235. {
  236. throw $oException;
  237. }
  238. }
  239. }
  240.  
  241. public function isLoggedIn()
  242. {
  243. return $this->_bIsLoggedIn;
  244. }
  245.  
  246. public function getUserId()
  247. {
  248. if($this->isLoggedIn())
  249. return $this->_iUserId;
  250. }
  251.  
  252. public function getUserObject()
  253. {
  254. if($this->isLoggedIn())
  255. {
  256. try
  257. {
  258.  $oUser = new User($this->getUserId());
  259.  return $oUser;
  260. }
  261. catch (Exception $oException)
  262. {
  263. throw $oException;
  264. }
  265. }
  266. else
  267. throw new SessionException('Can not return object. User was not logged in');
  268. }
  269.  
  270. public function login($sLogin, $sPlainPassword)
  271. {
  272. $sPassword = md5($sPlainPassword);
  273.  
  274. $sQuery = 'SELECT id FROM users
  275.  WHERE
  276.  login = "'.$sLogin.'" AND
  277.  password = "'.$sPassword.'"';
  278.  
  279. try
  280. {
  281. $this->_oDatabase->query($sQuery);
  282. if($this->_oDatabase->numRows() == 1)
  283. {
  284. $aUser = $this->_oDatabase->fetch();
  285. $this->_iUserId = $aUser['id'];
  286.  
  287. $aToUodate[] = array('logged_in', 'true');
  288. $aToUodate[] = array('user_id', $this->_iUserId);
  289.  
  290. $this->_oDatabase->update('session', $aToUodate, 'id = "'.$this->_iNativeSessionId.'"');
  291.  
  292. $this->_bIsLoggedIn = true;
  293.  
  294. return true;
  295. }
  296. }
  297. catch (Exception $oException)
  298. {
  299.  throw $oException;
  300. }
  301. }
  302.  
  303. public static function logout()
  304. {
  305. if($this->isLoggedIn())
  306. {
  307. $aToUodate[] = array('logged_in', false);
  308. $aToUodate[] = array('user_id', 0);
  309.  
  310. try
  311. {
  312. $this->_oDatabase->update('session', $aToUodate, 'id = "'.$this->_iNativeSessionId.'"');
  313.  
  314. $this->_bIsLoggedIn = false;
  315. return true;
  316. }
  317. catch (Exception $oException)
  318. {
  319.  throw $oException;
  320. }
  321. }
  322. }
  323. }
  324. ?>


Przepraszam, że bez komentarzy, ale zamieszczam opis metod:

Konstruktor: sprząta stare sesje. Wywoluje metodę _checkForActivesession a następnie uruchamia sesję.

_checkForActivesession: sprawdza, czy użytkownik jest w trakcje sesji - na podstawie ciasteczka. Pobiera id sesji z ciastka i sprawdza w bazie czy jest jeszcze prawdziwa. Jezeli nie, usuwa ciastko. Jezeli sesja jest poprawna - odswieza jej czas metoda _updateSession.

_sessionRead: wywoływana przez php. Sprawdza czy, jest sesja (jeżeli poprzednia metoda stwierdziała, że sesja jest poprawna, to także ustawiła id sesji). Jeżeli nie - tworzy nową za pomocą metody _createNewSession. Dalej pobiera dane sesji z bazy, ustawia id uzytkownika i inne dane, a następnie zwraca zmienne sesji.

_sessionWrite: zapisauje zmienne sesji w bazie.

login, logout, getUserObject to dodane przeze mnie metody, aby łatwiej zarządzać sesją winksmiley.jpg

CZekam na Wasze komentarze.
Pozdrawiam, Adrian.
Ociu
Ja podzieliłem sesje na: User Sessions, czyli sesje jak każde inne, zapisuje się tam np. czy użytkownik jest zalogowany i Attributes Sessions, które przenoszą dane z modelu do akcji, która przekazuje je do widoku.
Prph - sesje jak każde inne, mysle, że połaczenie logowania z klasą sesji nie jest zbyt dobrym pomysłem.
Prph
A dlaczego?
Takie rozwiązanie jest wygodne. Jeżeli spojrzysz na to w ten sposób, że zalogowany user to przecież też sesja, tyle że lepsza winksmiley.jpg to można przyjąć, że logowaniem zajmie sie obiekt sesji.

Co prawda chciałem przeniesc logowanie do klasu Auth, ale pojawiłby się mały problem - info, czy użytkownik jest zalogowany powinno być trzymane w obiekcje sesji. Natomiast, jezeli logowala by klasa Auth to musialaby poinformowac obiekt sesji, ze user jest zalogowany. Jak dla mnie to jedyne rozwiazanie takie: $oSession->setUserLoggedIn(true); a to oznacza, ze metoda jest publiczna. Oczywiste jest, ze nie powinna byc publiczna :/

Jezeli przeniose login do klasy sesji to problem znika. A z racj, z jednak bardziej intuicyjne wydaje sie logowanie w klasie Auth, Auth posiada metoda login taka:

  1. <?php
  2. public function login($l, $p)
  3. {
  4. $this->oSessInstance->login($l, $p);
  5. }
  6. ?>


Pozdrawiam.


}
Vomit
Po co pisac wlasna obsluge sesji? te wbudowane w php nie wystarcza?
Prph
Cytat(Vomit @ 2006-03-25 13:33:53)
Po co pisac wlasna obsluge sesji? te wbudowane w php nie wystarcza?

No wystarcza wystarcza, a widziales kiedys na stronie takie cos jak:

Obecni:
55 gości
12 zarejestrowanych użytkowników

Takie coś jest możliwe dzięki własnej obsługi sesji. Poza tym - przechowywanie sesji w bazie wydaje się bardziej bezpieczne.
sf
A mi sie wydaje, ze trzymanie sesji w bazie jest poprostu niepotrzebnym generowaniem zapytan SQL... na takie cos mozna sobie pozwolic w panelach administracyjnych, CRMach.

Pozatym nie trzeba miec wlasnej obslugi sesji by miec informacje ile osob jest zarejestrowanych, a ile gosci.. do tego wystarczy cos takiego jak napisal Ociu. Czyli mamy sobie klase np. CurrentUser, ktora odpowiada za uzytkownika, ktory chodzi po naszej stronie.
Prph
No zgodze sie smile.gif
Ale wlasna obsluga sesji wydaje sie o wiele bardziej ulatwiac prace we frameworku. Mozna oczywiscie pominac wykorzystanie bazy. Ale dzieji niej, wszystko robi sie szybciej.
hawk
@Prph:

1) Sesja, tak jak pisze Ociu, powinna być jednak oddzielona od uwierzytelniania. Bo jedno i drugie ma mało wspólnego. Można mieć przecież otwartą sesję nie będąc uwierzytelnionym (zalogowanym). Łącząc to bardzo się ograniczasz:
- tworzysz obiekt User, mimo że klasa sesji nie ma co z tym obiektem zrobić i w efekcie zwraca puste nie-wiadomo-co
- nie możesz wybierać źródła danych o użytkownikach (DB, pliki passwd, SMB i inne, LDAP, ...)
- nie możesz rozbudować uwierzytelniania chociażby o przypisanie użytkownika do grup, bo wszystko jest hard-coded w klasie sesji

2) Bug: wcale nie jest powiedziane, że session_name() == 'PHPSESSID', a twój kod to zakłada. Poza tym, nie jest powiedziane, że sesje przekazywane są przez cookies, chociaż to akurat założenie ma chyba wiele kilka zalet.

3) Sprawdzanie poprawności sesji (tutaj: user agent) nie powinno odbywać się w klasie sesji. Takie rozwiązanie powoduje, że:
- nie można tej funkcjonalności wyłączyć, gdy nie jest potrzebna
- nie można jej rozszerzyć, gdy nie jest wystarczająca
A przecież można zaprojektować to obiektowo, i dodawać do obiektu sesji obiekty "walidatorów", które sprawdzałyby poprawność i ew. zapisywały sobie user agent, referrer, IP lub inne dane potrzebne później do walidacji. Prosto, łatwo, obiektowo smile.gif.

4) Bardzo mi się za to podoba usuwanie niepotrzebnego cookie oraz przede wszystkim wywalanie sesji, gdy w cookie przyszedł session id nie istniejący w bazie. Domyślnie php (na plikach) tworzy wtedy nową sesję z podanym ID i dziura w bezpieczeństwie gotowa smile.gif

5) Nie podoba mi się natomiast wywoływanie metody _checkForActiveSession() z konstruktora. To zdecydowanie za wcześnie. Po pierwsze, nie wiadomo, czy sesja na pewno będzie na tej stronie potrzebna (może obiekt był utworzony na zapas, a session_start() nigdy nie będzie wywołane?). Po drugie, nie wiadomo, jaka będzie nazwa sesji. Weźmy taki kod:
  1. <?php
  2. $s = new Session();
  3. session_name('foo');
  4. ?>

Powyższy kod zakończy się pewnie katastrofą.

6) W metodzie _readSession, po stworzeniu nowej sesji nie ma sensu odczytywać jej z bazy smile.gif
Prph
No i to są bardzo dobre wskazówki - Hawk - dziękuję.
Co do PHPSESSID - wiedzialem o tym winksmiley.jpg
co do walidatorów - świetne rozwiązanie.

Witam ponownie.
Usiadlem dzisiaj do klasy sesji i zaczalem modyfikowac ja wedlug cennych uwag Hawka.

Cytat
"Można mieć przecież otwartą sesję nie będąc uwierzytelnionym (zalogowanym). Łącząc to bardzo się ograniczasz:
- tworzysz obiekt User, mimo że klasa sesji nie ma co z tym obiektem zrobić i w efekcie zwraca puste nie-wiadomo-co
- nie możesz wybierać źródła danych o użytkownikach (DB, pliki passwd, SMB i inne, LDAP, ...)
- nie możesz rozbudować uwierzytelniania chociażby o przypisanie użytkownika do grup, bo wszystko jest hard-coded w klasie sesji"


ad 1. - jak tworze pusty obiekt. Wedlug moich zalozen zawsze utworzony zostanienie obiekt uzytkownika.
ad2. Ale to nie sesja chyba o tym powinna decydowac - tym zajmuje sie klasa User. Ona sama siebie uzupelnia danymi.
ad3. Grupy do jakich zapisany jest uzytkownik przechowuje klasa uzytkownika. Dodam kod:

  1. <?php
  2.  
  3. class User
  4. {
  5. private $_oDatabase;
  6.  
  7. private $_sLogin;
  8. private $_sFirstName;
  9. private $_sLastName;
  10.  
  11. private $_iUserId;
  12.  
  13. private $_aGroups;
  14.  
  15. public function __construct($iUserId)
  16. {
  17. $this->_oDatabase = Database::getInstance();
  18.  
  19. $this->_iUserId = $iUserId;
  20.  
  21. $sQuery = 'SELECT login, first_name, last_name, groups
  22.  FROM users
  23.  WHERE id="'.$iUserId.'"';
  24.  
  25. try
  26. {
  27. $this->_oDatabase->query($sQuery);
  28.  
  29. if($this->_oDatabase->numRows() == 1)
  30. {
  31. $aRow = $this->_oDatabase->fetch();
  32.  
  33. $this->_sLogin = $aRow['login'];
  34. $this->_sFirstName = $aRow['first_name'];
  35. $this->_sLastName = $aRow['last_name'];
  36. $aGroups = $this->_groupsToArray($aRow['groups']);
  37.  
  38. if(count($aGroups))
  39. $this->_aGroups = $aGroups;
  40. }
  41. }
  42. catch (Exception $oException)
  43. {
  44. throw $oException;
  45. }
  46. }
  47.  
  48. public function getId()
  49. {
  50. return $this->_iUserId;
  51. }
  52.  
  53. public function getFullName()
  54. {
  55. return $this->_sFirstName.' '.$this->_sLastName;
  56. }
  57.  
  58. public function getGroups()
  59. {
  60. if(isset($this->_aGroups))
  61. return $this->_aGroups;
  62. else
  63. return false;
  64. }
  65.  
  66. private function _groupsToArray($sGroups)
  67. {
  68. $sGroups = strtolower($sGroups);
  69. $aGroups = explode(',', $sGroups);
  70.  
  71. return $aGroups;
  72. }
  73. }
  74.  
  75. ?>


Pozdrawiam.
hawk
@Prph: Teraz sprawa się komplikuje, bo dochodzi jeszcze cała klasa User, a ja generalnie nie lubię klas o nazwie User smile.gif

ad. 1. Pisząc "pusty obiekt" miałem na myśli obiekt klasy User, nie trzymający żadnych informacji oprócz ID usera. Patrząc na konstruktor klasy User, wycofuję to zdanie.

ad. 2. Tak, to nie sesja powinna decydować o tym, skąd wziąć "dane" użytkownika. Ale też nie powinna o tym decydować - o dziwo - klasa User. W twoim przykładzie jest to zaszyte częściowo w klasie sesji (sprawdzanie hasła i logowanie), a częściowo w klasie User (wyciąganie grup).

Teraz kilka słów natury ogólnej:

Klasa sesji, session handler, itd. powinny być minimalistyczne i nie robić nic poza utrzymywaniem i obsługą samej sesji, ponieważ wykorzystywane są często i wszelka dodatkowa funkcjonalność nas ogranicza. Przykładem jest twoja klasa sesji: umieszczając w niej kod sprawdzający poprawność hasła użytkownika pozbawiłeś się możliwości walidacji tego hasła na podstawie np. LDAP. Chcąć użyć tego nieszczęsnego LDAPa będziesz musiał napisać drugą klasę sesji, ze zmienioną funkcją do logowania. I w tym momencie okaże się, że najlepiej wyrzucić logowanie do osobnego obiektu i podmieniać w ramach potrzeb smile.gif

Klasy User, jak napisałem, nie lubię. Każda aplikacja może miec specyficzne dane, które związane są z użytkownikiem: e-mail, nr GG, avatar... Wspólnym mianownikiem jest tutaj konieczność uwierzytelniania użytkownika, a zatem login, hasło i - w zależności od przyjętego modelu uwierzytelniania - grupy, role, itd. To nie jest użytkownik! Najlepszym znanym mi słowem jest angielskie Credentials. Ewentualnie token, passport, itd.

Ale umieszczanie w tej klasie funkcjonalności pobierającej listę grup jest złe, ponieważ znowu ograniczamy się do jednego źródła danych. Jeżeli grupy użytkownika też zaczniemy przechowywać na serwerze LDAP, będziemy musieli napisać klasę LDAPUser i używać zamiast zwykłego User.
Prph
Widze, ze Hawk chcialbys napisac narzedzie idealne :]

Ja pisze framework (bez obrazy) - nie dla Ciebnie, czy pana Kazia. On bedzie dla mnie.
Kolejna sprawa - moze kiedys przeprawie go tak, zeby byl bardziej elastyczny. Moze jak wiecej sie naucze.
I ostatnia - jak napisze framework, gdzie session handler bedzie maksymalnie podstawowy, wiekszosc klas takze maksymalnie podstawowa, to jak przyjdzie mi pisac jakas aplikacje na frameworku to okaze sie, ze zamiast skupic sie na kodzie mojej palikacji, to bede rozbudowywal po kolei kazda klase, bo one w koncu nie dzialaja :/

Pozdrawiam :]
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2024 Invision Power Services, Inc.