Rozdział 8.
Bezpieczeństwo
Bezpieczeństwo danych jest jednym z najważniejszych zagadnień programowania sieciowego, ponieważ coraz więcej firm decyduje się na obsługę klientów za pośrednictwem sieci i ich Intranety zapełniane są tajnymi danymi. Bezpieczeństwo danych zajmuje się ochroną tajnych informacji przed niepowołanymi użytkownikami. W przypadku aplikacji sieciowych bezpieczeństwo rozpatrujemy jako:
Uwierzytelnienie — możliwość weryfikacji tożsamości biorących udział w wymianie informacji.
Autoryzacja — udostępnienie zasobów systemu wybranej grupie użytkowników lub programów.
Poufność — pewność, że przesyłane dane mogą odczytać tylko przez nas określeni użytkownicy.
Integralność — możliwość sprawdzenia, czy informacja nie została zmieniona podczas transmisji.
Wymienione powyżej aspekty bezpieczeństwa można zilustrować prostym przykładem: klient chce być pewien, że jest połączony z właściwym serwerem (uwierzytelnienie) i żadna informacja (np. numer karty kredytowej) nie zostanie przechwycona (poufność). Firma, sprzedając usługę lub przesyłając tajne informacje poprzez sieć do pracowników, chce zabezpieczyć dane przed nieuprawnionymi użytkownikami. Ponadto obie strony (klient firmy i jej pracownik) chcą mieć pewność, że otrzymali niezmienioną informację.
Wiarygodność, autoryzacja, poufność i integralność są zapewniane przy użyciu technologii certyfikatów cyfrowych. Certyfikaty cyfrowe pozwalają serwerom i klientom na używanie zaawansowanych technik kryptograficznych do zapewnienia identyfikacji i kodowania protekcyjnego. Java zawiera wbudowany zestaw narzędzi wspomagających użycie certyfikatów cyfrowych, co sprawia, że serwlety są wspaniałą platformą do obsługi bezpiecznych aplikacji sieciowych używających technologii certyfikatów cyfrowych.
Bezpieczeństwo powinno również zapewnić ochronę danych przechowywanych na serwerze przed krakerami. Java od podstaw została zaprojektowana jako bezpieczny, sieciowo-zorientowany język, w którym istnieje możliwość wykorzystania komponentów gwarantujących bezpieczeństwo własnych i obcych danych na serwerze.
Niniejszy rozdział przedstawia podstawy zabezpieczania danych w sieci i użycia technik certyfikatów cyfrowych w serwletach. Opisano tu również sposoby zabezpieczania serwera podczas uruchamiania serwletów pochodzących z niepewnych źródeł. W stosunku do poprzednich rozdziałów przedstawiono tu mniej przykładów, gdyż podejmuje on problem wyższego poziomu, a wiele poruszonych tu tematów wymaga implementacji obsługi serwera. Serwlety potraktowano tu jako dodatki.
Autorzy niniejszej książki nie biorą odpowiedzialności za problemy związane z bezpieczeństwem danych, które mogą pojawić się po zastosowaniu rad tu zawartych. Szerszy opis dotyczący problematyki bezpieczeństwa w sieci można znaleźć w książce Web Security & Commerce napisanej przez Simsona Garfinkela i Gene Spafford (O'Reilly). Oczywiście, oni także nie wezmą na siebie odpowiedzialności.
Uwierzytelnienie poprzez HTTP
W rozdziale 4, „Odczytywanie informacji” wspomniano, że protokół HTTP posiada wbudowaną obsługę uwierzytelnienia — zwaną uwierzytelnieniem podstawowym, która jest oparta o prosty model wezwanie-odpowiedź, nazwa_użytkownika-hasło. Korzystając z tej techniki serwer zarządza bazą danych zawierającą nazwy użytkowników i hasła dostępu oraz identyfikuje określone zasoby (pliki, katalogi, serwlety, itp.) jako chronione. Gdy użytkownik zażąda dostępu do zastrzeżonych zasobów, serwer odpowie żądaniem podania nazwy użytkownika i hasła. W tym momencie przeglądarka zwykle wyświetla okno dialogowe zawierające pola, w które użytkownik wpisuje swoją nazwę i hasło, po czym dane te przesyłane są do serwera. Jeśli przesłane serwerowi informacje istnieją w jego bazie danych, dostęp do zasobów chronionych jest przyznawany. Cały opisany tu proces uwierzytelnienia odbywa się na serwerze.
Uwierzytelnienie podstawowe nie zapewnia poufności i integralności, co sprawia, że jest to słaby system zabezpieczenia dostępu do danych. Problem polega na tym, że przesyłane poprzez sieć hasła są kodowane za pomocą powszechnie znanej metody Base64. Każdy monitorując strumień danych TCP/IP ma pełny i natychmiastowy dostęp do przesyłanej informacji łącznie z nazwą użytkownika i hasłem chyba, że zostanie wykorzystana dodatkowa metoda kryptografii SSL (omówiona w dalszej części rozdziału). Ponadto hasła są przechowywane na serwerze w postaci czystego tekstu czyniąc je łatwym łupem dla każdego, kto włamie się do systemu plików serwera. Strony zabezpieczone opisaną tu metodą nie mogą zostać uznane za bezpieczne.
Uwierzytelnienie szyfrowane (typu digest) bazuje na schemacie omówionej metody, ale poprzez sieć przesyłany jest łańcuch tworzony za pomocą algorytmu szyfrowania MD5 z nazwy użytkownika, hasła, URI, metody żądania HTTP i losowo generowanej przez serwer wartości jednokrotnego użycia (nounce). Obie strony transakcji (klient i serwer) znają hasło i na jego podstawie generują szyfr. Dostęp do danych jest dozwolony, jeśli łańcuchy klienta i serwera są zgodne. Transakcje zabezpieczone w ten sposób są bezpieczniejsze, gdyż łańcuch jest ważny tylko dla jednego żądania i jednej wartości jednokrotnego użycia (nonce value). Niestety, tak jak w poprzedniej metodzie, serwer nadal zawiera bazę danych z oryginalnymi (nie zaszyfrowanymi) hasłami. Poza tym niewiele przeglądarek obsługuje tą technikę.
Podsumowując można stwierdzić, że uwierzytelnianie za pomocą protokołu HTTP jest użyteczne w środowiskach wymagających niskiego poziomu bezpieczeństwa danych. Dobrym przykładem takiego środowiska jest płatna witryna gazety internetowej, gdzie twórcom bardziej zależy na dostępności serwisu niż na ścisłym bezpieczeństwie danych. W tym przypadku uwierzytelnianie za pomocą HTTP jest wystarczającą metodą.
Konfiguracja uwierzytelnienia HTTP
W starszych interfejsach Servlet API (wersje wcześniejsze niż 2.2) sposób konfiguracji uwierzytelnienia różnił się w zależności od typu serwera. Począwszy od wersji 2.2 sposób uwierzytelniania został znormalizowany. W pliku opisu rozmieszczenia* web.xml określa się metodę zabezpieczenia danych. Za pomocą tego pliku można stosować wybraną metodę zabezpieczenia w różnych serwerach.
Instalacja mechanizmu zabezpieczeń dla interfejsu Servlet API 2.2 na serwerze nie jest konieczna, aby zapewnić kompatybilność z tym interfejsem. Zalecana jest implementacja pełnego systemu bezpieczeństwa, ale kontener serwletu może użyć tylko część mechanizmu bezpieczeństwa lub nie użyć go wcale. Implementacja pełnego mechanizmu bezpieczeństwa na serwerze jest wymagana tylko wtedy, gdy chcemy, aby serwer był kompatybilny z bardziej zaawansowaną platformą Java 2 (Enterprise Edition — J2EE),w której Servlet API 2.2 stanowi tylko część systemu. Poza tym, bezpieczeństwo jest jednym z najnowszych i nie do końca poznanych aspektów interfejsu Servlet API 2.2, co sprawia, że serwery sieciowe mogą różnić się pod względem implementacji mechanizmów bezpieczeństwa. Aby mieć pewność, że witryna pozostanie bezpieczna, należy ją co najmniej raz przetestować podczas przenoszenia danych pomiędzy serwerami.
Uwierzytelnienie w oparciu o rolę
Za pomocą znaczników (umieszczonych w pliku rozmieszczenia aplikacji sieciowej) można określić strony, do których dostęp mają tylko użytkownicy posiadający specjalne uprawnienia. W ten sposób zapewniamy dostęp wszystkim użytkownikom do witryny, ale niektóre strony zawarte w witrynie możemy udostępniać przez nas wybranym użytkownikom. Jednym ze sposobów zapewnienia ograniczonego dostępu jest skorzystanie z metody uwierzytelnienia opartego o rolę. W tej metodzie, zezwolenia dostępu są przyznawane abstrakcyjnemu podmiotowi security role, a dostęp do danych mają tylko użytkownicy (lub grupy użytkowników) należący do określonej roli. Można, na przykład, utworzyć witrynę w ten sposób, aby strony zawierające informacje o wynagrodzeniach były dostępne tylko użytkownikom w roli manager. W pliku opisu rozmieszczenia określany jest tylko sposób dostępu do danych w zależności od roli użytkownika, a konkretne przypisanie ról użytkownikom (lub grupom użytkowników) ma miejsce podczas wdrażania aplikacji, przy pomocy narzędzi serwerowych. To przypisanie można realizować w oparciu o informacje zawarte w plikach tekstowych, tabelach baz danych, systemie operacyjnym, itd.
Ograniczanie dostępu do serwletu
Załóżmy, że chcemy zapewnić ograniczony dostęp do serwletu, co pokazano w przykładzie 8.1. (Za pomocą omawianej metody można zabezpieczać nie tylko serwlety, ale również pliki i inne aplikacje).
Przykład 8.1.
Czy jesteś pewien, że masz pozwolenie na czytanie tego przykładu?
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SalaryServer extends HttpServlet {
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType ("text/plain");
PrintWriter out = res.getWriter();
out.println("Informacja ściśle tajna:"); out.println("Każdy zarabia więcej od Ciebie!");
}
}
Załóżmy, że posiadamy bazę danych użytkowników naszego serwera, zawierającą listę identyfikatorów, haseł i ról. Nie wszystkie serwery obsługują taką bazę danych, a te, które to robią, implementują ją w dowolny sposób. W przypadku serwera Tomcat 3.2 dane użytkowników są umieszczane w pliku conf/tomcat-users.xml, pokazanym w przykładzie 8.2. Kolejne wersje Tomcata pozwolą na bezpieczniejszy zapis informacji (nasza wersja umieszcza dane w postaci niezaszyfrowanej).
Przykład 8.2.
Plik conf/tomcat-users.xml
<tomcat-users>
<user name="Dilbert" password="dnrc" roles="engineer" />
<user name="Willy" password="iluvalice" roles="engineer,slacker" />
<user name="MrPointyHair" password="MrPointyHair" roles="manager,slacker" />
</tomcat-users>
Należy zauważyć, że tylko MrPointyHair jest w roli manager. Zakładając, że potrafi podłączyć swój komputer do sieci, powinien być jedyną osobą mającą dostęp do naszego tajnego serwletu. Określimy to w pliku web.xml, pokazanym w przykładzie 8.3.
Zwróćmy uwagę na to, że istotne znaczenie ma kolejność wystąpienia znaczników w pliku web.xml. Należy zawsze stosować znaczniki w następującym porządku: <security-constraint>, <login-config>, a następnie <security-role>. Nie bez znaczenia jest również kolejność występowania elementów wewnątrz tych znaczników. Tworząc pliki web.xml najłatwiej skorzystać z pomocy narzędzi graficznych, które automatycznie „zadbają” o odpowiednią strukturę pliku.
Przykład 8.3.
Ograniczenie dostępu za pomocą uwierzytelniania podstawowego.
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web_app_2_2.dtd">
<web-app>
<servlet>
<servlet_name>
secret
</servlet-name>
<servlet-class>
SalaryServer
</servlet-class>
</servlet>
<security-constraint>
<web-resource-collection>
<web-resource-name>
SecretProtection
</web-resource-name>
<url-pattern>
/servlet/SalaryServer
</url-pattern>
<url-pattern>
/servlet/secret
</url-pattern>
<http-method>
GET
</http-method>
<http-method>
POST
</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>
manager
</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>
BASIC <!-- BASIC, DIGEST, FORM, CLIENT-CERT -->
</auth-method>
<realm-name>
Default <!-- Opcjonalne, użyteczne tylko dla BASIC -->
</realm-name>
</login-config>
<security-role>
<role-name>
manager
</role-name>
</security-role>
</web-app>
Za pomocą tego pliku opisu rozmieszczenia zabezpieczono wszystkie metody GET i POST, które udostępniają /servlet/secret i /servlet/SalaryServer użytkownikom w roli manager, zalogowanym za pomocą uwierzytelnienia podstawowego. Pozostała część witryny jest dostępna bez ograniczeń.
Znacznik <security-constraint> chroni dostęp do danych, których ścieżki dostępu znajdują się wewnątrz znacznika <web-resource-collection> w ten sposób, że dostęp jest zapewniony tylko użytkownikom, których role umieszczono wewnątrz znacznika <auth-constraint>. Każdy znacznik <web-resource-collection> zawiera nazwę, chronione adresy URL oraz kilka metod HTTP o ograniczonym dostępie. Dla wzorców adresów URL można użyć identycznych symboli wieloznacznych, jakich używa się do mapowania serwletu — co opisano w rozdziale 2 „Podstawy serwletu HTTP”. Powinno się wyznaczyć metody chronione, przynajmniej GET i POST. Jeśli nie wpiszemy znacznika <http-method>, to wszystkie metody zostaną uznane za chronione. Znacznik <auth-constraint> zawiera nazwy ról użytkowników, którym udostępnia się zbiór zasobów.
Znacznik <login-config> określa sposób logowania się, którego używa aplikacja. W tym przypadku określamy podstawowe uwierzytelnienie za pomocą dyrektywy Default. W znaczniku <auth-method> umieszcza się wartości BASIC, DIGEST, FORM i CLIENT-CERT, które odpowiadają typom uwierzytelnienia: podstawowemu, szyfrowanemu, w oparciu o formularz i przy użyciu certyfikatów klienckich. O uwierzytelnieniach w oparciu o formularz i przy użyciu certyfikatów klienckich będzie mowa w dalszej części rozdziału. Znacznik <realm-name> określa zakres użycia loginu (tu zawarta jest informacja określająca uprawnienia poszczególnych ról). Ten znacznik jest używany tylko w uwierzytelnieniu podstawowym i szyfrowanym.
Znacznik <security-role> zawiera listę ról, która może być użyta przez aplikację.
W omówionej metodzie nie ma możliwości udostępniania zasobów wszystkim z pominięciem użytkowników z „czarnej listy”. Najprostszym wyjściem z tej sytuacji może być utworzenie grupy użytkowników nie należących do „czarnej listy”.
Teraz, gdy plik web.xml jest już gotowy, wszystkie żądania dostępu do chronionych danych zostaną przechwycone przez serwer i dane uwierzytelniające użytkownika zostaną sprawdzone. Dostęp jest przydzielany, jeśli uprawnienia są ważne, a serwer przypisuje użytkownikowi rolę manager. W przeciwnym razie dostęp jest zabroniony, a okno dialogowe przeglądarki poprosi użytkownika o kolejną próbę zalogowania się.
Wyszukiwanie informacji uwierzytelnienia
Serwlet może odczytać informację na temat uwierzytelnienia dzięki dwóm metodom przedstawionym w rozdziale 4: getRemoteUser() i getAuthType(). Serwlet API 2.2 zawiera dodatkową metodę getUserPrincipal(), która zwraca obiekt implementujący interfejs java.security.Principal
public java.security.Principal HttpServletRequest.getUserPrincipal()
Principal to termin techniczny określający uwierzytelniany podmiot. Może nim być użytkownik, grupa, korporacja lub po prostu identyfikator. Interfejs Principal zawiera metodę getName() zwracającą nazwę podmiotu. Metoda getUserPrincipal() służy do określenia uwierzytelnionej tożsamości użytkownika, podczas gdy getRemoteUser()do zapewnienia kompatybilności ze skryptami CGI. Metoda IsUserInRole() została także wprowadzona w Servlet API 2.2. Ta metoda zwraca wartość true, gdy użytkownik należy do określonej roli:
public boolean HttpServletRequest.isUserInRole(String role)
Ta metoda pozwala na wykonanie pewnych decyzji wewnątrz serwletu. Załóżmy, że deskryptor rozmieszczenia pozwala uzyskać dostęp wielu różnym rolom. Wywołanie tej metody pozwala serwletowi na zróżnicowanie dostępu do danych zależności od uwierzytelnionej roli użytkownika.
W deskryptorze rozmieszczeń (pliku opisu rozmieszczenia) można utworzyć pseudonimy, (można sprawić, by zasięg roli mgr będzie taki sam, jak zasięg roli manager). Ma to zastosowanie podczas integracji serwletów pochodzących z innych aplikacji sieciowych, które używają innych nazw ról niż nasz serwlet. Pseudonimy są konfigurowane osobno dla każdego serwletu, za pomocą znacznika <security-role-ref> wewnątrz znacznika <servlet>, jak pokazano w poniższym fragmencie web.xml:
<servlet>
<servlet-name>
secret
</servlet-name>
<servlet-class>
SalaryViewer
</servlet-class>
<security-role-ref>
<role-name>
mgr <!-- Nazwa używana przez serwlet -->
</role-name>
<role-link>
manager <!-- Nazwa używana przez deskryptor rozmieszczenia -->
</role-link>
</security-role-ref>
</servlet>
Może istnieć dowolna ilość znaczników <security-role-ref>. Należy pamiętać, że dane pseudonimy są ważne tylko podczas dostępu do serwletu poprzez jego zarejestrowaną nazwę.
W przykładzie 8.4 serwlet wyświetla klientowi jego nazwę, podmiot, rodzaj uwierzytelnienia (BASIC, DIGEST, FORM, CLIENT-CERT) oraz przynależność do roli manager. Aby uruchomić ten serwlet należy zainstalować go na serwerze sieciowym i zabezpieczyć korzystając ze schematu objaśnionego w poprzednim podpunkcie (należy przy tym upewnić się, że dostęp do nowego <url-pattern> jest ograniczony).
Przykład 8.4.
Szpiegowanie informacji uwierzytelniania
import java.io.*;
import java.security.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class AuthenticationSnoop extends HttpServlet {
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
out.println("<HTML><HEAD><TITLE>AuthenticationSnoop</TITLE></HEAD><BODY>");
out.println("<PRE>");
out.println("Nazwa Uzytkownika: "+ req.getRemoteUser());
String name = (req.getUserPrincipal() == null) ?
null : req.getUserPrincipal().getName();
out.println("Nazwa Podmiotu: " + name);
out.println("Typ uwierzytelnienia: " + req.getAuthType());
out.println("Przynależność do roli Manager: " + req.isUserInRole("manager"));
out.println("</PRE>");
out.println("</BODY></HTML>");
. }
}
Po wykonaniu programu powinniśmy zobaczyć na ekranie:
This is a password protected resource
Nazwa Użytkownika: jhunter
Nazwa Podmiotu: jhunter
Typ Uwierzytelnienia: BASIC
Przynależność do roli Manager: false
Uwierzytelnienie w oparciu o formularz
Zamiast uwierzytelnienia poprzez HTTP, serwlety mogą używać formularzy HTML. Użycie tej techniki pozwala użytkownikom wejść na chronioną witrynę poprzez odpowiednio zaprojektowaną, przyjazną użytkownikowi stronę logującą. Wyobraźmy sobie, że prowadzimy bank internetowy. Lepiej utworzyć niestandardowy formularz logujący (rys. 8.1), który poprosi o odpowiednie dane, niż korzystać z obskurnego okna przeglądarki.
Wiele banków i innych serwisów online wybrało uwierzytelnienie oparte o formularz. Implementacja takiego systemu jest stosunkowo prostym zadaniem, ponieważ takie uwierzytelnienie jest wbudowane w Servlet API 2.2. Aby przekształcić uwierzytelnienie z podstawowego na oparte o formularz należy zamienić fragment pliku web.xml <login-config> z przykładu 8.3 na fragment z przykładu 8.5.
Rysunek 8.1. Strona logująca do banku online
Przykład 8.5.
Konfiguracja uwierzytelnienia opartego o formularz
<login-config>
<auth-method>
FORM <!-- BASIC, DIGEST, FORM, CLIENT-CERT
</auth-method>
<form-login-config> <!-- Przydatne tylko dla FORM -->
<form-login-page>
/loginpage.html
</form-login-page>
<form-error-page>
/errorpage.html
</form-error-page>
</form-login-config>
</login-config>
Należy zauważyć, że wewnątrz znacznika <auth-method> zmieniono tryb BASIC na FORM. To oznacza, że w aplikacji sieciowej należy użyć uwierzytelnienia za pomocą formularza HTML. Znacznik <realm-name> zamieniono na <form-login-config>. Ten znacznik określa stronę logującą i stronę błędnego logowania używane w procesie uwierzytelnienia. Obie strony powinny być dobrze zaprojektowane i opisowe, przy czym strona logująca powinna prosić użytkownika o podanie danych, a strona błędnego logowania informować serwer o błędnych danych. Obie ścieżki adresów URL muszą być umieszczone w context root.
Za każdym razem, gdy serwer otrzymuje żądanie do chronionych danych, sprawdza czy użytkownik nie jest już zalogowany. Serwer może na przykład przeszukiwać obiekt HttpSession w celu znalezienia obiektu Principal. Jeśli serwer odnajdzie ten obiekt, porównuje zawarte w nim role do tych, które mają dostęp do danych. Dostęp przyznawany jest tylko temu użytkownikowi, którego rola jest umieszczona w obiekcie Principal. Jeśli serwer nie zlokalizuje obiektu Principal albo ten obiekt nie zawiera uprawnionych ról, klient jest przekierowywany na stronę logującą (ale najpierw serwer zapisuje żądany adres URL w obiekcie użytkownika HttpSession).
Strona logująca zawiera formularz, gdzie użytkownik wprowadza nazwę i hasło, które są przesyłane do serwera. Dostęp do zasobów jest możliwy tylko wtedy, gdy nazwa użytkownika i hasło są ważne i należą do uprawnionych ról obiektu Principal. Wówczas serwer przekierowuje użytkownika do chronionych zasobów, a w przeciwnym przypadku do strony błędnego logowania.
Strona logująca musi zawierać formularz ze specjalnymi wartościami, aby zapewnić dostarczenie właściwych danych do serwera. Formularz musi być zadeklarowany jako metoda POST dla URL j_security_check (nie należy poprzedzać go ukośnikiem, chociaż niektóre serwery mogą uznać to za błąd) z nazwą użytkownika przesłaną w zmiennej j_username i hasłem w j_password, na przykład:
<FORM METHOD=POST ACTION="j_security_check">
Username: <INPUT TYPE=TEXT NAME="j_username"><br>
Password: <INPUT TYPE=PASSWORD NAME="j_password"><br>
<INPUT TYPE=SUBMIT>
</FORM>
Przykład 8.6 przedstawia plik loginpage.html, który generuje formularz pokazany na rysunku 8.2.
Przykład 8.6.
Plik loginpage.html
<HTML>
<TITLE>Login</TITLE>
<BODY>
<FORM METHOD=POST ACTION=j_security_check>
<CENTER>
<TABLE BORDER=0>
<TR><TD COLSPAN=2>
<P ALIGN=center>
Witam! Proszę o podanie nazwy użytkownika <br>
i hasła aby się zalogować.
</TD></TR>
<TR><TD>
<P ALIGN=right><B>Nazwa:</B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="j_username" VALUE="" SIZE=15>
</TD></TR>
<TR><TD>
<P ALIGN=RIGHT><B>Hasło:</B>
</TD>
<TD>
<P><INPUT TYPE=PASSWORD NAME="j_password" VALUE="" SIZE=15>
</TD></TR>
<TR><TD>
<CENTER>
<INPUT TYPE=submit VALUE=" OK. ">
</CENTER>
</TD></TR>
</TABLE>
</FORM>
</BODY></HTML>
Rysunek 8.2 przedstawia wygenerowany formularz.
Rysunek 8.2. Formularz logujący przyjazny dla użytkownika
Strona błędnego logowania określona w sekcji <login-config> pliku web.xml może być jakimkolwiek plikiem HTML. Nie przewidziano specjalnych znaczników, by ten plik dołączyć. Niestety, nie ma też dostępu do żadnych specjalnych informacji mówiących o tym, dlaczego dostęp do danych jest zabroniony i wskazujących użytkownikowi stronę, za pomocą której powinien spróbować zalogować się ponownie. Przykład 8.7 ilustruje prostą stronę błędnego logowania.
Przykład 8.7.
Plik errorpage.html
<HTML>
<TITLE> Login Denied <TITLE>
<BODY>
Niestety, operacja logowania nie powiodła się.
Proszę wrócić do poprzedniej strony i spróbować jeszcze raz.
</BODY></HTML>
W porównaniu z uwierzytelnieniem podstawowym, logowanie przez formularz jest lepsze, ponieważ użytkownik może wejść do witryny poprzez przyjazną i przejrzystą stronę logującą. W obu uwierzytelnieniach hasło jest przesyłane jako tekst jawny chyba, że kanał komunikacyjny zostanie zabezpieczony innymi metodami.
Oba omówione uwierzytelnienia nie zapewniają mechanizmu wylogowania. W przypadku uwierzytelnienia opartego o formularz można wywołać metodę session.Invalidate(), ale nie gwarantuje to zamierzonego efektu. W obu sposobach serwer sprawdza ważność użytkowników nawet wówczas, gdy kontrola nie powinna być dokonywana przez serwer (na przykład: niektóre banki wymagają podania numeru konta, hasła i PIN-u, by umożliwić dostęp). Aby rozwiązać ten problem, należy skorzystać z uwierzytelnienia niestandardowego.
Uwierzytelnienie niestandardowe
Zwykle uwierzytelnienie klienta odbywa się w serwerze sieciowym. Deskryptor rozmieszczenia przekazuje serwerowi informację o zastrzeżonych zasobach i sposobie przydzielania dostępu.
Często takie podejście jest wystarczające, ale czasem pożądany system ochrony nie może być zaimplementowany na serwerze. Może być to spowodowane tym, że lista użytkowników jest zapisana w formacie nieznanym serwerowi albo udostępnianie zasobów użytkownikom odbywa się za pomocą wspólnego hasła. Aby poradzić sobie w takich sytuacjach, można skorzystać z serwletów. Serwlety mogą pobierać informacje o użytkownikach ze specjalnie sformatowanego pliku lub relacyjnej bazy danych. Dodatkowo można zastosować określony system ochrony (standardowy lub niestandardowy). Za pomocą tych serwletów można dodawać, usuwać lub zmieniać dane użytkowników.
Serwlety używają kodu stanu i nagłówków HTTP do zarządzania własnym systemem ochrony. Serwlet otrzymuje zakodowane dane użytkownika w nagłówku Authorization. Odmawia dostępu danemu użytkownikowi poprzez przesłanie statusu SC_UNAUTHORIZED i nagłówka WWW-Authenticate, opisującego określone dane użytkownika. Serwer zazwyczaj obsługuje wyżej opisane zdarzenia bez udziału serwletu, ale nie ogranicza dostępu do danych, gdy autoryzacja jest przeprowadzona w serwlecie.
Nagłówek Authorization przesłany przez klienta zawiera jego nazwę i hasło. W podstawowym schemacie autoryzacji, nagłówek Authorization zawiera łańcuch tekstowy username:password, zakodowany za pomocą kodera Base64. Na przykład, nazwa webmaster i hasło try2gueSS są przesyłane w nagłówku Authorization o wartości:
Authorization: BASIC d2VibWFzdGVyOnRyeTJndWVTUw
Serwlet może przesłać nagłówek WWW-Authenticate, aby poinformować klienta o sposobie autoryzacji i o obszarze weryfikacji użytkowników. Obszar stanowi zbiór kont użytkowników i chronionych zasobów. Na przykład, do przesłania klientowi informacji, aby użył uwierzytelnienia podstawowego w dziedzinie Admin, nagłówek WWW-Authenticate wygląda następująco:
WWW-Authenticate: BASIC realm="Admin"
W przykładzie 8.8 pokazano, jak serwlet wykonuje niestandardową autoryzację, otrzymując nagłówek Authorization i wysyłając status SC_UNAUTHORIZED oraz nagłówek WWW-Authenticate. Tak skonfigurowany serwlet ogranicza dostęp do „ściśle tajnego zbioru” tylko do grona użytkowników, których rozpozna na liście. W tym przykładzie, lista jest przechowywana w prostej tablicy mieszającej, a jej zawartość jest zakodowana. Listę użytkowników można także dołączyć poprzez zewnętrzną relacyjną bazę danych.
Aby odczytać nazwę i hasło zakodowane za pomocą Base64, należy skorzystać z dekodera Base64. Na szczęście jest on dostępny w darmowej wersji. W naszym serwlecie użyjemy własnej klasy com.oreilly.servlet.Base64Decoder dostępnej na stronie http://www.servlets.com. Szczegóły dotyczące kodowania Base64 można znaleźć na stronie http://www.ietf.org/rfc/rfc1521.txt*.
Przykład 8.8.
Bezpieczeństwo w serwlecie.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.Base64Decoder;
public classs CustomAuth extends HttpServlet {
Hashtable users = new Hashtable();
public void init (ServletConfig config) throws ServletException {
super.init(config);
// Nazwy i hasła są tajne!
users.put("Wallace:cheese", "allowed");
users.put("Gromit:sheepnapper", "allowed");
users.put("Penguin:evil", "allowed");
}
public void doGet (httpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/plain");
PrintWriter out = res.getWriter();
// pobierz nagłówek autoryzacji
String auth = req.GetHeader("Authorization");
//Czy zezwalamy na dostęp temu użytkownikowi?
if(!allowUser(auth)) {
//nie zezwalamy, nie jest autoryzowany
res.setHeader("WWW-Autheticate", "BASIC realm=\"users\"");
res.sendError(res.S.C._UNAUTHORIZED);
//można zaoferować mu wpisanie na listę zatwierdzonych użytkowników
}
else{
//dopuszczony, wiec pokażmy mu tajne rzeczy
out.println("Ściśle tajne rzeczy");
}
}
//ta metoda sprawdza informacje użytkownika przesłana w nagłówku Authorization
//z baza danych użytkowników zawarta w tablicy mieszającej users
protected boolean allowUser (String auth) throws IOException {
if (auth==null) return false; //brak auth
if(!auth.toUpperCase().startsWith("BASIC"))
return false;//my tylko przeprowadzamy tryb BASIC
//popierz zakodowaną nazwę użytkownika i hasło po "BASIC"
String userpassEncoded = auth.substring(6);
//zdekoduj, używając jakikolwiek dekoder Base64 (my używamy com.oreilly.servlet)
String userpassDecoded = Base64Decoder.decode(userpassEncoded);
//sprawdź nasza listę użytkowników czy ten jest "allowed"
if("allowed".equals(users.get(userPassDecoded)))
return true;
else
return false;
}
Dostęp do serwletu mają wszyscy użytkownicy. W serwlecie dokonuje się proces uwierzytelnienia: dostęp do chronionych zasobów jest przydzielany użytkownikom których dane uwierzytelniające znajdują się na liście uprawnionych. Po dokonaniu kilku modyfikacji za pomocą tego serwletu można udostępnić dane użytkownikom posiadającym hasło grupowe albo, podobnie do anonimowego FTP, można udostępnić zasoby użytkownikowi o nazwie anonymous i haśle, którym jest adres email.
Autoryzację niestandardową można użyć nie tylko do ograniczenia dostępu do pojedynczego serwletu. Gdybyśmy taką logikę dodali do naszego serwletu ViewResource, moglibyśmy zaimplementować niestandardowy sposób dostępu do pełnego zestawu plików. Reguła mapowania za pomocą przedrostków URL mogłaby być utworzona w ten sposób, aby ochrona serwletu ViewResource udostępniała całą strukturę katalogów chronionych plików. Gdybyśmy utworzyli specjalną podklasę z HttpServlet i dodali do niej tą logikę, moglibyśmy w łatwy sposób ograniczyć dostęp do każdego serwletu pochodnego tej podklasy. W metodzie niestandardowej autoryzacji sposób ograniczenia zabezpieczeń serwera nie wpływa na sposób zabezpieczenia jego serwletów.
Autoryzacja niestandardowa w oparciu o formularz
Serwlety mają możliwość wykonania autoryzacji niestandardowej w oparciu o formularz. Poprzez logowanie niestandardowe oparte o formularz, aplikacja sieciowa może użyć strony logującej HTML. Na przykład, aby zalogować się do niektórych aplikacji bankowych, należy oprócz nazwy i hasła podać także numer konta i PIN. Takiej możliwości nie zapewnia użycie FORM <auth-method>, lecz autoryzacja niestandardowa w oparciu o formularz. Jest to bardziej skomplikowane zadanie, bo szczegóły procesu logowania muszą być obsługiwane ręcznie.
Najpierw trzeba stworzyć stronę logującą, którą można napisać tak jak inne formularze HTML. Przykład 8.9 przedstawia plik login.html, który generuje formularz pokazany na rysunku 8.3.
Przykład 8.9
Plik login.html
<HTML>
<TITLE Login </TITLE>
<BODY>
<FORM ACTION=/servlet/LoginHandler METHOD=POST>
<CENTER>
<TABLE BORDER=0>
<TR><TD COLSPAN=2>
<P ALIGN=CENTER>
Witamy!<br>
Proszę wprowadzić numer rachunku, <br>
hasło i PIN, aby się zalogować
</TD></TR>
<TR><TD>
<P ALIGN=RIGHT><B> Rachunek:</B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="account" VALUE="" SIZE=15>
</TD></TR>
<TR><TD>
<P ALIGN=RIGHT><B>Hasło: </B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="password" VALUE="" SIZE=15>
</TR></TD>
<TR><TD>
<P ALIGN=RIGHT><B>PIN:</B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="PIN" VALUE="" SIZE=15>
</TR></TD>
<TR><TD COLSPAN=2>
<CENTER>
<INPUT TYPE=SUBMIT VALUE=" OK. ">
</CENTER>
</TD></TR>
</TABLE>
</FORM>
</BODY></HTML>
Rysunek 8.3 przedstawia formularz.
Rysunek 8.3. Przyjazny formularz logowania się do banku
Ten formularz prosi klienta o podanie numeru konta, hasła i kodu PIN, po czym przesyła informację do serwletu LoginHandler zatwierdzającego logowanie. Klient może dostać się do tej strony bezpośrednio lub poprzez link ze strony głównej witryny. Gdyby użytkownik chciał dostać się bezpośrednio do zasobów (bez logowania się) powinien być automatycznie skierowany na stronę logującą, a po pomyślnym zalogowaniu przekierowany z powrotem. Proces powinien przebiegać niezauważalnie. Użytkownik odnosi wrażenie, że to jego przeglądarka otworzyła okno.
Przykład 8.10 przedstawia serwlet implementujący przekierowania. Serwlet ten wysyła tajne informacje tylko wtedy, gdy obiekt sesji klienta wskazuje zalogowanie. Jeśli tak nie jest, serwlet zapamiętuje żądany URL w sesji do przyszłego użytku i przekierowuje klienta na stronę logującą.
Przykład 8.10.
Chronione Źródło
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class protectResource extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/plain";
PrintWriter out = res.getWriter();
//pobierz sesję
HttpSession session = req.getSession();
//czy sesja rozpozna użytkownika jako już zalogowanego?
Object done=sesion.getAttribute("logon.isDone");//zaznacz obiekt
if (done == null) {
//zaprzeczenie logon.isDone znaczy, że użytkownik się nie zalogował
//zapisz zadany URL jako prawdziwy cel i prześlij do login page
session.setAttribute("login.target",
HttpUtils.getRequestURL(req).toString());
res.sendRedirect(„/login.html”);
return;
}
//użytkownik zalogował się i może zobaczyć towary
out.println("Nieopublikowane książki wydawnictwa Helion czekają na ciebie!");
}
}
Ten serwlet sprawdza, czy klient jest już zalogowany szukając obiektu logon.isDone. Jeśli taki obiekt istnieje, serwlet ma pewność, że klient już się zalogował, w związku z czym pozwala mu przeglądać tajne informacje. Brak tego obiektu oznacza, że klient nie jest zalogowany, wobec czego serwlet zapisuje żądany adres URL w zmiennej login.target i przekierowuje klienta na stronę logującą. W niestandardowej autoryzacji opartej o formularz należy zapewnić takie zachowanie. Użycie podklas lub klas użytkowych może uprościć to zadanie.
W naszym przykładowym serwlecie sprawdzana jest ważność numeru konta, hasła i kodu PIN. Jeśli wprowadzone przez klienta dane są nieprawidłowe, dostęp do informacji jest zabroniony. W przeciwnym wypadku, fakt ten jest zapisywany w obiekcie sesji i klient jest przekierowywany na żądaną stronę.
Przykład 8.11.
Obsługa logowania.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
//pobierz numer rachunku, hasło i PIN użytkownika
String account = req.getparametr("account");
String password = req.getParametr("password");
String pin = req.getParametr("pin");
//sprawdzenie ważności hasła i nazwy
if (!allowUser(account, password, pin)) {
out.println("<HTML><HEAD><TITLE>Odmowa dostępu</TITLE></HEAD>");
out.println("<BODY>Hasło i login są nieważne. <BR>");
out.println("Możesz <A HREF=\"/login.html\"> sprobować ponownie</A>");
out.println("</BODY></HTML>");
}
else
{
//login ważny, zanotować to w obiekcie sesji.
HttpSession session = req.getSession();
session.setAttribute ("logon.isDone", account); //zaznaczenie obiektu
spróbuj przesłać klienta do strony, na która chciał wejść
try{
String target = (String) session.getAttribute("login.target");
if (target != null {
res.sendRedirect(target);
return;
}
}
catch (Exception ignored){}
//nie było możliwości przesłania do target. Przekierowanie do strony startowej.
res.sendRedirect ("/");
}
}
protected boolean allowUser (String account, String password, String pin) {
return true;//zaufaj każdemu
}
}
Właściwy sposób kontroli danych w serwlecie jest prosty: serwlet zakłada, że wszystkie dane użytkownika są ważne. Jeśli logowanie się powiedzie, serwlet zachowuje numer rachunku użytkownika w sesji klienta pod nazwą logon.isDone. Dla chronionych zasobów oznacza to, że klient ma do nich prawo dostępu. Klient zostaje przekierowany do strony, której adres znajduje się w zmiennej login.target. Jeśli z jakiegokolwiek powodu ta operacja się nie powiedzie, serwlet przekieruje użytkownika na stronę główną.
Certyfikaty cyfrowe
Prawdziwe aplikacje wymagają zabezpieczeń wyższego poziomu niż opisane do tej pory. Wymagają również zagwarantowania poufności i integralności informacji oraz bardziej niezawodnego systemu uwierzytelniania. To właśnie oferuje technologia certyfikatów cyfrowych.
Główną koncepcją jest system kryptografii oparty o klucze publiczne. W tym systemie każdy uczestnik wymiany informacji posiada unikatową parę kluczy używanych do kodowania i dekodowania informacji. Jednym z nich jest ogólnie dostępny klucz publiczny, a drugim — tajny klucz prywatny. Aby zademonstrować zasadę działania systemu, załóżmy, że Jaś chce przesłać Krzysiowi tajną informację. Jaś znajduje w swoich zasobach klucz publiczny Krzysia i za jego pomocą koduje informację. Gdy Krzyś otrzyma wiadomość, użyje swojego klucza prywatnego do jej zdekodowania. Informacja staje się bezwartościowa dla każdego, kto ją przechwyci i nie posiada klucza prywatnego Krzysia.
Metody kodowania wykorzystujące klucze cyfrowe istnieją już od kilku lat i są dość dobrze rozwinięte. Większość z nich działa w oparciu o algorytm RSA stworzony przez Rona Rivesta, Adiego Shamira i Leonarda Adelmana.* RSA używa dużej ilości liczb pierwszych do generowania unikatowej pary kluczy asymetrycznych (wiadomość zakodowaną przez jeden klucz można zdekodować używając drugiego). Generowane klucze mogą różnić się długością, zwykle wyrażaną w ilości bitów składających się na klucz. Klucze 1024- lub 2048-bitowe są wystarczające do zapewnienia bezpiecznej komunikacji RSA.
Ponieważ klucze składają się z tak wielu znaków, niepraktycznym podejściem byłoby zmuszanie użytkownika do wpisywania klucza za każdym razem. Zamiast tego, klucze publiczne są przechowywane na dysku w formie certyfikatów cyfrowych, a klucze prywatne w zakodowanej formie. Certyfikaty cyfrowe mogą być generowane za pomocą specjalnych programów (takich jak np. pakiet PGP) lub są wydawane przez specjalistyczną firmę. Pliki z certyfikatami mogą być ładowane przez najbardziej chronione aplikacje (takie jak serwery, przeglądarki i programy obsługujące pocztę elektroniczną).
Kryptografia kluczy publicznych rozwiązuje problem poufności, ponieważ cały proces komunikacji jest zakodowany. Metoda ta zapewnia również integralność: w naszym przykładzie Krzyś wie, że wiadomość, którą otrzymał nie została zmodyfikowana (gdyby tak było, nie byłby w stanie jej zdekodować). Nie rozwiązaliśmy jeszcze problemu wierzytelności: Krzyś nie ma pewności, że wiadomość, którą odebrał została wysłana przez Jasia. Ten problem rozwiązuje podpis elektroniczny. Ponieważ klucze prywatne i publiczne są asymetryczne, Jaś może najpierw użyć własnego klucza prywatnego do zakodowania wiadomości, a następnie zaszyfrować informację jeszcze raz, za pomocą publicznego klucza Krzysia. Gdy Krzyś odbierze wiadomość, zdekoduje ją najpierw własnym kluczem prywatnym, a następnie kluczem publicznym Jasia. Teraz Krzyś wie, że jedynym nadawcą otrzymanej wiadomości mógł być tylko Jaś, bo wiadomość zakodowana jego kluczem prywatnym może być zdekodowana jedynie za pomocą jego klucza publicznego.
Prostszym systemem jest użycie kluczy symetrycznych, czyli jednego klucza kodującego i dekodującego. Klucze asymetryczne mają dużą zaletę: przy przesyłaniu informacji nie trzeba zapewniać bezpieczeństwa kanału komunikacyjnego. Mają też ogromną wadę: kodowanie i dekodowanie informacji za pomocą tych kluczy wymaga sporej mocy obliczeniowej. Jako kompromis, wiele systemów kryptograficznych korzysta z asymetrycznych kluczy prywatnych i publicznych do wzajemnej identyfikacji i poufnego przekazania oddzielnego klucza symetrycznego do kodowania właściwej informacji. Klucz symetryczny jest zwykle tworzony w oparciu o standard DES.
Rząd Stanów Zjednoczonych ograniczył rozmiar eksportowanych kluczy symetrycznych do długości 56 bitów (co daje około 72*1015 możliwych kombinacji kluczy). Wiadomości zakodowane 56-bitowym kluczem są trudne do zdekodowania, ale nie ma rzeczy niemożliwych — wyspecjalizowane maszyny potrafią złamać kod w ciągu kilku godzin. Wewnątrz USA wiele systemów używa 128-bitowych kluczy DES (około 3.40282*1038 możliwości). Ponieważ nie znaleziono jeszcze sposobu na złamanie kodu informacji zakodowanej kluczem DES w sposób brutalny, przekazywanie wiadomości z wykorzystaniem kluczy o dużych rozmiarach jest bardzo bezpieczne.
Pozostaje jeszcze jeden problem: w jaki sposób jeden użytkownik może upewnić się, że ten drugi jest faktycznie tym, za kogo się podaje? Jaś i Krzyś znają się, więc Krzyś wierzy, że klucz publiczny dostarczony mu osobiście przez Jasia jest prawdziwy**. Z drugiej strony, jeśli np. Małgosia, nie znając wcześniej Jasia, chciałaby mu udostępnić swój klucz publiczny, Jaś może podejrzewać, że Małgosia jest w rzeczywistości Markiem. Jeśli założymy, że Krzyś zna Małgosię, możemy poprosić Krzysia, by za pomocą swojego klucza prywatnego podpisał klucz publiczny Małgosi. Wówczas, Jaś dostanie klucz Małgosi potwierdzony przez Krzysia, do którego ma zaufanie.
W rzeczywistości, ta „trzecia strona” potwierdzająca autentyczność nazywana jest Centrum Certyfikacji. Takim centrum jest np. korporacja VeriSign. Ponieważ VeriSign jest ogólnie znaną organizacją (z powszechnie znanym kluczem publicznym) klucze zweryfikowane przez to centrum można uznać za pewne. VeriSign oferuje kilka klas identyfikatorów (wraz ze wzrostem numeru klasy rośnie jego poziom zaufania i cena). Certyfikaty pierwszej klasy można otrzymać po wypełnieniu odpowiedniego formularza zamieszczonego w witrynie VeriSign. Certyfikaty wyższych klas są weryfikowane indywidualnie przez pracowników tego centrum certyfikacji, przy użyciu specjalnych metod weryfikacyjnych.
Wybierając centrum autoryzacji należy zwrócić uwagę na jego prestiż. Certyfikaty VeriSign są dołączone do przeglądarek MS IE i Netscape, więc każdy użytkownik Internetu będzie je akceptował. Poniżej wypisano najpopularniejsze centra certyfikacji:
VeriSign (http://www.verisign.com)
Thawte Consulting (http://www.thawte.com)
Entrust Technologies (http://www.entrust.com)
Więcej informacji na temat certyfikatów cyfrowych znajduje się w książce Gail'a L.Granta (McGraw-Hill) Understanding Digital Signatures, która dostarcza wprowadzenie odpowiednie dla programistów i zwykłych użytkowników. O powiązaniu kryptografii z Javą traktuje książka Jonathana Knudsena Java Cryptography (O'Reilly).
Protokół bezpiecznej transmisji danych (SSL)
Protokół SSL zajmuje miejsce pomiędzy protokołem poziomu aplikacji (w tym przypadku HTTP) a protokołem transportowym niskiego poziomu (w przypadku Internetu — prawie wyłącznie TCP/IP). Protokół ten wykorzystuje klucze publiczne do wymiany kluczy symetrycznych, które służą do zakodowania komunikacji pomiędzy klientem i serwerem. Po raz pierwszy wykorzystano ten protokół w przeglądarce Netscape Navigator 1. Od tamtej pory SSL jest standardem bezpieczeństwa komunikacji online i jest podstawą nowego protokołu TLS (który jest tworzony przez IETF). Więcej informacji na temat TLS można uzyskać na stronie http://www.ietf,org/rfc/rfc2246.txt.
SSL Version 2.0 jest pierwszą wersją, która zyskała szeroką akceptację. Zawiera ona wsparcie tylko dla certyfikatów serwerowych, zapewnia uwierzytelnienie serwera, poufność i integralność. Zasady działania protokołu są następujące:
Użytkownik łączy się z zabezpieczoną witryną przy użyciu protokołu HTTPS (protokół HTTP i SSL).Adresy URL witryn, które używają protokołu HTTPS, zaczynają się od https: zamiast http:.
Serwer podpisuje kluczem prywatnym swój klucz publiczny i przesyła go przeglądarce.
Przeglądarka używa klucza publicznego serwera, by sprawdzić, czy osoba podpisująca klucz jest w jego posiadaniu.
Przeglądarka upewnia się, czy zaufane centrum certyfikacji podpisało klucz. Jeśli nie, przeglądarka pyta użytkownika, czy klucz można uznać za wiarygodny i postępuje zgodnie z jego zaleceniami.
Klient generuje dla tej sesji klucz symetryczny DES, koduje publicznym kluczem serwera i odsyła serwerowi. Powstały w ten sposób nowy klucz jest użyty do zakodowania transakcji. Klucz symetryczny jest używany dlatego, że systemy oparte o klucze asymetryczne bardzo obciążają system.
Wszystkie opisane tu procedury są niewidoczne dla twórców serwerów i serwletów. Należy po prostu otrzymać odpowiedni certyfikat serwerowy, zainstalować go i odpowiednio skonfigurować serwer. Informacje przesyłane pomiędzy serwletami i klientami będą od tej pory zakodowane.
Uwierzytelnianie klienta za pomocą SSL
Nasza skrzynka z narzędziami zabezpieczającymi zawiera już solidne systemy kodowania i uwierzytelnienia serwera, ale słaby system uwierzytelniania klienta. Oczywiście użycie SSL 2.0 stawia nas w dużo lepszej sytuacji. Serwery wyposażone w SSL mogą jednocześnie używać podstawowych metod uwierzytelniania omówionych na początku niniejszego rozdziału, ponieważ nie ma możliwości przechwycenia informacji. Nadal jednak nie mamy dowodu na wiarygodność klienta, bo każdy mógłby zgadnąć lub zawładnąć hasłem i nazwą użytkownika.
Powyższy problem rozwiązano w nowszej wersji SSL 3.0 poprzez zapewnienie obsługi certyfikatów klienckich. Te certyfikaty są zarejestrowane jako klienckie, ale niczym nie różnią się od serwerowych. SSL 3.0 z uwierzytelnieniem klienckim pracuje podobnie do SSL 2.0, ale po uwierzytelnieniu serwera przez klienta, serwer żąda od niego certyfikatu. Klient przesyła podpisany certyfikat i serwer przeprowadza taką samą procedurę uwierzytelniającą, jaka odbyła się po stronie klienta (serwer porównuje certyfikat klienta z biblioteką istniejących certyfikatów lub przechowuje certyfikat, aby zweryfikować użytkownika przy kolejnej wizycie). Wiele przeglądarek zanim wyśle certyfikat, jako zabezpieczenie, wymaga podania hasła przez użytkownika.
Klientowi raz uwierzytelnionemu serwer udostępni chronione zasoby (serwlety lub pliki) tak, jak za pomocą uwierzytelnienia HTTP. Cały proces przebiega w sposób niewidoczny dla użytkownika. Ponadto ten sposób dostarcza dodatkowy poziom uwierzytelnienia, bo serwer wie, że klient posługujący się certyfikatem Jasia Kowalskiego jest w istocie Jasiem Kowalskim (w dodatku może rozpoznać konkretnego Jasia Kowalskiego poprzez jego unikatowy certyfikat). Wadami certyfikatów klienckich jest konieczność ich otrzymania i instalacji przez użytkowników. Ponadto serwery muszą zawierać bazę danych z kluczami publicznymi, a przede wszystkim obsługiwać SSL 3.0.
Konfigurowanie ochrony za pomocą SSL
Aplikacja sieciowa, która wymaga ochrony za pomocą SSL (HTTPS) może o tym powiadomić serwer przy użyciu deskryptora rozmieszczenia. Wymaganie SSL modyfikuje plik web.xml w ten sposób, że znacznik <security-constraint> zawiera znacznik <user-data-constraint> wskazujący wymagania dotyczące systemu zabezpieczeń. Przykład 8.12 przedstawia ten plik.
Przykład 8.12.
Ta kolekcja wymaga bezpiecznego połączenia
<!-- ...itp... -->
<security-constraint>
<web-resource-collection>
<web-resource-name>
SecretProtection
</web-resource-name>
<url-pattern>
/servlet/SalaryServer
</url-pattern>
<url-pattern>
/servlet/secret
</url-pattern>
<url-method>
GET
</url-method>
<url-method>
POST
</url-method>
</web-resource-collection>
<auth-constraint>
<role-name>
manager
</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>
CONFIDENTIAL
</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- ...itp... -->
Dodany znacznik <user-data-constraint> informuje o tym, że dostęp do danych jest zapewniany użytkownikom w roli manager oraz, że użytkownicy muszą łączyć się używając trybu CONFIDENTIAL.
Znacznik <transport-guaratee> może zawierać jedną z dwóch wartości: INTEGRAL albo CONFIDENTIAL. INTEGRAL wymaga niezmienności danych podczas transmisji. Z kolei CONFIDENTIAL wymaga poufności danych. Zastosowanie CONFIDENTIAL pociąga za sobą jednoczesne użycie INTEGRAL. Serwer decyduje, które algorytmy kryptograficzne można zakwalifikować jako INTEGRAL, a które jako CONFIDENTIAL. Większość powszechnie znanych algorytmów SSL zalicza się do obu grup. Jednakże serwer może zaliczyć algorytmy wykorzystujące kodowanie 56-bitowe DES do INTEGRAL (informacja nie może być naruszona, aby ją zdekodować), a nie do CONFIDENTIAL (bo łatwo złamać taki kod)*. W praktyce pomiędzy tymi trybami jest niewielka różnica, a ogólnie gwarantowanym standardem jest CONFIDENTIAL.
Znaczniki <auth-constraint> i <user-data-constraint> mogą występować razem lub osobno. Na przykład, strona obsługująca kartę kredytową wymaga komunikacji typu CONFIDENTIAL, ale może pozostać widoczna dla wszystkich użytkowników.
Konfiguracja uwierzytelnienia SSL
SSL 3.0 zyskuje popularność, bo coraz więcej witryn uwierzytelnia użytkowników za pomocą certyfikatów klienckich. To zapewnia automatyczną i bezpieczną metodę identyfikacji użytkownika klienta. W zależności od poziomu zaufania powiązanego z certyfikatem klienckim, można uważać mechanizm uwierzytelnienia za dostatecznie bezpieczny, by wspomagać podpisywanie prawnie wiążących kontraktów lub nawet głosowanie online.
Można zgadnąć, że uwierzytelnienie oparte o certyfikaty klienckie może być wskazane wewnątrz znacznika <login-config>:
<login-config>
<auth-method>
CLIENT_CERT <!—Klient musi zostać zidentyfikowany za pomocą certyfikatu X.509 --!>
</auth-method>
</login-config>
Tak określony znacznik jest informacją dla serwera, że wszystkie procesy uwierzytelnienia dla danej aplikacji będą wykonywane przy użyciu certyfikatów klienckich zamiast tradycyjnych metod: podstawowej lub w oparciu o formularz. Klient nigdy nie ujrzy strony logującej, mimo że przeglądarka poprosi o hasło do odblokowania certyfikatu przed przesłaniem do serwera. Jeśli przeglądarka nie otrzyma certyfikatu klienckiego, dostęp do danych będzie zabroniony.
Wyszukiwanie informacji uwierzytelnienia SSL
Podczas uwierzytelnienia podstawowego i szyfrowanego wszystkie szczegóły dotyczące SSL obsługiwane są przez serwer w sposób niewidoczny dla serwletów. Innymi słowy, serwlet nie musi spełniać jakichkolwiek kryteriów, by mieć dostęp do zabezpieczonego serwera! Serwlet może czasami wyszukiwać użytecznych informacji dotyczących uwierzytelnienia SSL. Na przykład, serwlet może sprawdzić, czy połączenie z klientem jest bezpieczne używając metody isSecure():
public boolean ServletRequest.isSecure()
Metoda zwraca true, jeśli serwer uzna połączenie za bezpieczne. Poziom kodowania zależy od implementacji serwera. Potrzebne detale można znaleźć w dokumentacji serwera. Niestety, serwlet nie może w standardowy sposób zażądać właściwego algorytmu kodowania użytego do połączenia, a nawet długości symetrycznego klucza (40, 56, 128). Taką opcje można ustawić w serwerze jako atrybut o nieokreślonej do tej pory nazwie. Taka cecha jest oczekiwana w Servlet API 2.3, który używa atrybutów żądania: javax.servlet.request.cipher_suite i javax.servlet.request.key_size.
Gdy klient zostanie uwierzytelniony za pomocą CLIENT-CERT metoda getUserPrincipal() zwróci nazwę jego podmiotu. Nazwa ta jest wzięta z pola certyfikatu Distinguished Name.
Za każdym razem, gdy certyfikat kliencki jest wysyłany do serwera, serwlet może otrzymać go jako atrybut żądania. Może to pojawić się podczas zwykłego procesu przesłania z potwierdzeniem SSL 3.0, nawet gdy CLIENT-CERT nie jest określony.
java.security.cert.X509Certificate cert =
(java.security.cert.X509Certificate)
req.getAttribute(„javax.servlet.request.X509Certificate”);
Dla każdego serwera pracującego na J2SE 1.2 (JDK 1.2) lub obsługującego J2EE 1.2 atrybut żądania javax.servlet.request.X509Certyficate zwróci obiekt java.security.cert.X509Certyficate reprezentujący certyfikat X.509v3 (instrukcje konfigurujące można znaleźć w dokumentacji serwera). Serwery pracujące na JDK 1.1, które są niekompatybilne z J2EE 1.2 nie wspierają tego atrybutu, ponieważ JDK 1.1 nie zawiera pakietu java.security.cert. Serwlet z przykładu 8.13 drukuje łańcuch certyfikatu klienta.
Przykład 8.13
Sprawdzanie certyfikatów klienckich
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class X509Snoop extends HttpServlet {
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType ("text/plain");
PrintWriter out = res.getWriter();
X509Certificate[] certs = (X509Certificate[])
req.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null) {
for (int i=0; i<certs.length; i++) {
out.println("certyfikat klienta [" + i + "] = " + certs[i].tostoring());
}
}
else {
if („https”.equals(req.getScheme())){
out.println(„To było HTTPS, ale brak certyfikatu,”+”...”);
}
else {
out.println(„to nie był HTTPS,”+
„wiec żaden certyfikat nie jest możliwy”);
}
}
}
}
Zwrócony certyfikat X509Certyficate może być użyty do sprawdzenia ważności, wydawcy, numeru seryjnego, podpisu i tak dalej. Wydruk certyfikatu jako String pokazano w przykładzie 8.14. Pierwszy certyfikat jest kluczem publicznym użytkownika. Drugi z nich to podpis VeriSign potwierdzający autentyczność pierwszego podpisu.
Przykład 8.14.
Przykładowy certyfikat X509 dla Ramesha Mandavy.
Client Certificate [0] =[
[
Version: V3
Subject: EmailAddress=rmandava@talentportal.com, CN=Ramesh babu mandava, OU=Digital
ID Class 1 -
Netscape, OU=Persona Not Validated, OU="www.verisign.com/repository/RPA Incorp. By Ref., LIAB.LTD©98", OU=VeriSign Trust Network, O="VeriSign, INC."
Signature Algorithm: MD5withRSA, OID = 1.2.840.113549.1.1.4
Key: com.sun.net.ssl.internal.ssl.JSA_RSAPublicKey@5b5870e3
Validity: [From: Tue Oct 10 17:00:00 PDT 2000,
To: sun Dec 10 15:59:59 PST 2000]
Issuer: CN=VeriSign Class 1 CA Individual subscriber-Persona not Validated,
OU="www.verisign.com/repository/RPA incorp. By Ref.,LIAB.LTD(c)98", OU=VeriSign Trust Network, O="VeriSign, Inc."
SerialNumber: [ 1ef11638 5ab8aaa1 bfa2b11b3 c0fb9cd9]
Certificate Extensions: 4
[1]: ObjectId: 2.16.840.1.113730.1.1 Criticality=false
NetscapecertType [
SSL client
]
[2]: ObjectId: 2.5.29.32 Criticality=false
Extension unknown: DER encoded OCTET string =
0000: 04 3D 30 3B 30 39 06 0B 60 86 48 01 86 F8 45 01 .=0;09..'.H...E.
0010: 07 01 08 30 2A 30 28 06 08 2B 06 01 05 05 07 02 ...0*0(..+......
0020: 01 16 1C 68 74 74 70 73 3A 2F 2F 77 77 77 2E 76 ...https:/www.v
0030: 65 72 69 73 69 67 6E 2E 63 6F 6D 2F 72 70 61 erisign.com/rpa
[3]: ObjectId: 2.5.29.31 Criticality=false
Extension unknown: DER encoded OCTET string =
0000: 04 2C 30 2A 30 28 A0 26 A0 24 86 22 68 74 74 70 .,0*0(.&.$."http
0010: 3A 2F 2F 63 72 6C 2E 76 65 72 69 73 69 67 6E 2E ://crl.verisign.
0020: 63 6F 6D 2F 63 6C 61 73 73 31 2E 63 72 6C com/class1.crl
[4]: ObjectId: 2.5.29.19 Criticality=false
BasiConstraints:[
CA:false
PathLen: undefined
]
]
Algorithm: [MD5withRSA]
Signature:
0000: 5E EC 5C F9 96 D5 3F F6 19 8B 66 0A 46 DE 02 FC ^.\...?...f.F...
0010: 52 4E 32 70 5F DA 8B 92 43 F4 19 51 C3 A3 36 7D RN2P^...C...Q..6
0020: 02 4A 5B 35 B6 76 05 F8 FE C0 4F D7 9C B1 5B BA .J[5.v....O...[.
0030: EE 38 A7 98 C5 57 A7 6B 86 B9 B2 A1 4F 25 5F FF .8...W.k....0%_.
0040: OB 19 54 86 D7 14 7A F7 97 A1 E8 E7 D3 89 75 B0 ..T...z.......u.
0050: 72 4F 4B 77 E4 56 5D B2 40 D2 7E 69 26 77 DD F1 rOkw.V].@..i&w..
0060: E6 31 3D F2 EF 5A 11 22 78 23 47 C2 D6 ED DD 14 .1=..Z."x#G.....
0070: 2F E9 2E 46 73 D9 20 72 BF 9B 6C 04 12 0D 68 C7 /..Fs. r..l...h.
]
Client Certificate [1] = [
[
Version: V3
Subject: CN=VeriSign Class 1 CA Individual Subsciber-Persona Not Validated,
OU="www.verisign,com/repository/RPA Incorp. By Ref., LIAB.LTD(c)98", OU=VeriSign Trust Network, O="VeriSign, Inc."
Signature Algorithm: MD2withRSA, OID = 1.2.840.113549.1.1.2
Key: com.sun.net.ssl.internal.ssl.JSA_RSAPublicKey@9ae870e3
Validity: [From: Mon May 11 17:00:00 PDT 1998,
To: Sun May 12 16:59:59 PST 2008]
Issuer: OU=Class 1 Public Primary Certification Authority, O="VeriSign, Inc.", C=US
SerialNumber: [ d2762e8d 140c3d7d b2a8255d afee0d75 ]
Certificate Extensions: 4
[1]: ObjectId: 2.16.840.1.113730.1.1 Criticality=false
NetscapeCerttype [
SSL CA
S/MIME CA
]
[2]: ObjesctID: 2.5.29.32 Criticality=false
Extension unknown: DER encoded OCTET string =
0000: 04 40 30 3E 30 3C 06 0B 60 86 48 01 86 E8 45 01 .@0>0<..'.H...E.
0010: 07 01 01 30 2D 30 2B 06 08 2B 06 01 05 05 07 02 ...0-0+..+......
0020: 01 16 1F 77 77 77 2E 76 65 72 69 73 69 67 6E 2E ...www.verisign.
0030: 63 6F 6D 2F 72 65 70 6F 73 69 74 6F 72 79 2F 52 com/repository/R
0040: 50 41 PA
[3]: ObjectId: 2.5.29.15 Criticality=false
KeyUsage [
Key_CertSign
Crl_Sign
]
[4]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
CA:true
PathLen:0
]
]
Algorithm: [MD2withRSA]
Signature:
0000: 88 B8 37 3B DD DA 94 37 00 AD AA 9F E1 81 01 71 ..7;...7.......q
0010: 1E 92 6A 6D 2F F6 F1 9D D3 CA 64 38 DC 1B 98 0C ..jm/.....d8....
0020: 07 86 5B 85 15 6A 0F B9 49 85 A4 95 F1 17 7D 67 ..[..j..I......g
0030: B4 7F 2D 2C DD 9A 42 9E C3 3E B4 8E AA E5 0B 06 ..-,..B..>......
0040: DE F2 56 2A FA 33 C7 BE 19 D7 53 4C C3 BD C8 E3 ..V*.3....SL....
0050: 17 B5 A4 49 42 63 EC C2 A6 17 0F 5D 58 1A 49 3C ...Ibc.....]X.I<
0060: 90 5C 55 A3 65 20 00 FD 18 20 E5 5F 82 A6 B1 A8 .\U.e... ._....
0070: 92 C5 58 6A C1 8D 03 3C EB C3 CD 05 A2 90 AE 6E ..Xj...<.......n
]
* Plik opisu rozmieszczenia jest również znany jako deskryptor rozmieszczenia - te dwa określenia można stosować zamiennie.
* Można także użyć klasy sun.misc.BASE64Decoder, która towarzyszy JDK.
* Patent USA nr 4.405.829. Utracił ważność 20 września 2000 r.
** Szczerze mówiąc, ludzie nie spotykają się w ciemnej alejce po to, aby wymienić swoje klucze publiczne. Zamiast tego wymieniają klucze w cyfrowy sposób (np. za pomocą email) i rozpoznają klucz po kilku pierwszych znakach.
* Połączenie klient-serwer poprzez bezpieczną sieć VPN może spotkac się z ograniczeniem CONFIDENTIAL nawet bez użycia SSL.
W tym rozdziale:
Uwierzytelnienie poprzez HTTP
Uwierzytelnienie w oparciu o formularz
Uwierzytelnienie niestandardowe
Certyfikaty cyfrowe
Protokół bezpiecznej transmisji danych(SSL)