Wstęp
W tej recenzji chciałbym poruszyć taki temat jak bezpieczeństwo aplikacji internetowych. Java ma kilka technologii zapewniających bezpieczeństwo:-
„ Architektura zabezpieczeń platformy Java SE ”, więcej szczegółów można przeczytać w Przewodniku firmy Oracle: „ Architektura zabezpieczeń platformy JavaTM SE ”. Architektura ta opisuje, w jaki sposób musimy zabezpieczyć nasze aplikacje Java w środowisku wykonawczym Java SE. Ale nie to jest tematem naszej dzisiejszej rozmowy.
-
„ Architektura kryptografii Java ” to rozszerzenie Java opisujące szyfrowanie danych. Więcej o tym rozszerzeniu można przeczytać na JavaRush w recenzji „ Java Cryptography Architecture: First znajomy ” lub w Przewodniku firmy Oracle: „ Java Cryptography Architecture (JCA) Reference Guide ”.
JAAS
JAAS jest rozszerzeniem Java SE i jest opisany w Przewodniku po usłudze uwierzytelniania i autoryzacji Java (JAAS) . Jak sugeruje nazwa technologii, JAAS opisuje, w jaki sposób należy przeprowadzać uwierzytelnianie i autoryzację:-
„ Uwierzytelnienie ”: W tłumaczeniu z języka greckiego „authentikos” oznacza „prawdziwy, autentyczny”. Oznacza to, że uwierzytelnianie jest testem autentyczności. Że ktokolwiek jest uwierzytelniany, jest naprawdę tym, za kogo się podaje.
-
„ Upoważnienie ”: przetłumaczone z języka angielskiego oznacza „pozwolenie”. Oznacza to, że autoryzacja to kontrola dostępu przeprowadzana po pomyślnym uwierzytelnieniu.
- jego prawo jazdy jako reprezentacja osoby jako użytkownika drogi
- paszportu, jako reprezentację osoby jako obywatela swojego kraju
- paszport zagraniczny, jako reprezentacja osoby jako uczestnika stosunków międzynarodowych
- jego kartę biblioteczną w bibliotece, jako reprezentację osoby jako czytelnika dołączonej do biblioteki
Aplikacja internetowa
Potrzebujemy więc aplikacji internetowej. Pomoże nam w jego stworzeniu system automatycznego budowania projektów Gradle. Dzięki zastosowaniu Gradle możemy wykonując małe polecenia złożyć projekt Java w potrzebnym nam formacie, automatycznie utworzyć niezbędną strukturę katalogów i wiele więcej. Więcej o Gradle możesz przeczytać w krótkim przeglądzie: „ Krótkie wprowadzenie do Gradle ” lub w oficjalnej dokumentacji „ Gradle Pierwsze kroki ”. Musimy zainicjalizować projekt (Inicjalizacja) i w tym celu Gradle ma specjalną wtyczkę: „ Gradle Init Plugin ” (Init to skrót od Inicjalizacja, łatwy do zapamiętania). Aby użyć tej wtyczki, uruchom polecenie w wierszu poleceń:gradle init --type java-application
Po pomyślnym zakończeniu będziemy mieli projekt Java. Otwórzmy teraz skrypt kompilacji naszego projektu do edycji. Skrypt kompilacji to plik o nazwie build.gradle
, który opisuje niuanse kompilacji aplikacji. Stąd nazwa, skrypt kompilacji. Można powiedzieć, że jest to skrypt budujący projekt. Gradle to takie wszechstronne narzędzie, którego podstawowe możliwości rozszerzane są za pomocą wtyczek. Dlatego przede wszystkim zwróćmy uwagę na blok „wtyczek”:
plugins {
id 'java'
id 'application'
}
Domyślnie Gradle, zgodnie z tym, co określiliśmy „ --type java-application
”, skonfigurował zestaw kilku podstawowych wtyczek, to znaczy tych wtyczek, które są zawarte w dystrybucji samego Gradle. Jeżeli przejdziemy do sekcji „Dokumenty” (czyli dokumentacja) na stronie gradle.org , to po lewej stronie listy tematów w sekcji „Referencje” zobaczymy sekcję „ Core Plugins ”, czyli: sekcję z opisem tych bardzo podstawowych wtyczek. Wybierzmy dokładnie te wtyczki, których potrzebujemy, a nie te, które wygenerował dla nas Gradle. Zgodnie z dokumentacją „ Gradle Java Plugin ” zapewnia podstawowe operacje na kodzie Java, takie jak kompilacja kodu źródłowego. Ponadto zgodnie z dokumentacją „ wtyczka aplikacji Gradle ” udostępnia nam narzędzia do pracy z „wykonywalną aplikacją JVM”, tj. z aplikacją Java, którą można uruchomić jako samodzielną aplikację (na przykład aplikację konsolową lub aplikację z własnym interfejsem użytkownika). Okazuje się, że wtyczka „aplikacja” jest nam zbędna, bo… nie potrzebujemy samodzielnej aplikacji, potrzebujemy aplikacji internetowej. Usuńmy to. Jak również ustawienie „mainClassName”, które jest znane tylko tej wtyczce. Ponadto w tej samej sekcji „ Pakowanie i dystrybucja ”, w której podano łącze do dokumentacji wtyczki aplikacji, znajduje się łącze do wtyczki Gradle War. Wtyczka Gradle War , jak podano w dokumentacji, zapewnia obsługę tworzenia aplikacji internetowych Java w formacie wojennym. W formacie WAR oznacza, że zamiast archiwum JAR zostanie utworzone archiwum WAR. Wydaje się, że właśnie tego nam potrzeba. Ponadto, jak mówi dokumentacja, „Wtyczka War rozszerza wtyczkę Java”. Oznacza to, że możemy zastąpić wtyczkę Java wtyczką war. Dlatego nasz blok wtyczek będzie ostatecznie wyglądał następująco:
plugins {
id 'war'
}
Również w dokumentacji „Gradle War Plugin” jest powiedziane, że wtyczka wykorzystuje dodatkowy „Układ projektu”. Układ jest tłumaczony z języka angielskiego jako lokalizacja. Oznacza to, że wtyczka wojenna domyślnie oczekuje istnienia określonej lokalizacji plików, których będzie używać do swoich zadań. Będzie używać następującego katalogu do przechowywania plików aplikacji internetowych: src/main/webapp
Zachowanie wtyczki opisano w następujący sposób:
web.xml
- jest to „Deskryptor wdrożenia” lub „deskryptor wdrożenia”. Jest to plik opisujący sposób skonfigurowania naszej aplikacji webowej do działania. Plik ten określa, jakie żądania będzie obsługiwać nasza aplikacja, ustawienia zabezpieczeń i wiele więcej. W swej istocie jest nieco podobny do pliku manifestu z pliku JAR (zobacz „ Praca z plikami manifestu: podstawy ”). Plik Manifest informuje, jak pracować z aplikacją Java (tj. archiwum JAR), a plik web.xml informuje, jak pracować z aplikacją internetową Java (tj. archiwum WAR). Sama koncepcja „deskryptora wdrożenia” nie powstała sama, ale została opisana w dokumencie „ Specyfikacja API serwletu”". Każda aplikacja internetowa Java zależy od tego "Servlet API". Ważne jest, aby zrozumieć, że jest to API - to znaczy jest to opis jakiejś umowy interakcji. Aplikacje internetowe nie są aplikacjami niezależnymi. Działają na serwerze WWW , która zapewnia komunikację sieciową z użytkownikami. Czyli serwer www to swego rodzaju „kontener” na aplikacje webowe. Jest to logiczne, bo chcemy napisać logikę aplikacji webowej, czyli jakie strony zobaczy użytkownik i w jaki sposób powinny reagować na działania użytkownika. A my nie chcemy pisać kodu, w jaki sposób wiadomość zostanie wysłana do użytkownika, w jaki sposób będą przesyłane bajty informacji i inne rzeczy niskiego poziomu i bardzo wymagające pod względem jakości. Poza tym, okazuje się, że aplikacje internetowe są różne, ale transfer danych jest taki sam. Oznacza to, że milion programistów musiałoby w kółko pisać kod w tym samym celu. Zatem serwer WWW jest odpowiedzialny za część interakcji użytkownika i wymianę danych, a za generowanie tych danych odpowiedzialna jest aplikacja internetowa i jej programista. A żeby połączyć te dwie części, tj. serwer WWW i aplikacja internetowa, na ich interakcję potrzebna jest umowa, czyli tzw. jakich zasad będą się trzymać, aby to zrobić? Aby w jakiś sposób opisać umowę, jak powinna wyglądać interakcja pomiędzy aplikacją internetową a serwerem WWW, wymyślono Servlet API. Co ciekawe, nawet jeśli używasz frameworków takich jak Spring, pod maską nadal działa API serwletów. Oznacza to, że używasz Springa, a Spring współpracuje z Servlet API za Ciebie. Okazuje się, że nasz projekt aplikacji internetowej musi opierać się na Servlet API. W tym przypadku API serwletu będzie zależnością. Jak wiemy, Gradle pozwala także na opisanie zależności projektu w sposób deklaratywny. Wtyczki opisują sposób zarządzania zależnościami. Na przykład wtyczka Java Gradle wprowadza metodę zarządzania zależnościami „testImplementation”, która mówi, że taka zależność jest potrzebna tylko do testów. Ale wtyczka Gradle War dodaje metodę zarządzania zależnościami „providedCompile”, która mówi, że taka zależność nie zostanie uwzględniona w archiwum WAR naszej aplikacji internetowej. Dlaczego nie umieścimy interfejsu API serwletu w naszym archiwum WAR? Ponieważ Servlet API zostanie dostarczony do naszej aplikacji internetowej przez sam serwer WWW. Jeśli serwer WWW udostępnia interfejs API serwletów, wówczas serwer nazywany jest kontenerem serwletów. Dlatego też serwer WWW jest odpowiedzialny za udostępnienie nam Servlet API, a naszym obowiązkiem jest udostępnienie ServletAPI tylko w momencie kompilacji kodu. Dlatego providedCompile
. Zatem blok zależności będzie wyglądał następująco:
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testImplementation 'junit:junit:4.12'
}
Wróćmy więc do pliku web.xml. Domyślnie Gradle nie tworzy żadnego deskryptora wdrożenia, więc musimy to zrobić sami. Stwórzmy katalog src/main/webapp/WEB-INF
, a w nim utworzymy plik XML o nazwie web.xml
. Otwórzmy teraz samą „Specyfikację serwletu Java” i rozdział „ ROZDZIAŁ 14 Deskryptor wdrażania ”. Jak stwierdzono w „14.3 Deskryptor wdrażania”, dokument XML deskryptora wdrożenia jest opisany przez schemat http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd . Schemat XML opisuje, z jakich elementów może składać się dokument i w jakiej kolejności powinny się one pojawiać. Które są obowiązkowe, a które nie. Ogólnie opisuje strukturę dokumentu i pozwala sprawdzić, czy dokument XML jest poprawnie skomponowany. Posłużmy się teraz przykładem z rozdziału „ 14.5 Przykłady ”, ale schemat musi być określony dla wersji 3.1, tj.
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd
Nasz pusty web.xml
będzie wyglądał tak:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>JAAS Example</display-name>
</web-app>
Opiszmy teraz serwlet, który będziemy chronić za pomocą JAAS. Wcześniej Gradle generował dla nas klasę App. Zamieńmy go w serwlet. Jak stwierdzono w specyfikacji w „ ROZDZIAŁ 2 Interfejs serwletu ”, że „ W większości zastosowań programiści będą rozszerzać HttpServlet w celu implementacji swoich serwletów ”, to znaczy, aby uczynić klasę serwletem, należy dziedziczyć tę klasę z HttpServlet
:
public class App extends HttpServlet {
public String getGreeting() {
return "Secret!";
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().print(getGreeting());
}
}
Jak powiedzieliśmy, Servlet API to umowa pomiędzy serwerem a naszą aplikacją internetową. Umowa ta pozwala nam opisać, że gdy użytkownik skontaktuje się z serwerem, serwer wygeneruje żądanie od użytkownika w postaci obiektu HttpServletRequest
i przekaże je do serwletu. Udostępni także serwletowi obiekt, HttpServletResponse
dzięki czemu będzie mógł napisać do niego odpowiedź dla użytkownika. Po zakończeniu działania serwletu serwer będzie mógł na jego podstawie udzielić użytkownikowi odpowiedzi HttpServletResponse
. Oznacza to, że serwlet nie komunikuje się bezpośrednio z użytkownikiem, a jedynie z serwerem. Aby serwer wiedział, że mamy serwlet i do jakich żądań ma zostać użyty, musimy poinformować o tym serwer w deskryptorze wdrażania:
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>jaas.App</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/secret</url-pattern>
</servlet-mapping>
W takim przypadku wszystkie żądania /secret
nie będą kierowane do naszego jednego serwletu po nazwie app
, która odpowiada klasie jaas.App
. Jak powiedzieliśmy wcześniej, aplikację internetową można wdrożyć tylko na serwerze internetowym. Serwer WWW można zainstalować osobno (samodzielnie). Jednak na potrzeby tej recenzji odpowiednia jest alternatywna opcja - działanie na serwerze wbudowanym. Oznacza to, że serwer zostanie utworzony i uruchomiony programowo (wtyczka zrobi to za nas), a jednocześnie zostanie na nim wdrożona nasza aplikacja internetowa. System kompilacji Gradle umożliwia użycie wtyczki „ Gradle Gretty Plugin ” do następujących celów:
plugins {
id 'war'
id 'org.gretty' version '2.2.0'
}
Dodatkowo wtyczka Gretty ma dobrą dokumentację . Zacznijmy od tego, że wtyczka Gretty umożliwia przełączanie pomiędzy różnymi serwerami WWW. Jest to opisane bardziej szczegółowo w dokumentacji: „ Przełączanie pomiędzy kontenerami serwletów ”. Przejdźmy do Tomcata, bo... jest jednym z najpopularniejszych w użyciu, a także posiada dobrą dokumentację oraz wiele przykładów i analizowanych problemów:
gretty {
// Переключаемся с дефолтного Jetty на Tomcat
servletContainer = 'tomcat8'
// Укажем Context Path, он же Context Root
contextPath = '/jaas'
}
Teraz możemy uruchomić „gradle appRun”, a wtedy nasza aplikacja internetowa będzie dostępna pod adresem http://localhost:8080/jaas/secret
Uwierzytelnianie
Ustawienia uwierzytelniania często składają się z dwóch części: ustawień po stronie serwera i ustawień po stronie aplikacji internetowej działającej na tym serwerze. Ustawienia zabezpieczeń aplikacji internetowej nie mogą nie wchodzić w interakcję z ustawieniami zabezpieczeń serwera internetowego, choćby z innego powodu niż to, że aplikacja internetowa nie może nie wchodzić w interakcję z serwerem internetowym. Nie na próżno przeszliśmy na Tomcata, bo... Tomcat ma dobrze opisaną architekturę (patrz „ Architektura Apache Tomcat 8 ”). Z opisu tej architektury jasno wynika, że Tomcat, jako serwer WWW, reprezentuje aplikację internetową jako pewien kontekst, który nazywany jest „ Kontekstem Tomcat ”. Ten kontekst pozwala każdej aplikacji internetowej mieć własne ustawienia, odizolowane od innych aplikacji internetowych. Dodatkowo aplikacja internetowa może mieć wpływ na ustawienia tego kontekstu. Elastyczny i wygodny. Aby uzyskać głębsze zrozumienie, zalecamy przeczytanie artykułu „ Zrozumienie kontenerów kontekstowych Tomcat ” i sekcji dokumentacji Tomcat „ Kontener kontekstowy ”. Jak wspomniano powyżej, nasza aplikacja internetowa może wpływać na kontekst Tomcat naszej aplikacji za pomocą pliku/META-INF/context.xml
. Jednym z bardzo ważnych ustawień, na które możemy wpływać, są Security Realms. Security Realms to swego rodzaju „obszar bezpieczeństwa”. Obszar, dla którego określono określone ustawienia zabezpieczeń. W związku z tym, korzystając ze Dziedziny Bezpieczeństwa, stosujemy ustawienia bezpieczeństwa określone dla tej Dziedziny. Security Realms zarządzane są poprzez kontener, tj. serwer WWW, a nie nasza aplikacja internetowa. Możemy jedynie powiedzieć serwerowi, jaki zakres bezpieczeństwa należy rozszerzyć na naszą aplikację. Dokumentacja Tomcat w sekcji „ Komponent Realm ” opisuje dziedzinę jako zbiór danych o użytkownikach i ich rolach służących do przeprowadzania uwierzytelniania. Tomcat udostępnia zestaw różnych implementacji Security Realm, z których jedną jest „ Jaas Realm ”. Po zrozumieniu odrobiny terminologii, opiszemy kontekst Tomcat w pliku /META-INF/context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Realm className="org.apache.catalina.realm.JAASRealm"
appName="JaasLogin"
userClassNames="jaas.login.UserPrincipal"
roleClassNames="jaas.login.RolePrincipal"
configFile="jaas.config" />
</Context>
appName
- Nazwa aplikacji. Tomcat spróbuje dopasować tę nazwę do nazw określonych w pliku configFile
. configFile
- jest to "plik konfiguracyjny logowania". Przykład tego można zobaczyć w dokumentacji JAAS: „ Załącznik B: Przykładowe konfiguracje logowania ”. Dodatkowo ważne jest, aby ten plik był najpierw przeszukiwany w zasobach. Dlatego nasza aplikacja internetowa może sama udostępnić ten plik. Atrybuty userClassNames
i roleClassNames
zawierają wskazanie klas reprezentujących podmiot główny użytkownika. JAAS oddziela pojęcia „użytkownika” i „roli” jako dwa różne pojęcia java.security.Principal
. Opiszmy powyższe klasy. Stwórzmy najprostszą implementację dla użytkownika głównego:
public class UserPrincipal implements Principal {
private String name;
public UserPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
Powtórzymy dokładnie tę samą implementację dla RolePrincipal
. Jak widać z interfejsu, najważniejszą rzeczą dla zleceniodawcy jest przechowywanie i zwracanie nazwy (lub identyfikatora) reprezentującej zleceniodawcę. Mamy teraz Królestwo Bezpieczeństwa, mamy główne klasy. Pozostaje wypełnić plik z configFile
atrybutu „ ”, czyli login configuration file
. Jego opis można znaleźć w dokumentacji Tomcata: „ The Realm Component ”.
\src\main\resources\jaas.config
Ustawmy zawartość tego pliku:
JaasLogin {
jaas.login.JaasLoginModule required debug=true;
};
Warto zauważyć, że context.xml
ta sama nazwa jest używana tutaj i w. Spowoduje to mapowanie obszaru zabezpieczeń na moduł logowania. Zatem kontekst Tomcat powiedział nam, które klasy reprezentują podmioty główne, a także jakiego modułu LoginModule użyć. Wszystko, co musimy zrobić, to zaimplementować ten LoginModule. LoginModule jest prawdopodobnie jedną z najciekawszych rzeczy w JAAS. Oficjalna dokumentacja pomoże nam w opracowaniu LoginModule: „ Usługa uwierzytelniania i autoryzacji Java (JAAS): Przewodnik programisty LoginModule ”. Zaimplementujmy moduł logowania. Stwórzmy klasę implementującą interfejs LoginModule
:
public class JaasLoginModule implements LoginModule {
}
Najpierw opisujemy metodę inicjalizacji LoginModule
:
private CallbackHandler handler;
private Subject subject;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, <String, ?> sharedState, Map<String, ?> options) {
handler = callbackHandler;
this.subject = subject;
}
Metoda ta pozwoli zapisać plik Subject
, który będziemy dalej uwierzytelniać i wypełniać informacjami o zleceniodawcy. Zachowamy także do wykorzystania w przyszłości CallbackHandler
to, co zostało nam dane. Z pomocą CallbackHandler
możemy nieco później poprosić o różne informacje na temat podmiotu uwierzytelnienia. Więcej na ten temat można przeczytać CallbackHandler
w odpowiedniej sekcji dokumentacji: „ Przewodnik referencyjny JAAS: CallbackHandler ”. Następnie wykonywana jest metoda login
uwierzytelniania Subject
. To jest pierwsza faza uwierzytelniania:
@Override
public boolean login() throws LoginException {
// Добавляем колбэки
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("login");
callbacks[1] = new PasswordCallback("password", true);
// При помощи колбэков получаем через CallbackHandler логин и пароль
try {
handler.handle(callbacks);
String name = ((NameCallback) callbacks[0]).getName();
String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());
// Далее выполняем валидацию.
// Тут просто для примера проверяем определённые значения
if (name != null && name.equals("user123") && password != null && password.equals("pass123")) {
// Сохраняем информацию, которая будет использована в методе commit
// Не "пачкаем" Subject, т.к. не факт, что commit выполнится
// Для примера проставим группы вручную, "хардkodно".
login = name;
userGroups = new ArrayList<String>();
userGroups.add("admin");
return true;
} else {
throw new LoginException("Authentication failed");
}
} catch (IOException | UnsupportedCallbackException e) {
throw new LoginException(e.getMessage());
}
}
Ważne jest, abyśmy login
nie zmieniali pliku Subject
. Zmiany takie powinny nastąpić jedynie w metodzie potwierdzenia commit
. Następnie musimy opisać metodę potwierdzania udanego uwierzytelnienia:
@Override
public boolean commit() throws LoginException {
userPrincipal = new UserPrincipal(login);
subject.getPrincipals().add(userPrincipal);
if (userGroups != null && userGroups.size() > 0) {
for (String groupName : userGroups) {
rolePrincipal = new RolePrincipal(groupName);
subject.getPrincipals().add(rolePrincipal);
}
}
return true;
}
Oddzielenie metody login
i commit
. Ale chodzi o to, że moduły logowania można łączyć. Aby uwierzytelnienie przebiegło pomyślnie, może być konieczne pomyślne działanie kilku modułów logowania. I tylko jeśli wszystkie niezbędne moduły zadziałały, zapisz zmiany. To drugi etap uwierzytelniania. Zakończmy metodami abort
i logout
:
@Override
public boolean abort() throws LoginException {
return false;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().remove(userPrincipal);
subject.getPrincipals().remove(rolePrincipal);
return true;
}
Metoda abort
wywoływana jest w przypadku niepowodzenia pierwszej fazy uwierzytelniania. Metoda logout
wywoływana jest w momencie wylogowania się z systemu. Po zaimplementowaniu Login Module
i skonfigurowaniu naszego Security Realm
, musimy teraz wskazać web.xml
fakt, że chcemy użyć konkretnego Login Config
:
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>JaasLogin</realm-name>
</login-config>
Podaliśmy nazwę naszego obszaru bezpieczeństwa i określiliśmy metodę uwierzytelniania - BASIC. Jest to jeden z typów uwierzytelniania opisanych w Servlet API w sekcji „ 13.6 Uwierzytelnianie ”. Pozostał n
GO TO FULL VERSION