PHP/Sesje
Sesje
edytujProtokół HTTP jest protokołem bezstanowym. Oznacza to, że serwer WWW rozpatruje każde żądanie niezależnie od innych, nie szukając żadnych powiązań w stylu wysyłania ich przez tego samego internautę. Utrudnia to teoretycznie tworzenie wszelkich systemów autoryzacji, które wymagają śledzenia poczynań użytkownika na naszej stronie i przenoszenia jego danych autoryzacyjnych między kolejnymi żądaniami, czyli krótko mówiąc - wymagają obecności systemu sesji. Używając PHP lub innego dynamicznego języka server-side można je jednak zasymulować. "Nasz" język jest o tyle prosty, iż posiada już zaimplementowane stosowne funkcje. My tylko musimy zacząć ich używać.
Działanie sesji w PHP jest bardzo proste. W momencie pierwszego trafienia na stronę interpreter tworzy specjalny, losowy oraz unikalny identyfikator przesyłany między żądaniami za pomocą ciastek lub parametru PHPSESSID doklejanego automatycznie do adresów URL. Na jego podstawie odczytywany jest później odpowiedni plik z danymi sesji zapisany gdzieś na serwerze. Pod koniec przetwarzania żądania wszystkie wprowadzone przez skrypt zmiany są z powrotem zapisywane do wspomnianego pliku tak, aby były widoczne przy wejściu na kolejną podstronę. I tak to się toczy.
Wprowadzenie do sesji
edytujCzas na trochę praktyki. Aby zainicjować mechanizm sesji, wystarczy wywołać funkcję session_start(), najlepiej na początku naszej aplikacji. Od tego momentu do naszej dyspozycji zostaje oddana superglobalna tablica $_SESSION - wszystkie zapisane do niej dane są przesyłane między kolejnymi żądaniami. Popatrzmy na pierwszy, bardzo prosty przykład licznika odwiedzonych już podstron:
<?php
session_start(); // 1
if(!isset($_SESSION['licznik'])) // 2
{
$_SESSION['licznik'] = 0;
}
$_SESSION['licznik']++; // 3
echo 'Odwiedziłeś już '.$_SESSION['licznik'].' podstron!'; // 4
?>
Oto analiza:
- Inicjalizujemy sesje
- Jeżeli jest to pierwsza wizyta, tablica z sesjami nie zawiera żadnych danych. Dobrym zwyczajem jest ich inicjowanie, aby nie zostać zaatakowanym tysiącami komunikatów Notice.
- Zmieniamy dane sesji
- Odczytujemy dane sesji
Po odświeżeniu strony zauważymy, że licznik wskazuje już "2", po kolejnym - "3". Oznacza to, że PHP zapisuje zmienną $licznikprzesyła ją między naszymi żądaniami. Różnica pomiędzy sesjami, a ciastkami jest taka, że dane te w ogóle nie opuszczają serwera WWW, są przez to (w teorii) bezpieczniejsze.
Prosta autoryzacja użytkowników
edytujNapiszemy teraz prosty skrypt do autoryzacji użytkowników bazujący na sesjach. Jak wspomnieliśmy w poprzednim rozdziale, nie powinno się przesyłać pomiędzy żądaniami haseł oraz loginów użytkowników, nieważne czy w formie ciastek, czy sesji. Alternatywą jest ich ID. Jak przekonasz się w następnych rozdziałach, bazy danych, gdzie większość aplikacji WWW trzyma swoje informacje, wcale nie identyfikują rekordów jakimiś abstrakcyjnymi rzeczami typu login lub tytuł. Operują na zwyczajnych, automatycznie nadawanych i unikalnych liczbach zwanych identyfikatorami (w skrócie pisze się "id"). Dzięki temu można szybko je do siebie porównać, co ma szczególne znaczenie w przypadku ogromnej liczby rekordów. Takie też ID przypisane użytkownikom przesyłane są w sesjach. Oczywiście, zgodnie z praktyką stosowaną w bazach danych, numerację rekordów rozpoczynamy od 1. Rekord o ID równym 0 nie istnieje.
Zatem, jak taki system logowania działa? Kiedy sesja jest już załadowana, skrypt sprawdza zapisany w niej ID użytkownika. Jeżeli jest on większy od zera, ktoś jest już zalogowany i wystarczy tylko pobrać skądś dane jego profilu. W przypadku ID równego 0 mamy do czynienia z kimś anonimowym. Tu, w zależności od sytuacji możemy mu wyświetlać ogólnodostępne treści albo formularz logowania. Pisanie skryptu zaczniemy od stworzenia sobie namiastki bazy danych. Będzie nią tablica $uzytkownicy przechowująca loginy oraz hasła użytkowników. Ponadto napiszemy funkcję czyIstnieje() znajdującą ID użytkownika o podanym loginie i haśle lub false, kiedy takowy nie istnieje. Hasła naturalnie haszujemy poznanym już algorytmem sha1:
<?php
$uzytkownicy = array(1 =>
array('login' => 'user1', 'haslo' => sha1('ppp')),
array('login' => 'user2', 'haslo' => sha1('ddd')),
array('login' => 'user3', 'haslo' => sha1('fff'))
);
function czyIstnieje($login, $haslo)
{
global $uzytkownicy;
$haslo = sha1($haslo);
foreach($uzytkownicy as $id => $dane)
{
if($dane['login'] == $login && $dane['haslo'] == $haslo)
{
// O, jest ktos taki - zwroc jego ID
return $id;
}
}
// Jeżeli doszedłeś aż tutaj, to takiego użytkownika nie ma
return false;
} // end czyIstnieje();
Teraz zaczniemy właściwy skrypt. Zainicjujemy sesję i obsłużymy sytuację pierwszej wizyty - w takim wypadku gościa oznaczymy jako osobę anonimową (niezalogowaną):
// Wlasciwy skrypt
session_start();
if(!isset($_SESSION['uzytkownik']))
{
// Sesja się zaczyna, wiec inicjujemy użytkownika anonimowego
$_SESSION['uzytkownik'] = 0;
}
Krótka piłka - ID większy od 0? Ktoś jest zalogowany:
if($_SESSION['uzytkownik'] > 0)
{
// Ktos jest zalogowany
echo 'Witaj, '.$uzytkownicy[$_SESSION['uzytkownik']]['login'].' na naszej stronie!';
}
else
{
Jednak jeśli nie jest, to musimy zaprogramować zarówno pokazywanie formularza logowania, jak i autoryzację użytkownika na podstawie danych z niego. Zaczynamy od tej drugiej opcji. Korzystając z funkcji czyIstnieje() pobieramy ID użytkownika. Jeżeli jest on różny od false, wpisujemy go do sesji, tym samym logując go.
// Niezalogowany
if($_SERVER['REQUEST_METHOD'] == 'POST')
{
if(($id = czyIstnieje($_POST['login'], $_POST['haslo'])) !== false)
{
// Logujemy uzytkownika, wpisal poprawne dane
$_SESSION['uzytkownik'] = $id;
echo 'Dziekujemy, zostales zalogowany! <a href="sesje_2.php">Dalej</a>';
}
else
{
echo 'Podales nieprawidlowe dane, zegnaj! <a href="sesje_2.php">Dalej</a>';
}
}
else
{
Kiedy dane nie nadeszły z formularza, znaczy to, że trzeba go wyświetlić:
echo '<form method="post" action="sesje_2.php">
Zaloguj sie: <input type="text" name="login"/>
<input type="password" name="haslo"/>
<input type="submit" value="OK"/></form>';
}
}
I gotowe. Wszystko fajnie, wszystko pięknie, nasza witryna sobie działa, użytkownicy się logują, dodając coraz to nowe treści, lecz pewnego dnia dostajesz e-maila z ostrzeżeniem, że ktoś włamał się do serwisu i wszystko rozwalił. Co nawaliło? Czyżby zawiódł mechanizm sesji?
Bezpieczeństwo sesji
edytujSytuacja z mechanizmem sesji standardowo dostępnym w PHP jest o tyle śmieszna, że jego autorzy właśnie nam zostawili swobodę działania odnośnie tego, jak go zabezpieczyć przed kradzieżą. Normalnie użyty jest on bowiem dziurawy jak sito. Kradzież sesji nie nastręcza wielkich trudności, lecz na nieszczęście, wiele osób o tym nie pamięta (także autorów artykułów pokazujących, jak z niego korzystać!).
Włamania wykorzystujące dziury w mechanizmach sesji mają swoje fachowe nazwy. Pierwszą z nich jest Session Fixation. Jej działanie jest bardzo proste. Wykorzystujemy tutaj właściwość, że kiedy podamy skryptowi jakiś nieistniejący ID sesji, PHP automatycznie dorobi dane i nie przejmie się tym, że tak naprawdę to my go wygenerowaliśmy, a nie jakiś algorytm losująco-mieszający. Teraz popatrz: podszywasz się pod pracownika jakiegoś serwisu internetowego i każesz nieświadomemu użytkownikowi odwiedzić jakiśtam adres, najlepiej wymagający zalogowania. Do adresu URL doczepiasz ciąg ?PHPSESSID=abcdef. "abcdef" jest wymyślonym przez nas identyfikatorem. Kiedy użytkownik posłusznie się zaloguje, jest zdany na naszą łaskę. Mamy ID jego sesji i możemy działać tak, jakbyśmy byli nim.Jeszcze więcej cwaniactwa i możemy uzyskać nawet sesję administratora, co dla serwisu oznacza oczywiście katastrofę. Aby się przed tym zabezpieczyć, wystarczy po zainicjowaniu mechanizmu sesji dokleić bardzo prosty kod:
<?php
session_start();
if (!isset($_SESSION['inicjuj']))
{
session_regenerate_id();
$_SESSION['inicjuj'] = true;
}
Przy tworzeniu nowej sesji, dzięki funkcji session_regenerate_id() mamy pewność, że sesja dostanie losowy ID. Teraz nawet, jeżeli podamy zmyślnie ?PHPSESSID=abcdef, nic nam to nie da, bo PHP i tak sobie wszystko wygeneruje po swojemu i zostaniemy w tym samym miejscu, co byliśmy. Jednak nie spoczywaj jeszcze na laurach. ID nadal może zostać wykradziony. Wystarczy, że jakaś ciamajda skopiuje znajomemu link do jakiegoś zasobu, mając przy tym wyłączone ciastka. Oczywiście w linku znajdzie się wtedy jego LOSOWY identyfikator sesji, który w ten sposób zostanie ujawniony przed światem. Znajomy może teraz wędrować sobie po serwisie wykorzystując sesję ciamajdy. session_regenerate_id() tutaj nie zadziałała, bo przecież ta sesja już istnieje. Ataki tego rodzaju określa się mianem Session Hijacking. Zabezpieczenie się przed nimi także jest proste. Wystarczy w sesji przesyłać np. adres IP komputera wraz z nazwą przeglądarki, spod których została ona utworzona, a następnie porównywać je przy kolejnych wizytach z danymi dostarczonymi przez serwer. Jeżeli wystąpi niezgodność, ktoś próbuje użyć cudzej sesji.
<?php
session_start();
if (!isset($_SESSION['inicjuj']))
{
session_regenerate_id();
$_SESSION['inicjuj'] = true;
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
}
if($_SESSION['ip'] != $_SERVER['REMOTE_ADDR'])
{
die('Proba przejecia sesji udaremniona!');
}
Powyższy przykład zawiera łatki przeciwko obu włamaniom (co prawda w przypadku tej drugiej o wiele lepszym rozwiązaniem byłoby utworzenie sesji na nowo w przypadku niezgodności, niż obwieszczanie całemu światu naszego "odkrycia"). Pamiętaj jednak, że najsłabszym ogniwem jest zawsze człowiek. Jeżeli hacker zdobędzie twoje hasło, zabezpieczenia na nic się nie zdadzą, chyba że akurat masz stały adres IP i tak sobie wszystko napisałeś, że na twoje konto można się tylko z niego logować. Tylko kto stosuje tak rygorystyczne i nieporęczne środki bezpieczeństwa?
Podsumowanie
edytujSesje PHP są bardzo dobrym rozwiązaniem, jednak paradoksalnie wiele profesjonalnych aplikacji pisze własne systemy sesji całkowicie od zera. Powodów jest kilka:
- Integracja ze strukturą kodu reszty aplikacji
- Niezależność od PHP. Kiedy mechanizm sesji pojawił się w PHP po raz pierwszy, używało się go zupełnie inaczej, niż obecnie. Przyszłość może przynieść różne rzeczy, a nasz własny mechanizm zawsze pozostanie mimo tego taki sam.
- Bezpieczeństwo - kiedy piszesz wszystko od zera, możesz wstawić dodatkowe zabezpieczenia tam, gdzie normalnie nie sięgniesz.
- Elastyczność - dlaczego dane sesji muszą być trzymane akurat w pliku? Baza danych jest przecież równie dobra, jeśli nie lepsza.
- Specyfika zastosowania - wiele systemów sesji służy tylko celom autoryzacji. Stąd też pisze się je od zera pod to jedno zastosowanie, co umożliwia wprowadzenie pewnych uproszczeń oraz optymalizacji.
Jednak własny system sesji jest odpowiedzialnym kawałkiem kodu, gdyż tam to ty musisz wszystko od A do Z zaprogramować. Na razie jedynie sygnalizujemy taką możliwość jako alternatywę dla systemu sesji wbudowanego w PHP. Który z nich wybierzesz, to zależy tylko od Ciebie.