W tym rozdziale:
Uwierzytelnianie użytkownika
Ukryte pola danych formularza
Trwałe cookies
Śledzenie sesji API
Rozdział 7.
Śledzenie sesji
HTTP jest protokołem bezstanowym: nie ma wbudowanej funkcji, która umożliwiałaby serwerowi rozpoznawanie, że cała sekwencja pochodzi w całości od tego samego użytkownika. Zwolennicy poszanowania prywatności mogą postrzegać taką sytuację pozytywnie, jednakże większość programistów WWW uważa to za wielki kłopot, jako że aplikacje WWW nie są bezstanowe. Rozbudowane aplikacje WWW muszą współdziałać z klientem zarówno wstecznie jak i z wyprzedzeniem, zapamiętując informacje o nim pomiędzy zleceniami. Klasycznym przykładem może tutaj być aplikacja koszyka zakupów — klient musi mieć możliwość wkładania artykułów do swojego wirtualnego koszyka, a serwer musi zapamiętać te artykuły, aż do czasu kiedy klient wyloguje się kilka stron zleceniowych później (często trwa to nawet kilka dni!).
Problem stanu HTTP można najlepiej zrozumieć wyobrażając sobie szerokie forum dyskusji, na którym jesteśmy gościem honorowym. Następnie wyobraźmy sobie, na tym forum, dziesiątki internautów rozmawiających z nami w tym samym czasie. Wszyscy zadają nam pytania, odpowiadając na nasze, powodując że klepiemy w klawiaturę jak opętali usiłując odpowiedzieć wszystkim. Teraz wyobraźmy sobie, z kolei, że kiedy ktoś do nas pisze forum dyskusyjne nie podaje jego tożsamości, jedyne co widzimy to mieszanina pytań i odpowiedzi. W forum tego typu najlepsze co możemy zrobić to prowadzenie prostych konwersacji, np. poprzez odpowiadanie na bezpośrednie pytania. Jeżeli będziemy próbowali zrobić coś więcej, np. zadać swoje pytanie, może okazać się, że nie będziemy wiedzieli w którym momencie nadejdzie odpowiedź. Na tym właśnie polega problem stanu HTTP. Serwer HTTP „widzi” tylko serię zleceń, potrzebuje więc dodatkowej pomocy aby dowiedzieć się kto właściwie składa zlecenie.*
Rozwiązaniem, jak już może się zdążyliśmy domyśleć, jest przedstawienie się klienta w momencie składania każdego zlecenia. Każdy klient musi dostarczyć niepowtarzalny identyfikator, który umożliwi serwerowi jego identyfikację, mogą to być również informacje, które pozwolą serwerowi poprawnie obsłużyć zlecenie. Wykorzystując przykład forum dyskusji, każdy uczestnik musi rozpocząć swoje każdą wypowiedź od czegoś na wzór: „Cześć, Mam na imię Jason i...” lub „Cześć, właśnie zapytałem o twój wiek i...”. Jak się przekonamy podczas lektury tego rozdziału, istnieje wiele sposobów umożliwiających klientom „HTTP-owym” na przesłanie wstępnych informacji wraz z każdym zleceniem.
Pierwsza połowa tego rozdziału poświęcona będzie tradycyjnym technikom śledzenia sesji, wykorzystywanych przez programistów CGI: uwierzytelnianie użytkownika, ukryte pola danych formularza, przepisywanie URL-u oraz trwałe cookies. Natomiast druga połowa rozdziału omawia wbudowaną obsługę dla śledzenia sesji w Interfejsie API. Obsługa ta jest zbudowana na szczycie tradycyjnych technik i znacznie upraszcza zadanie śledzenia sesji w naszych apletach. Wszystkie omówienia zawarte w tym rozdziale opierają się na założeniu, że używany jest serwer pojedynczy. --> Rozdział 12 „Aplety Przedsiębiorstw i J2EE[Author:PG] ” wyjaśnia jak jest realizowana obsługa stanu wspólnej sesji w serwerach wielo-wejściowych.
Uwierzytelnianie użytkownika
Jednym ze sposobów przeprowadzenia śledzenia sesji jest wykorzystanie informacji pochodzącej z uwierzytelniania użytkownika. Uwierzytelnianie użytkownika zostało omówione w rozdziale 4 „Odczytywanie Informacji”, jednak w celu przypomnienia, uwierzytelnianie użytkownika stosowane jest kiedy serwer WWW ogranicza dostęp do niektórych ze swoich zasobów tylko do tych klientów, którzy logują się przy użyciu określonego hasła i nazwy użytkownika. Po zalogowaniu się klienta, nazwa użytkownika jest dostępna dla apletu poprzez metodę getRemoteUser().
Możemy wykorzystać nazwę użytkownika do śledzenia sesji klienta. Kiedy użytkownik już się zaloguje, przeglądarka pamięta jego nazwę i odsyła ją wraz z hasłem kiedy użytkownik przegląda kolejne strony witryny WWW. aplet może zidentyfikować użytkownika wykorzystując jego nazwę, tym samym może również wyśledzić jego sesję. Dla przykładu kiedy użytkownik doda coś do swojego wirtualnego koszyka na zakupy, fakt ten może zostać zapamiętany (we wspólnej klasie lub np. w zewnętrznej bazie danych) i wykorzystany w przyszłości przez inny aplet — kiedy użytkownik wejdzie na stronę końcową.
Aplet, który wykorzystuje uwierzytelnianie użytkownika, mógłby dla przykładu dodać artykuł do koszyka użytkownika, przy pomocy następującego kodu:
String name = req.getRemoteUser();
if (name == null) {
// Wyjaśnij, że administrator serwera powinien chronić tą stronę
}
else (
String[] items = req.getParameterValues("artykuł");
if (items != null) {
for (int i = 0; i < items.length; i++) (
addItemToCart(name, items[i]) ;
}
}
}
Inny aplet, może w przyszłości odczytać artykuły z koszyka zakupów za pomocą następującego kodu:
String name = req.getRemoteUser();
if (name == null) {
// Wyjaśnij, że administrator serwera powinien chronić tą stronę
}
else (
String[] items = getItemsFromCart(name);
}
Największą zaletą wykorzystywania uwierzytelniania użytkownika w celu przeprowadzania śledzenia sesji jest prosta implementacja. Po prostu mówimy serwerowi, żeby chronił określony zestaw stron (zgodnie z instrukcjami z rozdziału 8 „Bezpieczeństwo”), a następnie stosujemy metodę getRemoteUser() w celu identyfikacji każdego klienta. Kolejna zaleta jest taka, iż technika ta działa nawet kiedy użytkownik wchodzi na naszą stronę z różnych komputerów. Technika ta działa również kiedy użytkownik nie łączy się z naszą witryną lub kiedy zamyka przeglądarkę przed powrotem na naszą stronę.
Największym mankamentem uwierzytelniania użytkownika jest fakt, iż każdy użytkownik musi zarejestrować się na konto, a następnie logować się za każdym razem kiedy odwiedza naszą witrynę. Większość użytkowników toleruje rejestrowanie i logowanie się jako zło konieczne kiedy uzyskują dostęp do informacji poufnych, jednakże w przypadku zwykłego śledzenia sesji jest to prawdziwa uciążliwość. Kolejna uciążliwość to fakt, że podstawowe uwierzytelnianie HTTP nie zapewnia żadnego mechanizmu wylogującego; użytkownik musi zamknąć swoją przeglądarkę żeby się wylogować. Problemy, w przypadku uwierzytelniania użytkownika stwarza niemożność utrzymania przez użytkownika więcej niż jednej sesji na tej samej stronie (witrynie) WWW. Wniosek z powyższych rozważań nasuwa się sam: koniecznie potrzebujemy innych rozwiązań dla obsługi śledzenia anonimowego konta i dla obsługi uwierzytelnionego śledzenia sesji z wylogowaniem.
Ukryte pola danych formularza
Jednym ze sposobów obsługi śledzenia anonimowego konta jest zastosowanie ukrytych pól danych formularza. Jak sama nazwa wskazuje, są to pola dodane do formularza HTML, które nie są wyświetlane w przeglądarce klienta. Pola te są odsyłane do serwera, kiedy formularz zawierający je, zostaje przedłożony. Ukryte pola danych formularza dołączamy przy pomocy HTML-u podobnego do poniższego:
<FORM ACTION="/ servlet /MovieFinder" METHOD="POST">
...
<INPUT TYPE=hidden NAME="zip" VALUE=" 94040 ">
<INPUT TYPE=hidden MAME="poziom" VALUE="expert">
...
</FORM>
W pewnym sensie ukryte pola danych formularza definiują zmienne stałe dla formularza. Dla apletu otrzymującego przedłożony formularz nie ma żadnej różnicy pomiędzy ukrytym polem danych a polem, które jest widoczne.
Przy zastosowaniu ukrytych pól danych formularzowych, możemy przepisać nasze aplety koszyka zakupów, tak że użytkownicy będą mogli dokonywać zakupów anonimowo, do czasu zakończenia. Na przykładzie 7.1 została zaprezentowana technika wykorzystująca aplet, który wyświetla zawartość koszyka zakupów użytkownika i pozwala użytkownikowi na dodanie większej liczby artykułów lub na zakończenie zakupów. Przykładowy widok ekranu został przedstawiony na rysunku 7.1.
Przykład 7.1.
Śledzenie sesji przy użyciu ukrytych pól danych formularza
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ShoppingCartViewerHidden extends HttpServlet {
public void doGet(HttpServletReguest req, HttpServletResponse res)
throws ServletException, IOException {
res. setContentType (" tekst/html") ;
PrintWriter out = res.getWriter();
out.println("<HEAD><TITLE>Current Shopping Cart
Items</TITLE></HEAD>");
out.println("<BODY>") ;
// Artykuły z koszyka przekazywane są jako parametr artykułu.
String [ ] items = req.getParameterValues ("artykuł") ;
// Drukuj bieżące artykuły z koszyka.
out.println("Aktualnie w swoim koszyku masz następujące artykuły
cart:<BR>");
if (items == null) {
out.println("<B>None</B>") ;
}
else (
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]);
}
out.println"</UL>") ;
}
// Spytaj czy użytkownik chce jeszcze dodać jakieś artykuły, czy
też // chce zakończyć.
// Dołącz bieżące artykuły jako ukryte pola danych, tak żeby mogły
być // przekazane.
out.println("<FORM ACTION=\"/servlet/ShoppingCart\" METHOD=POST>");
if (items != null) {
for (int i = 0; i < items.length; i++) {
out.println("<INPUT TYPE=HIDDEN NAME="artykuł" VALUE=\"" +
items[i] + "\">");
}
}
out.println("Chciałbyś<BR>");
out.println("<INPOT TYPE=SUBMIT VALUE=\" Dodaj Więcej Artykułów \">");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Zakończ \">");
out.println("</FORM>")
out.println("</BODY></HTML>");
}
}
Rysunek 7.1.
Zawartość koszyka na zakupy
Aplet zaczyna od czytania artykułów, które już znajdują się w koszyku, używając getParameterValues("item"). Przypuszczalnie wartości parametrów artykułów zostały przesłane do tego apletu przy wykorzystaniu ukrytych pól danych. Następnie aplet wyświetla użytkownikowi bieżące artykuły i pyta go czy chce jeszcze jakieś dodać, czy też chce już zakończyć zakupy. Pytania zadawane są za pomocą formularza zawierającego ukryte pola danych, tak więc adresat formularza (aplet ShoppingCart) otrzymuje aktualne artykuły jako część przedłożenia.
W sytuacji, w której sesji klienta towarzyszy coraz więcej informacji, przekazywanie ich za pomocą ukrytych pól danych formularza może okazać się uciążliwe. W takich sytuacjach, można przesłać tylko jedną, określoną identyfikację (ID) sesji, która umożliwi zidentyfikowanie określonej (niepowtarzalnej) sesji klienta. ID sesji może być częścią kompletnych informacji o sesji, które są przechowywane na serwerze.
Zwróćmy uwagę, iż identyfikacje sesji muszą być przechowywane jako informacje tajne serwera, ponieważ klient znający ID sesji innego klienta mógłby, używając sfałszowanego ukrytego pola danych formularza, przyjąć drugą tożsamość. W konsekwencji identyfikacje sesji powinny być generowane w taki sposób, żeby nie dało się ich łatwo odgadnąć lub sfałszować, a bieżące ID sesji powinny być chronione, tak więc dla przykładu, nie upubliczniajmy loginu dostępu serwera, ponieważ zarejestrowane URL-e mogą zawierać identyfikacje sesji dla formularzy, przedłożonych za pomocą zleceń GET.
Ukryte pola danych formularza mogą zostać wykorzystane do wdrożenia uwierzytelniania bez wylogowania się. Prezentujemy po prostu formularz HTML jako ekran logujący, kiedy użytkownik został już raz uwierzytelniony przez serwer, jego tożsamość może zostać powiązana z jego określonym ID sesji. Przy wylogowywaniu się ID sesji może zostać usunięte (poprzez nie przesyłanie do klienta późniejszych formularzy) lub po prostu nie zapamiętane. Rozwiązanie to zostało szerzej omówione w rozdziale 8.
Zaletą ukrytych pól danych formularza jest ich powszechność oraz obsługa „anonimowości”. Ukryte pola danych są obsługiwane we wszystkich popularnych przeglądarkach, ponadto mogą być wykorzystywane przez klientów, którzy się nie zarejestrowali lub nie zalogowali. Główną, z kolei, wadą tej techniki jest fakt, że sesja utrzymuje się tylko poprzez sekwencję dynamicznie generowanych formularzy. Sesja nie może być utrzymana za pomocą dokumentów statycznych, dokumentów przesłanych jako e-mail, dokumentów z których utworzono zakładki czy za pomocą zamknięć przeglądarki.
Przepisywanie URL-u
Przepisywanie URL-u jest kolejnym sposobem na obsługę śledzenia anonimowego konta. W przypadku stosowania przepisywania URL-u, każdy lokalny URL, na który kliknie użytkownik, jest dynamicznie modyfikowany lub przepisywany — w celu dołączenia dodatkowych informacji. Te dodatkowe informacje mogą być w formie informacji dodatkowej ścieżki, w formie parametrów dodanych lub w formie jakiejś własnej, serwerowo — specyficznej zmiany URL-u. Z powodu ograniczonej przestrzeni dostępnej dla przepisywania URL, dodatkowe informacje są zwykle ograniczane do ID określonej sesji. Dla przykładu, poniższe URL-e zostały przepisane aby przekazać identyfikację sesji 123:*
http://server:port/servlet/Rewritten original
http://server:port/servlet/Rewritten/123 extra path infonnation
http://server:port/servlet/Rewritten?sessionid=123 added parameter
http://server:port/servlet/Rewritten;jsessionid=123 custom change
Każda technika przepisywania ma swoje złe i dobre strony. Informacja dodatkowej ścieżki działa poprawnie na wszystkich serwerach, z wyjątkiem sytuacji kiedy serwer musi użyć tą informację jako informację prawdziwej ścieżki. Zastosowanie parametru dodanego również sprawdza się na wszystkich serwerach, jednak może ono powodować konflikt nazw parametrów. Standardowa, serwerowo-specyficzna zmiana działa we wszystkich warunkach dla apletów, które obsługują tą zmianę.
Przykład 7.2 prezentuje poprawioną wersję naszego „shopping cart viewer”, który wykorzystuje przepisywanie URL-u (w formie informacji dodatkowej ścieżki) w celu anonimowego śledzenia koszyka zakupów.
Przykład 7.2.
Śledzenie sesji za pomocą przepisywania URL-u
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ShoppingCartViewerRewrite extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IQException {
res.setContentType (" tekst/html") ;
PrintWriter out = res.getWriter();
out.println("<HEAD><TITLE>Current Shopping Cart
Items</TITLE></HEAD>");
out.println("<BODY>");
// Pobierz bieżące ID sesji lub, w razie konieczności, wygeneruj
String sessionid = req.getPathInfo();
if (sessionid == null) {
sessionid = generateSessionId();
}
// Artykuły z koszyka związane są z ID sesji
String[] items = getItemsFromCart(sessionid);
// Drukuj bieżące artykuły z koszyka.
out.println("Aktualnie masz w swoim koszyku nastepujące
artykuły:<BR>");
if (items == null) {
out.println("<B>None</B>") ;
}
else {
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]) ;
}
out.println("</UL>") ;
}
// Spytaj użytkownika czy chce jeszcze dodać jakieś artykuły, czy też // zakończyć.
// Dołącz ID sesji do URL-u operacyjnego.
out.println("<FORM ACTION=\"/servlet/ShoppingCart/" + sessionid+
"\" METHOD=POST>") ;
out.println("Czy chcesz<BR>");
out.println("<INPUT TYPE=SUHMIT VALUE=\" Dodać Więcej Artykułów \">"),
out.println("<INPUT TYPE=SUBMIT VALUE=\" Zakończyć \">");
out.println("</FORM>") ;
// zaproponuj stronę pomocy. Dołącz ID sesji do URL-u.
out. println ("W celu uzyskania pomocy kliknij <A
HREF=\"/servlet/Help/" + sessionid+
"?topic=ShoppingCartViewerRewrite\">here</A>");
out.println("</BODY></HTML>") ;
}
private static String generateSessionId() {
String uid = new java.rmi.server. UID().toStringO; // gwarantowana
niepowtarzalność
return Java.net.URLEncoder.encode(uid); // zakoduj wszystkie
specjalne // znaki
}
private static String[] getItemsFromCart(String sessionid) {
// Nie wdrożone
}
}
Pierwszą rzeczą wykonywaną przez ten aplet jest pobranie aktualnej identyfikacji sesji przy użyciu metody IDgetPathInfo(). Jeżeli ID sesji nie jest określone, aplet wywołuje generateSessionId(), aby wygenerować nową, niepowtarzalną identyfikację sesji, wykorzystując w tym celu specjalnie do tego celu zaprojektowaną klasę RMI. ID sesji jest również wykorzystywane do załadowania i wyświetlenia aktualnych artykułów z koszyka. Identyfikacja jest później dodawana do atrybutu formularza ACTION, tak że może on zostać odczytany przez aplet ShoppingCart. Identyfikacja sesji jest również dodawana do URL-u pomocy, który wywołuje aplet Help. Coś takiego nie było możliwe przy ukrytych polach danych formularza, ponieważ aplet Help nie jest adresatem przedłożenia formularza.
Złe i dobre strony przepisywania URL-u ściśle pokrywają się z dobrymi i złymi stronami ukrytych pól danych formularza. Obie techniki są stosowane we wszystkich typach przeglądarek, pozwalają na dostęp anonimowy obie również mogą zostać użyte do wdrożenia uwierzytelniania z wylogowaniem się. Główna różnica między tymi technikami polega na tym, że przepisywanie URL-u działa dla wszystkich dynamicznie utworzonych dokumentów, takich jak aplet Help, a nie tylko, jak ma to miejsce w przypadku ukrytych pól danych formularza — dla formularzy. Dodatkowo, przy poprawnej obsłudze serwera, własne przepisywanie URL-u może działać, nawet z dokumentami statycznymi. Niestety często przepisywanie URL-u bywa męczące.
Trwałe cookies
Czwarta z kolei technika umożliwiająca śledzenie sesji wiąże się z trwałymi cookies. Cookie (ciasteczko) to bit informacji przesyłany do przeglądarki przez serwer WWW, który następnie może zostać odczytany z tej przeglądarki. Kiedy przeglądarka otrzymuje cookie, zapisuje go, i następnie odsyła go z powrotem do serwera, za każdym razem kiedy wchodzi na jakąś jego stronę, zgodnie z pewnymi zasadami. Ponieważ wartość cookie może jednoznacznie zidentyfikować klienta, cookies są często używane do śledzenia sesji.
Cookies zostały jako pierwsze wprowadzone w Netscape Navigator'ze. Mimo iż nie były one oficjalną częścią specyfikacji HTTP, szybko stały się, de facto, standardem we wszystkich popularnych przeglądarkach, włącznie z Netscape 0.94 Beta (i późniejszych) oraz Microsoft Internet Explorer'ze 2 (i późniejszych). Obecnie grupa robocza Internet Engineering Task Force (IETF) — HTTP Working Group, pracuje nad przekształceniem cookies w oficjalny, napisany w RFC 2109, standard. Więcej informacji o cookies można znaleźć w specyfikacji Netscape'a — Netscape's cookie specification, na stronie http://home.netscape.com/newsref/sts/cookie_spec.html i w RFC 2109, na stronie http://www.ietf.org/rfc/rfc2109.txt. Inną, godną polecenia stroną poświęconą cookies, jest http://www.cookiecentral.com.
Praca z cookies
Interfejs API ofreuje klasę javax.servlet.http.Cookie, dla pracy z cookies. Szczegóły nagłówka HTTP dla cookies, są obsługiwane przez Interfejs API. „Ciasteczko” (cookie) tworzymy przy pomocy konstruktora Cookie():
public Cookie(String name, String value)
Konstruktor ten tworzy nowe cookie z początkową wartością i nazwą. Zasady dla poprawnych nazw i wartości podane są w specyfikacji „Netscape's cookie specyfication” i w RFC 2109.
Aplet może przesłać „ciasteczko” do klienta przekazując obiekt Cookie do metody HttpServletResponse — addCookie():
public void HttpServletResponse.addCookie(Cookie cookie)
metoda ta dodaje określone cookie do odpowiedzi. Dodatkowe „ciasteczka” są dodawane poprzez kolejne wywołania do metody addCookie(). Ponieważ cookie są wysyłane przy użyciu nagłówków HTTP, powinny zostać dodane do odpowiedzi zanim jeszcze zostanie ona zatwierdzona. Od przeglądarek wymaga się, żeby przyjmowały 20 cookies na witrynę WWW, 300 (wszystkich) na użytkownika, mogą one ponadto ograniczyć rozmiar każdego „ciasteczka” do 4096 bajtów.
Kod potrzebny do ustawienia cookie prezentuje się w poniższy sposób:
Cookie cookie = new Cookie("ID", "123");
res.addCookie(cookie) ;
Aplet pobiera cookies poprzez wywołanie metody HttpServletRequest — getCookies():
public Cookie[] HttpServletRequest.getCookies()
Metoda ta odsyła tablicę obiektów Cookie, zawierających wszystkie cookies przesłane przez przeglądarkę jako część zlecenia lub pustą tablicę — jeżeli żadne cookies nie zostały przesłane. Wersje Interfejsu API, z przed 2.1 powodowały, że metoda getCookies() odsyłała null, jeżeli żadne cookies nie zostały przesłane. Dla maksymalnej kompatybilności najlepiej jest przyjąć null, albo jakąkolwiek pustą tablicę.
Kod, potrzebny do ściągania cookies wygląda w następujący sposób:
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
String name = cookies[i].getName() ;
String value = cookies[i].getValue();
}
}
Niestety, w standardowym Interfejsie API nie ma żadnej metody umożliwiającej ściągnięcie wartości cookie poprzez nazwę. Poza jego wartością i nazwą, możemy ustawić wiele atrybutów dla cookie. Poniższe metody używane są do ustawiania tych atrybutów. Jak można się przekonać przeglądając uzupełnienie B „HTTP Interfejs API — Krótkie Omówienie” dla każdej metody „set” istnieje odpowiadająca jej metoda „get”. Metody „get” są rzadko używane, ponieważ kiedy cookie jest przesyłane do serwera, zawieraja tylko swoje imię, wartość i wersję. Jeżeli ustawimy atrybut na cookie, otrzymane od klienta, musimy dodać go do odpowiedzi, żeby zmiana doszła do skutku, powinniśmy również dopilnować, aby wszystkie atrybuty z wyjątkiem nazwy, wartości i wersji zostały wyzerowane, podobnie jak cookie.
public void Cookie.setversion(int v)
ustala wersję „ciasteczka”. Aplety mogą przesyłać i otrzymywać cookies sformatowane, tak aby pasować zarówno do „Netscape persistent cookies” (Wersja 0) lub nowszego, jednak trochę eksperymentalnego RFC 2109 cookies (Wersja 1). Nowo zaprojektowane cookies domyślne dla wersji 0, utworzone dla zmaksymalizowania interoperacyjności.
public void Cookie.setDomain(String pattern)
określa wzór ograniczenia domeny. Wzór domeny, z kolei, określa które serwery powinny zobaczyć cookie. Domyślnie „ciasteczka” są odsyłane tylko do komputera centralnego który je zapisał. Określanie wzoru nazwy domeny przesłania to. Wzór musi rozpoczynać się kropką i zawierać co najmniej dwie kropki. Wzór pokrywa się tylko z jedną pozycją, poza początkową kropką. Dla przykładu .foo.com jest poprawne i pokrywa się z www.foo.com i z upload.foo.com, lecz nie z www.upload.foo.com.* Więcej o wzorach domen można znaleźć w specyfikacji „Netscape's cookie specyfication i z RFC 2109.
public void Cookie.setMaxAge(int expiry)
określa maksymalny wiek cookie w sekundach, po którym traci ono ważność. Wartość negatywna wskazuje domyślnie, że „ciasteczko” powinno się „przeterminować” kiedy przeglądarka „wychodzi”. Wartość zerowa informuje przeglądarkę, że ma natychmiast wyzerować cookie.
public void Cookie.setPath(String uri)
— określa ścieżkę dla cookie, które jest podzbiorem URI, do których cookie powinno być wysłane. Domyślnie „ciasteczka” są wysyłane do strony, która ustawia cookie oraz do wszystkich stron w tym katalogu lub pod tym katalogiem. Dla przykładu, jeżeli /servlet/CookieMonster ustawi cookie, wartością domyślną ścieżki będzie /servlet. Ścieżka ta informuje o tym, że cookie powinno być przesłane do /servlet/Elmo oraz do /servlet/subdir/BigBird, jednak nie do zamiennika apletu /Oscar.html lub do jakichkolwiek programów pod /cgi-bin. Ścieżka ustalona na / powoduje iż cookie jest przesyłane do wszystkich stron na serwerze. Ścieżka cookie musi zawierać aplet, który ustawia cookie.
public void Cookie.setSecure(boolean flag)
informuje czy cookie powinno zostać przesłane wyłącznie przez bezpieczny kanał, taki jak SSL. Wartością domyślną jest false.
public void Cookie.setComment(String comment)
ustawia pole komentarza „ciasteczka”. Komentarz opisuje cel, dla którego cookie zostało stworzone. Przeglądarki WWW mogą zdecydować się na wyświetlenie tego tekstu użytkownikowi. Komentarze nie są obsługiwane przez wersję „0” cookies.
public void Cookie.setValue(String newValue)
— przypisuje nową wartość do cookie. W przypadku wersji „0” cookies, wartości nie powinny zawierać: spacji, nawiasów kwadratowych, nawiasów zwykłych, znaków równości, przecinków, cudzysłowów, ukośników, znaków zapytania, znaków @, dwukropków oraz średników. Puste wartości nie mogą zachowywać się w ten sam sposób na wszystkich przeglądarkach.
Robienie zakupów przy pomocy trwałych cookies
Przykład 7.3 ukazuje wersję naszego „shopping cart viewer”, zmodyfikowanego aby obsługiwać koszyk zakupów przy użyciu trwałych cookies.
Przykład 7.3.
Śledzenie sesji wykorzystujące trwałe cookies
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ShoppingCartViewerCookie extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException (
res.setContentType (" tekst/html") ;
PrintWriter out = res.getWriter() ;
// Pobierz aktualne ID sesji poprzez przeszukanie otrzymanych
cookies.
String sessionid = null;
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("sessionid")) {
sessionid = cookies[i].getValue() ;
break;
}
}
}
// Jeżeli ID sesji nie zostało przesłane, wygeneruj je.
// Następnie prześlij go do klienta razem z odpowiedzią.
if (sessionid == null) {
sessionid = generateSessionId();
Cookie c = new Cookie("sessionid", sessionid);
res.addCookie(c);
}
out.println("<HEAD><TITLE>Current Shopping Cart
Items</TITLE></HEAD>");
out.println("<BODY>") ;
// Artykuły z koszyka są związane z ID sesji
String[] items = getItemsFronCart(sessionid);
// Drukuj aktualne artykuły z koszyka.
out.println("aktualnie masz w koszyku następujące artykuły
cart;<BR>");
if (items == null) {
out. println (" <B>None< /B>") ;
)
else {
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]) ;
)
out.println("</UL>") ;
}
// Spytaj czy chcą dodać jeszcze jakieś artykuły czy też zakończyć // zakupy.
out.println. ("<FORM ACTION=\"/servlet/ShoppingCart\" METHOD=POST>°);
out.println("Czy chcesz <BR>");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Dodać Więcej Artykułów \">");
out.println ("<INPUT TYPE=SUBMIT VALUE=\" Check Out \">");
out.println("</FORM>") ;
// Zaproponuj stronę pomocy.
out.println("Dla pomocy, kliknij <A HKEF=\"/servlet/Help" +
"?topic=ShoppingCartViewerCookie\">here</A>") ;
out.println( "</BODY></HTML>") ;
}
private static String generateSessionId() {
String uid = new java.rmi.server.UID().toString(); // gwarantowana
// niepowtarzalniość
return java.net.URLEncoder.encode(uid); // zakoduj wszystkie
specjalne znaki
}
private static String[] getItemsFromCart (String sessionid) {
// Nie wdrożono
}
}
Aplet ten próbuje najpierw ściągnąć identyfikację klienta, przechodząc kolejno przez cookies, które otrzymał jako część swojego zlecenia. Jeżeli żadne „ciasteczko” nie zawiera ID sesji, aplet generuje nowe identyfikację, wykorzystując metodę generateSessionId() i dodając cookie zawierające nowe ID sesji do zlecenia. Pozostała część apletu pokrywa się z wersją — przepisywanie URL-u, z wyjątkiem wersji, która nie przeprowadza przepisywania.
Trwałe cookie oferują elegancki, prosty i wydajny sposób wdrażania śledzenia sesji. „Ciasteczka” zapewniają najprostsze z możliwych wprowadzeń dla każdego zlecenia. Dla każdego również zlecenia, cookie może automatycznie dostarczyć identyfikację klienta lub nawet listę preferencji klienta. Dodatkowo, zdolność „Ciasteczek” do dostosowywania, daje im dodatkową przewagę i urozmaicenie.
Największym mankamentem cookies jest fakt, iż przeglądarki nie zawsze je akceptują. Czasem jest to spowodowane tym, że po prostu nie obsługują „Ciasteczek”. Częściej jednak dzieje się tak z powodu tego, że użytkownik ma skonfigurowaną przeglądarkę aby odrzucała cookies (w grę może wchodzić, np. chęć zachowania prywatności). Urządzenia WAP, z powodu ograniczonej objętości pamięci, również obecnie nie obsługują „Ciasteczek”, a bramy WAP dopiero niedawno zaczęły radzić sobie z nimi, w imieniu swoich urządzeń. Jeżeli któryś z naszych klientów nie zaakceptuje cookies, powinniśmy wtedy odwołać się do rozwiązań omówionych wcześniej w tym rozdziale.
API — śledzenie sesji
Szczęśliwie dla nas, projektantów apletów, aplety nie muszą wykorzystywać do obsługi swoich własnych sesji, technik które omówiliśmy wcześniej. Interfejs API dostarcza wielu metod oraz klas zaprojektowanych specjalnie w celu obsługi krótko terminowego śledzenia sesji, w imieniu apletów. Inaczej mówiąc aplety mają wbudowane krótko terminowe śledzenie sesji.*
Śledzenie Sesji API, jako że powołujemy się na fragment Interfejsu API, poświęcony śledzeniu sesji, powinno być obsługiwane przez wszystkie serwery WWW obsługujące aplety. Jednakże stopień obsługi zależny jest od serwera. Większość serwerów obsługuje śledzenie sesji poprzez stosowanie trwałych cookies, zachowując zdolność powrotu do przepisywania URL-u, w sytuacji gdy „Ciasteczko” zawiedzie. Niektóre serwery pozwalają na to, aby obiekty sesji były zapisywane na dysku serwera lub, w razie wypełnienia pamięci lub wyłączenia serwera, w bazie danych. (Artykuły które umieszczamy w sesji, aby opcja ta przynosiła korzyści, wymagają implementacji interfejsu Serializable). Szczegóły związane z naszym serwerem można znaleźć w jego dokumentacji. Pozostała część tego podrozdziału opisuje funkcję — najniższy wspólny mianownik, wymagany przez Interfejs API 2.2 dla nie rozproszonych serwerów. Rozproszone śledzenie sesji zostało omówione w rozdziale 12.
Podstawy śledzenia sesji
Śledzenie sesji jest zadziwiająco „eleganckie”. Każdy użytkownik jest związany z obiektem javax.servlet,http.HttpSession, który może zostać wykorzystany przez aplety do przechowywania i pobierania informacji dotyczących użytkownika. W obiekcie sesji możemy zapisać dowolny zestaw obiektów Javy. Dla przykładu obiekt sesji użytkownika zapewnia wygodną lokalizację do przechowywania (przez aplet) zawartości koszyka na zakupy użytkownika lub, jak się przekonamy czytając rozdział 9 Podłączalność do bazy danych” połączenie z bazą danych użytkownika.
W celu zapewnienia niezależności działania aplikacji WWW, sesje są „kalibrowane” na poziomie aplikacji WWW. Oznacza to, iż każdy ServletContext utrzymuje swoją własną pulę kopii HttpSession oraz że aplet działający wewnątrz jednego kontekstu nie ma dostępu do informacji sesji zapisanej przez aplet w innym kontekście.
Aplety wykorzystują metodę getSession() swojego obiektu zlecenia, w celu pobrania bieżącego obiektu HttpSession:
public HttpSession HttpServletRequest.getSession(boolean create)
Metoda ta odsyła bieżącą sesję, związaną z użytkownikiem składającym zlecenie. W przypadku gdy użytkownik nie ma żadnej aktualnej sesji, metoda ta tworzy ją, jeżeli create jest prawdziwe (true) lub odsyła null (zero) — gdy create jest fałszywe (false). Standardowym zastosowaniem tej metody jest getSession (true), tak więc w celu uproszczenia, w Interfejsie API 2.1 nie wprowadzono żadnej bezargumentowej wersji, która przyjmowałaby znacznik create lub true.
public HttpSession HttpServletRequest.getSession()
Aby mieć pewność, że sesja jest właściwie utrzymywana, metoda getSession() musi zostać wywołana przynajmniej raz przed zatwierdzeniem odpowiedzi.
Za pomocą metody setAttribute() możemy dodać dane do obiektu HttpSession:
public void HttpSession.setAttribute(String name. Object value)
Metoda wiąże określoną wartość obiektu z określoną nazwą. Wszystkie wcześniejsze powiązania z tą nazwą są usuwane. Aby pobrać obiekt z sesji, wykorzystujemy metodę getAttribute():
public Object HttpSession.getAttribute(String name)
Metoda ta odsyła związany z określoną nazwą lub null — jeżeli nie ma żadnego powiązania. Możemy również uzyskać nazwy wszystkich obiektów związanych z sesją, za pomocą metody getAttributeNames():
public Enumeration HttpSession.getAttributeNames()
odsyła Enumeration (wyliczenie) zawierające nazwy wszystkich obiektów związanych z tą sesją jako obiekty String lub puste Enumeration — jeżeli nie było żadnych powiązań. Możemy też wreszcie usunąć obiekt z sesji przy pomocy metody removeAttribute():
public void. HttpSession.removeAttribute(String name)
Metoda ta usuwa obiekt związany z określoną nazwą lub, jeżeli nie ma wiązania, nie wykonuje żadnych operacji. Każda z tych metod może zgłosić wyjątek java.lang.IllegalStateException, w przypadku gdy sesja będąca przedmiotem połączenia jest nieprawidłowa (nieprawidłowe sesje zostaną omówione w kolejnym podrozdziale).
Warto zwrócić uwagę, iż w Interfejsie 2.2 zostały zmienione nazwy tych metod z: setValue(), getValue(), getValueNames() oraz removeValue(), na bardziej standardowe setAttribute(), getAttribute(), getAttributeNames() oraz removeAttribute(). Metody „value” działają nadal, jednak nie są zalecane.
Wykorzystywanie śledzenia sesji — liczba wizyt
Przykład 7.4 prezentuje prosty aplet, wykorzystujący śledzenie sesji do zliczania ile razy klient go wywoływał, tak jak to zostało pokazane na rysunku 7.2. Aplet wyświetla również wszystkie powiązania z bieżącą sesją.
Przykład 7.4.
Sesja śledząca liczbę wizyt
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SessionTracker extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException (
res. setContentType (" tekst /html") ;
PrintWriter out = res.getWriter();
// pobierz bieżący obiekt sesji, w razie konieczności utwórz
HttpSession session = req.getSession();
// powiększ o jednostkę liczbę wizyt dla tej strony. Wartość
//jest zapisywana w sesji tego klienta pod nazwą "
tracker.count".
Integer count = (Integer)session.getAttrifcute("tracker.count");
if (count == null)
count = new Integer(1) ;
else
count = new Integer(count.intValue() + 1);
session.setAttribute("tracker.count", count) ;
out. println("<HTML><HEAD><TITLE>SessionTracker< /TITLE></HEAD>") ;
out.println("<BODY><Hl>Session Tracking Demo</Hl>");
// Wyświetl liczbę wizyt dla tej strony
out.println("Odwiedziłeś tą stronę " + count +
((count. intValue () ==1) ? " raz." : " razy."));
out.println("<P>") ;
out.println("<H2>Oto twoje dane sesji:</H2>");
Enumeration enum = session.getAttributeNames();
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement() ;
out.println(name + ": " + session.getAttribute(name) + "<BR>");
}
out.println("</BODY></HTML>") ;
}
}
Rysunek 7.2.
Liczenie liczby wizyt
Aplet pobiera najpierw obiekt HttpSession, związany z aktualnym klientem. Poprzez wykorzystanie bezargumentowej wersji metody getSession(), aplet prosi aby w razie konieczności sesja została utworzona. Aplet pobiera następnie obiekt Integer, związany z nazwą tracker.count. Jeżeli nie m żadnego obiektu, rozpoczyna liczenie od nowa. W przeciwnym wypadku, zastępuje obiekt Integer nowym obiektem Integer, którego wartość została zwiększona o jeden. W końcu wyświetla aktualną liczbę wizyt oraz wszystkie aktualne pary nazwa-wartość, w sesji.
Czas istnienia (cykl życia) sesji
Sesje nie trwają nieskończenie. Czas istnienia sesji kończy się automatycznie, albo po jakimś, określonym czasie nie wykorzystywania, (dla serwera „Tomcat” domyślny czas nie wykorzystywania wynosi 30 minut) lub jest to wykonywane ręcznie — w wypadku gdy sesja jest unieważniona przez aplet w sposób wyraźny. Wyłączenie serwera może, ale nie musi oznaczać unieważnienia sesji, jest to zależne od możliwości serwera. Kiedy czas istnienia kończy się (lub kiedy sesja jest unieważniana) obiekt HttpSession oraz wartości danych, które zawiera, są usuwane z systemu.
Pamiętajmy o tym, że wszystkie informacje zapisane w obiekcie sesji użytkownika zostają utracone w momencie, w którym sesja jest unieważniana. Jeżeli takie informacje potrzebne będą nam jeszcze w późniejszym terminie, powinniśmy je przechowywać w lokalizacji zewnętrznej, (takiej jak baza danych) i zaopatrzyć nasz własny mechanizm wiążący użytkownika, w pozycje bazy danych użytkownika. Powszechnymi rozwiązaniami tego problemu są: jakaś forma logowania się, ustawianie długo terminowego cookie, które może być odczytywane przez lata, i (lub) dostarczenie klientowi przepisanego do zakładki URL-u.
Przykładem wziętym z codziennego życia jest „My Yahoo!” gdzie użytkownicy logują się przy użyciu formularza HTML, ich sesja logująca jest śledzona przy użyciu „Ciasteczka”, którego czas istnienia kończy się wraz z zamknięciem przeglądarki, i gdzie (jeżeli zaznaczą pole „Remember My ID & Password box — Zapamiętaj moją identyfikację i hasło) ich tożsamość jest rejestrowana przez ustawione w tym celu, trwałe cookie, aż czasu kiedy ostatecznie się wylogują. Jest to szczególnie interesujące, że nawet gdy „Yahoo” zna tożsamość użytkownika, dzięki trwałemu cookie, nadal jednak wymaga od użytkownika wprowadzenia swojego hasła — przed uzyskaniem dostępu do poczty elektronicznej oraz innych poufnych informacji.
Wbudowana zdolność śledzenia sesji może być nadal używana w połączeniu z długookresowymi sesjami, w celu obsłużenia sesji logowania, przechowania dojścia do danych zewnętrznych i (lub) utrzymywania przechowywanej w pamięci podręcznej bazy danych, informacji, w celu szybkiego z nich skorzystania w przyszłości.
Ustawianie terminu ważności sesji
Idealną sytuacją byłoby unieważnienie sesji w momencie kiedy użytkownik zamyka swoją przeglądarkę, łączy się z inną stroną (witryną), lub odchodzi od swojego komputera. Niestety serwer nie ma żadnej możliwości wykrycia podobnych zdarzeń. W konsekwencji sesje „żyją” przez cały okres bezaktywności, po upływie którego serwer zakłada, że użytkownik nie będzie już dalej aktywny i że w związku z tym nie ma sensu utrzymywania dalej dla niego stanu sesji.
Domyślny termin ważności (limit czasu) sesji może zostać określony przy pomocy deskryptora wdrożenia web.xml; odnosi się to do wszystkich sesji, utworzonych w danej aplikacji WWW. Zobaczmy jak prezentuje się sytuacja na przykładzie 7.5.
Przykład 7.5.
Ustawianie terminu ważności na jedną godzinę
<?xml version="1.0" kodowanie="ISO-8859-l"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.// Aplikacja WWW DTD 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">
<web-app>
<!-- ..... ->
<session-config>
<session-tiineout>
60 <!-- minutes -->
</session-timeout>
</session-config>
</web-app>
Znacznik <session-timeout> utrzymuje wartość limitu czasu (terminu ważności) podaną w minutach. Dla przykładu, ustaliliśmy, że jeżeli użytkownik nie złoży żadnego zlecenia do aplikacji WWW, wtedy serwer może unieważnić sesję użytkownika i „rozwiązać” obiekty w niej przechowywane. Specyfikacja apletu wymaga, aby wartość terminu ważności wyrażona była liczbą całkowitą, co wyklucza wartości ujemne, jednak niektóre serwery używają takich wartości, aby zasygnalizować, że sesja nigdy nie ulegnie przeterminowaniu.
Wartości limitu czasu mogą być również dla sesji konfigurowane indywidualnie. Obiekt HttpSession dysponuje metodą setMaxInactivateInterval() do takiego precyzyjnego sterowania:
public void HttpSession.setMaxInactiveInterval(int secs)
Metoda ta określa wartość terminu ważności dla sesji, podanego w sekundach. Wartość ujemna przedziału oznacza iż sesja nie ulegnie nigdy przeterminowaniu. Tak, jednostki nie pokrywają się z tymi ze znacznika <session-timeout>, nie jest to niczym uwarunkowane, jest to po prostu przypadek. Aby łatwiej zapamiętać, które jednostki należy zastosować, stosujmy następujący schemat myślenia: „Dla precyzyjnej kontroli używamy precyzyjnych jednostek”.
Aktualna (bieżąca) wartość limitu czasu może być uzyskana przy wykorzystaniu metody getMaxInactiveInterval():
public int HttpSession.getMaxInactiveInterval()
Metoda ta odsyła wartość terminu ważności dla każdej sesji, w sekundach. Jeżeli nie określimy znacznika <session-timeout> możemy wywołać tą metodę na nową sesję, aby określić domyślny limit czasu naszego serwera.
Na przykładzie 7.6 zaprezentowaliśmy omawiane metody, w aplecie, który wyświetla bieżącą wartość limitu czasu, a następnie ustawia nową wartość terminu ważności na dwie godziny. Podczas pierwszego wywołania, bieżąca wartość limitu czasu wyświetla ogólno-aplikacyjne ustawienia. Podczas drugiego wywołania aktualny limit czasu wyświetla dwie godziny — ponieważ jest to limit czasu ustawiony podczas pierwszego wywołania.
Przykład 7.6.
Ustawianie terminu ważności
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SessionTimer extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IQException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter();
// Pobierz aktualny obiekt sesji, w razie konieczności utwórz go
HttpSession session = req.getSession();
out.println("<HTML><HEAD><TITLE>SessionTimer</TITLE></HEAD>") ;
out.println("<BODY><Hl>Session Timer</Hl>") ;
// Wyświetl poprzedni termin ważności
out.println("Poprzedni termin ważności to " +
session. getMaxInactiveInterval ()) ;
out.println("<BR>") ;
// Ustaw nowy termin ważności
session.setMaxInactiveInterval(2*60*60); // dwie godziny
// Wyświetl nowy termin ważności
out.println("Nowo ustalony termin ważności to " +
session.getMaxInactiveInterval()) ;
out.println("</BODY></HTML>") ;
}
}
Wybieranie właściwego terminu ważności
Tak więc znamy, na ten moment, kilka sposobów na kontrolowanie terminu ważności sesji, lecz jaka wartość limitu czasu jest najlepsza? Odpowiedź brzmi: to (oczywiście) zależy.
Pierwszą sprawą, którą trzeba zapamiętać, jest fakt iż wartość limitu czasu sesji nie determinuje jak długo sesja będzie trwała. Limit ten determinuje tylko jak długo będzie czekał z unieważnieniem sesji pomiędzy zleceniami. Sesje z półgodzinnym limitem czasu mogłaby trwać godzinami.
Określenie prawidłowego okresu bez-aktywności musi być kompromisem pomiędzy wygodą użytkownika, jego bezpieczeństwem a jego skalownością. Dłuższe terminy ważności dają użytkownikowi większą wygodę, ponieważ może on robić dłuższe przerwy pomiędzy zleceniami, uzyskując czas aby np. wykonać telefon lub aby sprawdzić pocztę elektroniczną — bez utraty stanu. Krótsze limity czasu zwiększają bezpieczeństwo użytkownika, ponieważ ograniczają czas wrażliwości (jeżeli użytkownik np. zapomniał się wylogować) jednocześnie zwiększają skalowalność serwera, jako że serwer może wtedy uwolnić obiekty w sesji dużo wcześniej.
Na wstępie zadajmy sobie pytanie jaki jest maksymalny czas oczekiwania naszego użytkownika pomiędzy zleceniami? Zwykle odpowiedź brzmi: pół godziny.
Rozważmy sobie tą odpowiedź i poznajmy parę niezmiennych zasad:
Bezpieczne aplikacje WWW, takie jak bankowość „on line”, powinny mieć krótsze niż zwykłe limity czasu aby umożliwić serwerowi odzyskanie lub „wypuszczenie” artykułów tak szybko jak to możliwe.
Sesje nie przechowujące „kosztownych” artykułów, mogą mieć dłuższe niż zwykłe terminy ważności.
Sesje przechowujące zawartości koszyków zakupów (kosztowne lub nie) powinny mieć dłuższe limity czasu, ponieważ może się zdarzyć, że użytkownicy nie będą pamiętali zawartości swych koszyków zakupów, co, w przypadku wczesnego unieważnienia, oznaczałoby dla nas koszty finansowe!
Sesje przechowujące w pamięci podręcznej informacje bazy danych powinny mieć, w przypadku gdy pamięć podręczna ma dużą objętość, krótsze terminy ważności, jednak terminy te powinny być dłuższe dla tych sesji jeżeli połączenie z bazą danych jest wyjątkowo wolne.
Jeżeli wymagamy od naszych użytkowników, aby wylogowywali się kiedy kończą pracę, domyślny limit czasu może być ustawiony na dłuższy.
Pamiętajmy również, iż termin ważności nie musi być taki sam dla każdego użytkownika. W celu ustawienia własnego limitu czasu, opartego na preferencjach użytkownika lub nawet w celu jego zmiany w czasie trwania sesji — np. aby uczynić limit czasu krótszym po przechowywaniu „kosztownego” artykułu, możemy posłużyć się metodą setMaxInactiveInterval().
Metody czasu trwania
Istnieje wiele dodatkowych metod związanych z obsługą czasu trwania sesji:
public boolean HttpSession.isNew()
Metoda ta odsyła informację odnośnie tego czy sesja jest nowa czy nie. Sesja jest uznawana za nową, jeżeli została utworzona przez serwer, lecz klient nie potwierdził jeszcze połączenia z nią.
public void HttpSession.invalidate()
Metoda ta powoduje, iż sesja jest natychmiast unieważniana. Wszystkie obiekty przechowywane w sesji zostają „rozwiązane”. Metodę tą wywołuje się w celu wdrożenia wylogowania.
public long HttpSession.getCreationTime()
Metoda ta odsyła czas, w którym sesja została utworzona, jako wartość long reprezentującą liczbę milisekund, która upłynęła od północy, 1 stycznia, 1970, czasu GMT.
public long HttpSession.getLastAccessedTime()
Metoda ta odsyła czas, w którym klient przesłał ostatnie zlecenie związane z sesją, jako wartość long reprezentującą liczbę milisekund, która upłynęła od północy, 1 stycznia, 1970 roku. Bieżące zlecenie nie jest uznawane jako ostatnie.
Każda z metod może zgłosić wyjątek java.lang.IllegalStateException w przypadku gdy sesja, do której uzyskiwany jest dostęp jest nieważna.
„Ręczne” unieważnianie starej sesji
Aby zademonstrować działanie omawianych metod, na przykładzie 7.7 został zaprezentowany aplet „ręcznie” unieważniający sesję — jeżeli istnieje ona dłużej niż jeden dzień lub jeżeli nie była aktywna przez okres dłuższy niż jedną godzinę.
Przykład 7.7.
Unieważnianie starej sesji
import java. io. *;
import java.uti1.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ManualInvalidate extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter() ;
// Pobierz bieżący obiekt sesji, w razie konieczności utwórz go
HttpSession session = reg.getSession() ;
Unieważnij sesję, jeżeli istnieje dłużej niż jeden dzień lub nie
była // aktywna dłużej niż jedną godzinę.
if (! session. isNew()) { // pomiń nowe sesje
Date dayAgo = new Date(System.currentTimeMillis() - 24*60*60*1000);
Date hourAgo = new Date (System.currentTiineMillis () -
60*60*1000);
Date created = new Date (session.getCreationTime ());
Date accessed = new Date (session.getLastAccessedTtme ());
if (created.before(dayAgo) || accessed.before(hourAgo)) {
session.invalidate() ;
session = reg.getSession() ; // pobierz nową sesję
}
}
// Kontynuuj przetwarzanie
}
}
Zasada działania sesji
Tak więc zastanówmy się jak serwer WWW wdraża śledzenie sesji? Kiedy użytkownik po raz pierwszy uruchamia aplikację WWW, jest mu przypisywany nowy obiekt sesji HttpSession oraz niepowtarzalna identyfikacja sesji (ID sesji). ID sesji identyfikuje użytkownika i jest wykorzystywana do dopasowania użytkownika z obiektem HttpSession w kolejnych zleceniach. Niektóre serwery wykorzystują pojedynczą identyfikację sesji dla całego serwera, z każdą aplikacją WWW odwzorowującą to ID do innej kopii HttpSession. Z kolei inne serwery przypisują jedną identyfikację sesji do jednej aplikacji WWW, jako dodatkowe zabezpieczenie przed złośliwymi aplikacjami WWW, pomagającymi intruzowi w „zdepersonalizowaniu” nas.
W tle, ID sesji jest zwykle zapisywane po stronie klienta w cookie zwanym JSESSIONID. Dla klientów, którzy nie obsługują „Ciasteczek”, identyfikacja sesji może zostać przesłana jako część przepisanego URL-u, zakodowanego przy użyciu parametru ścieżki jsessionid, np. http://www.servlets.com/catalog/servlet/ItemDisplay;jsession=123?item=156592391X. Inne implementacje, takie jak wykorzystanie SSL — protokołu bezpiecznej transmisji danych sesji, są również możliwe.
Aplet może poznać ID sesji za pomocą metody getId():
public String HttpSession.getId()
Metoda ta odsyła niepowtarzalny identyfikator String, przypisany do sesji. Dla przykładu, ID serwera „Tomcat” mogłoby wyglądać w następujący sposób awj4gyhsn2. Metoda zgłasza wyjątek IllegalStateException w przypadku gdy sesja nie jest ważna.
Pamiętajmy, iż identyfikacja sesji powinna być traktowana jako tajna informacja serwera. Zwracajmy uwagę na to, co robimy z tą wartością.
Wycofanie Obiektu HttpSessionContext W Interfejsie API 2.0 istniał obiekt HttpSessionContext, który był wykorzystywany do „tropienia” aktualnych sesji (oraz odpowiadającym im ID sesji), wykonywanych przez serwer. Prawie zawsze klasa była wykorzystywana do „debagowania” i sprawdzania jakie sesje jeszcze istnieją. Klasa ciągle jeszcze istnieje — dla zachowania kompatybilności binarnej, jednak począwszy od wersji 2.1 Interfejsu API, jest wycofana i określana jako pusta. Powodem jest fakt, iż identyfikacje sesji muszą być pilnie strzeżone, więc nie powinny być przechowywane w łatwo dostępnej, pojedynczej lokalizacji, zwłaszcza jeżeli istnienie takiej lokalizacji nie daje żadnych znaczących korzyści, poza „debagowaniem”.
|
Apletowe śledzenie sesji
Niemal każdy serwer obsługujący aplety wdraża śledzenie sesji oparte na „Ciasteczkach”, gdzie identyfikacja sesji jest zapisywana po stronie klienta w trwałym cookie. Serwer odczytuje ID sesji z cookie JSESSIONID, a następnie determinuje który obiekt sesji uczynić dostępnym podczas każdego zlecenia.
Dla klientów apletu taka sytuacja może przedstawiać problem. Większość środowisk apletowych wdraża HttpURLConnection w taki sposób, że kiedy aplet tworzy połączenie HTTP, środowisko automatycznie dodaje zawierające cookies przeglądarki, do zlecenia. Pozwala to apletowi na uczestniczenie w tej samej, co inne zlecenia przeglądarki, sesji. Problemem jest jednak, iż inne środowiska apletowe, takie jak starsze wersje środowiska „Java Plug-In enviroment” nie są zintegrowane z przeglądarką i dlatego zlecenia apletów jawią się jako oddzielone od normalnej sesji przeglądarki. Rozwiązanie dla sytuacji, w której aplety muszą działać w podobnych środowiskach jest przesyłane identyfikacji sesji do apletu oraz pozwalanie apletowi na przekazanie tego ID z powrotem do serwera, jako sztucznie utworzone cookie JSESSIONID --> . W rozdziale 10 „Komunikacja aplet zwykły — aplet tworzony na serwerze[Author:PG] ” została zamieszczona klasa HttpMessage — aby pomagać w tego typu sytuacjach. Aplet może otrzymać identyfikację sesji jako zwykły parametr apletu (dodany dynamicznie do strony HTML zawierającej aplet).
Awaryjne zmiany trybu pracy — „nie-ciasteczkowe”
Specyfikacja apletu mówi, iż serwery WWW muszą obsługiwać śledzenie sesji również dla przeglądarek, które nie akceptują cookies, których tak wiele wykorzystuje przepisywanie URL-u jako awaryjną zmianę trybu pracy. Wymaga to dodatkowej pomocy ze strony apletów, które generują strony zawierające URL-e. W celu obsłużenia śledzenia sesji poprzez przepisywanie URL-u, aplet musi przepisać każdy lokalny URL, zanim odeśle go klientowi. Interfejs API zawiera dwie metody, aby wykonać to zadanie: encode() oraz encodeRedirectURL():
public String HttpServlrtresponse.encodeURL(String url)
Metoda ta koduje (przepisuje) określony URL aby dołączyć ID sesji, a następnie odsyła nowy URL lub, jeżeli kodowanie nie jest potrzebne albo nie obsługiwane, pozostawia URL niezmienionym. Zasady decydujące o tym kiedy i jak ma być zakodowany URL są domeną serwera. Wszystkie URL-e wychodzące z serwera, powinny być uruchamiane poprzez tą właśnie metodę. Zwróćmy uwagę, iż metoda encodeURL() mogłaby być bardziej precyzyjnie nazwana rewriteURL() — aby nie myliła się z procesem kodowania URL-u, który koduje specjalne znaki w strumieniu URL-u.
public String HttpServlrtresponse.encodeRedirectURL(String url)
Metoda ta koduje (przepisuje) określony URL aby dołączyć identyfikację sesji, a następnie odsyła nowy URL lub, jeżeli kodowanie nie jest potrzebne albo nie obsługiwane — pozostawia URL niezmienionym. Zasady decydujące o tym kiedy i jak ma być zakodowany URL są domeną serwera. Metoda ta może używać odmiennych zasad niż metoda encodeURL(). Wszystkie URL-e przekazane do metody HttpServletResponse — sendDirect(), powinny być uruchamiane poprzez tą metodę.
Poniższy fragment kodu ukazuje aplet piszący łącznik do samego siebie, który jest kodowany tak, aby zawierał bieżącą identyfikację sesji:
out.println("Click <A HREF=\"" +
res.encodeURL(req.getRequestURI()) + "\">here</A>");
out.println("aby powtórnie załadować tą stronę.");
Na serwerach, które nie obsługują przepisywania URL-u lub mają wyłączoną tą funkcję, końcowy URL pozostaje bez zmian. Poniżej przedstawiamy fragment kodu, na którym jest zaprezentowany aplet przekierowujący użytkownika do URL-u zakodowanego tak, aby zawierał ID sesji:
res.sendRedirect(res.encodeRedirectURL("/servlet/NewServlet")) ;
Aplet jest w stanie wykryć czy identyfikacja sesji, wykorzystywana do zidentyfikowania aktualnego obiektu HttpSession, pochodzi od cookie czy od zakodowanego URL-u wykorzystującego metody isRequestedSessionIdFromCookie() i isRequestedSessionIdFromURL():
public boolean HttpServletRequest.isRequestedSessionIdFromCookie()
public boolean HttpServletRequest.isRequestedSessionIdFromURL()
Określenie czy identyfikacja sesji pochodzi z innego źródła, takiego jak sesja SSL, nie jest aktualnie możliwe.
ID sesji będące przedmiotem zlecenia może nie pokrywać się z identyfikacją sesji, odesłaną przez metodę getSession(), tak jak ma to miejsce kiedy ID sesji jest nieważne. Aplet może jednak ustalić czy identyfikacja sesji będąca przedmiotem zlecenia jest ważna, przy pomocy metody isRequestedSessionIdValid():
public boolean HttpServletRequest.isRequestedSessionIdValid()
Miejmy również świadomość, iż kiedy używamy śledzenia sesji opartego na przepisywaniu URL-u, wielokrotne okna przeglądarki mogą należeć do różnych sesji lub do tej samej sesji, w zależności od tego w jaki sposób okna te zostały utworzone oraz czy tworzący je łącznik miał zastosowane przepisywanie URL-u.
Aplet „SessionSnoop”
Zaprezentowany na przykładzie 7.8 aplet SessionSnoop wykorzystuje większość opisanych w rozdziale metod, aby „podsłuchać” informację o aktualnej sesji. Rysunek 7.3 prezentuje przykładowy wydruk wyjściowy apletu.
Przykład 7.8.
„Podsłuchiwanie” informacji sesji
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SessionSnoop extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("tekst/html");
PrintWriter out = res.getWriter() ;
// Pobierz aktualny obiekt sesji, w razie konieczności utwórz go
HttpSession session = req.getSession();
// Zwiększ o jeden liczbę wejść na tą stronę. Wartość jest zapisana
// w sesji klienta pod nazwą "snoop.count".
Integer count = (Integer)session.getAttribute("snoop.count");
if (count == null)
count = new Integer(1);
else
count = new Integer(count.intValue() + 1) ;
session.setAttribute("snoop.count", count) ;
out.println("<HTML><HEAD><TITLE>SessionSnoop</TITLE></HEAD>");
out.println("<BODY><Hl>Session Snoop</Hl>");
// Wyświetl ilość wejść na tą stronę
out.println("Odwiedziłeś już tą " + count +
((count.intValue() == 1) ? " raz." : " razy."));
out.println("<P>") ;
out.println("<H3>Oto twoje zachowane dane sesji:</H3>");
Enumeration enum = session.getAttributeNames();
while (enum.hasMoreElements()) {
name = (String) enum.nextElement();
intln (name + ": " + session.getAttribute (name) + "<;BR>");
}
out.println("<H3>Oto parę ważniejszych stanów twojej sesji:</H3>");
out.println("ID sesji: " + session.getid() +
" <I>(keep it secret )</I><BR>");
out.println("Now sesja: " + session.isNew() + "<BR>");
out.println("Termin ważności: " +
session.getMaxInactiveInterval());
out.println("<I>(" + session.getMaxInactiveInterval() / 60 +
" minut) < / Ix><BR>") ;
out.println("Czas utworzenia: " + session. getCreationTime() );
out. println ("<I>(" + new Date (session.getCreationTime ()) +
")</I>><BR>") ;
out.println("Ostatni czas wejścia: " +
session.getLastAccessedTime());
out.println("<I>(" + new Date(session.getLastAccessedTime()) +
")</I><BR>") ;
out.println("ID sesji będące przedmiotem zlecenia z URL: " +
req. isRequestedSessionIdFromURL () + " <BR>") ;
out.println("ID sesji, będące przedmiotem zlecenia jest ważne: " +
req.isRequestedSessionIdValid() + "<BR>");
out.println("<H3>Test URL Rewriting</H3>");
out.println("Kliknij <A HKEF=\"" +
res.encodeURL (req.getRequestURI()) + "\">here</A>") ;
out.println("Aby sprawdzić czy śledzenie sesji działa poprzez
przepisywanie");
out.println("URL-u, nawet wtedy gdy cookies nie są obsługiwane.");
out.println("</BODY></HTML>") ;
}
}
Aplet rozpoczyna tym samym kodem, który został zamieszczony w przykładzie 7.4, w aplecie SessionTracker. Następnie przechodzi do dalszego wyświetlania bieżącego ID sesji, tego czy jest to sesja nowa, wartości limitu czasu, czasu utworzenia sesji oraz czas ostatniego połączenia z sesją. Później aplet wyświetla czy identyfikacja sesji (jeżeli jest taka), będąca przedmiotem zlecenia, pochodzi od cookie czy od URL-u oraz czy to ID jest ważne. W końcu aplet drukuje zakodowany URL, który może zostać wykorzystany do powtórnego załadowania tej strony, w celu sprawdzenia czy przepisywanie URL-u działa nawet wtedy kiedy cookies nie są obsługiwane.
Zdarzenia wiążące sesję
Niektóre obiekty mogą „chcieć” przeprowadzić operację zarówno wtedy, kiedy są związane z sesją, jak i wtedy kiedy nie są z nią związane. Dla przykładu, połączenie z bazą danych może rozpoczynać transakcję (obsługę zlecenia) w stanie związania z sesją, a kończyć kiedy nie są z nią związane. Wszystkie obiekty, które wdrażają interfejs javax.servlet.http.HttpSessionBindingListner, są powiadamiane o związaniu z sesją oraz o tym, że nie są z nią związane. Interfejs deklaruje dwie metody: valueBound() oraz valueUnbound(), które muszą zostać wdrożone:
public void HttpSessionBindingListener.valueBound(
HttpSessionBindingEvent event)
public void HttpSessionBindingListener.valueUnbound (
HttpSessionBindingEvent event)
Metoda valueBound() jest wywoływana, kiedy odbiornik jest związany z sesją, a metoda valueUnbound() kiedy odbiornik nie jest z nią związany — poprzez usunięcie, zastąpienie albo poprzez unieważnienie sesji.
Argument javax.servlet.http.HttpSessionBindingEvent zapewnia dostęp do nazwy pod którą obiekt jest wiązany (lub odwiązywany od niej) z metodą getName():
public String HttpSessionBindingEvent.getName()
Rysunek 7.3.
Przykładowy wydruk wyjściowy apletu „SessionSnoop”
Obiekt HttpSessionBindingEvent zapewnia również dostęp do obiektu, do którego jest wiązany (lub od którego jest „odwiązywany”) odbiornik, wykorzystując w tym celu metodę getSession():
public HttpSession HttpSessionBindingEvent.getSession()
Na przykładzie 7.9 zaprezentowano użycie HttpSessionBindingListner oraz HttpSessionBindingEvent, z odbiornikiem rejestrującym w czasie związania jak i w czasie rozwiązania z sesją.
Przykład 7.9.
Zdarzenia wiążące śledzenie sesji
import java.io.*;
import java.util.*;
import javax.servlet.* ;
import javax.servlet.http.*;
public class SessionBindings extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IQException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();
// Pobierz aktualny obiekt sesji. w razie potrzeby utwórz go
HttpSession session = reg.getSession();
// Dodaj CustomBindingListener
session.setAttribute("bindings.listener",
new CustomBindingListener(getServletContext()));
out.println("Ta strona jest celowo pozostawiona pustą");
)
}
class CustomBindingListener inplements HttpSessionBindingListener {
// Zapisz ServletContext dla użytku jego metody log ()
ServletContext context;
public CustomBindingListener(ServletContext context) {
this.context = context;
}
public void valueBound(HttpSessionBindingEvent event) {
context.log("[" + new Dated + "] BOUND as " + event.getName() +
" to " + event. getSession () . getid ()) ;
}
public void valueUnbound(HttpSessionBindingEvent event) {
context.log("[" + new Date() + "] UNBOUND as " + event.getName () +
" from " + event. getSession().getId() );
}
}
Za każdym razem kiedy obiekt CustomBindingListner jest wiązany z sesją, jego metoda valueBound(), jest wywoływana i zdarzenie jest rejestrowane. Za każdym razem kiedy obiekt ten jest „odwiązywany” od sesji, wywoływana jest jego metoda valueUnbound(), tak że zdarzenie jest również rejestrowane. Możemy śledzić sekwencję zdarzeń poprzez obserwowanie dziennika zdarzeń serwera.
Załóżmy, iż aplet ten jest wywołany jeden raz, powtórnie ładowany 30 sekund później i następnie nie wywoływany co najmniej przez następne pół godziny. Dziennik zdarzeń mógłby wyglądać w następujący sposób:
[Tue Sep 27 22:46:48 PST 2000]
BOUND as bindings.listener to awj4qyhsn2
[Tue Sep 27 22:47:18 PST 2000]
UNBOUND as bindings.listener from awj4qyhsn2
[Tue Sep 27 22:47:18 PST 2000]
BOUND as bindings.listener to awj4qyhsn2
[Tue Sep 27 23:17:18 PST 2000]
UNBOUND as bindings.listener from awj4qyhsn2
Pierwsza pozycja występuje podczas zlecenia pierwszej strony, kiedy odbiornik jest związany z nową sesją. Pozycje: druga oraz trzecia występują podczas ponownego ładowania, ponieważ odbiornik jest „odwiązywany” i powtórnie wiązany z sesją, w czasie tego samego wywołania setAttribute(). Czwarta pozycja ma miejsce pół godziny później, kiedy limit czasu sesji kończy się, a ona sama jest unieważniana.
Robienie zakupów przy użyciu śledzenia sesji
Zakończmy ten rozdział spojrzeniem na to, jak zadziwiająco prostym może stać się nasz aplet „shopping cart viewer, przy zastosowaniu śledzenia sesji. Na przykładzie 7.10 zaprezentowano aplet zapisujący każdy artykuł z koszyka, w sesji użytkownika, pod nazwą cart.items. Zwróćmy uwagę, iż URL-e, znajdujące się na stronie, zostały przepisane tak, aby obsługiwać klientów z zablokowanymi „Ciasteczkami”.
Przykład 7.10.
Wykorzystanie Śledzenia Sesji API
import java.io.*;
import javax.servlet.* ;
import javax.servlet.http.*;
public class ShoppingCartViewerSession extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res. setContentType ("tekst/html") ;
PrintWriter out = res.getWriter () ;
// Pobierz obiekt bieżącej sesji.
HttpSession session = req.getSession();
// Artykuły z koszyka są utrzymywane w obiekcie sesji.
String [] items = (String []) session. getAttribute ("cart.
items") ;
out.println("<HTML><HEAD><TITLE>SessionTracker</TITLE></HEAD>");
out.println("<BODY><Hl>Session Tracking Demo</Hl>");
// Drukuj aktualne artykuły z koszyka.
out.println("Aktualnie masz w swoim koszyku następuje
artykuły:<BR>");
if (items == null) {
out.println("<B>None</B>") ;
}
else (
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]);
}
out.println("</UL>") ;
}
// Spytaj czy chcą dodać jeszcze do koszyka jakieś artykuły, czy też // chcą zakończyć.
out.println("<FORM ACTIQN=\"" +
res.encodeURLC/servlet/ShoppingCart") + "\" METHOD=POST>") ;
out.println("Czy chcesz<BR>");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Dodać Więcej Artykułów \">");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Zakończyć \">");
out.println("</FORM>") ;
// Zaproponuj stronę pomocy. Zakoduj w razie potrzeby.
out.println("Dla pomocy, kliknij <A HREF=\"" +
res.encodeURL("/servlet/Help?topic=ShoppingCartViewer") +
"\">here</A>") ;
out.println("</BODY></HTML>") ;
}
}
* Jeżeli ktoś zastanawia się nad tym, dlaczego serwer HTTP nie może zidentyfikować klienta poprzez adres IP komputera łączącego, odpowiedź brzmi: zgłoszonym adresem IP może być adres serwera pośredniczącego lub serwera, który obsługuje wielokrotnych użytkowników.
* Pamiętajmy, że wartości ID sesji powinny być trudne do odgadnięcia i sfałszowania. Identyfikacja 123 nie jest dobrym przykładem ID sesji, nadaje się tylko jako przykład książkowy.
* Technicznie rzecz ujmując, zgodnie ze specyfikacją „Netscape cookie specyfication”, dla domen najwyższej klasy wymagane są dwie kropki, domenami tymi mogą być .com, .edu, .net, .org, .gov, .mil, i .int, natomiast dla pozostałych domen wymagane są trzy kropki. Reguła nie pozwala serwerowi na przypadkowe lub szkodliwe ustawianie cookie dla domen .co.uk i przekazywanie ich do wszystkich przedsiębiorstw w Wielkiej Brytanii, dla przykładu. Niestety prawie wszystkie przeglądarki ignorują metodę trzech kropek. Więcej na ten temat na stronie http://homepages.paradise.net.nz~glineham/cookiemonster.html.
* Takie, a nie inny układ materiału niniejszej książki może przypominać sytuację nauczyciela trzeciej klasy szkoły podstawowej, który tłumaczy swoim uczniom rachunkowe zasady dzielenia, tylko po to, aby powiedzieć im później, że dzielenie najlepiej wykonuje się za pomocą kalkulatora. Wierzymy jednak, iż poznanie tradycyjnych metod pozwala na lepsze zrozumienie problemu.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\0-praca\Java Servlet - programowanie. Wyd. 2\r07-04-rysunki.doc
Tytuł rozdziału 12
Tytuł rozdziału 10