Treść:
Wstęp
We współczesnym świecie nie można żyć bez aplikacji internetowych. Zaczniemy od małego eksperymentu. Pamiętam, że jako dziecko na wszystkich straganach sprzedawano gazetę „Argumenty i Fakty”. Zapamiętałem je, ponieważ według mojego osobistego spostrzeżenia z dzieciństwa te gazety zawsze wyglądały dziwnie. I zdecydowałem, czy powinniśmy wejść na ich stronę internetową:
Jeżeli przejdziemy do pomocy przeglądarki Google Chrome, przeczytamy, że ta witryna nie korzysta z bezpiecznego połączenia i informacje wymieniane z witryną mogą być dostępne dla osób trzecich. Sprawdźmy inne wiadomości, na przykład wiadomości z Petersburga z Fontanki, mediów elektronicznych:
Jak widać, według tych danych, witryna Fontanka nie ma problemów z bezpieczeństwem. Okazuje się, że zasoby sieciowe mogą, ale nie muszą, być bezpieczne. Widzimy również, że dostęp do niechronionych zasobów następuje poprzez protokół HTTP. A jeśli zasób jest chroniony, to wymiana danych odbywa się za pomocą protokołu HTTPS, gdzie litera S na końcu oznacza „Bezpieczny”. Protokół HTTPS opisano w specyfikacji rfc2818: „
HTTP Over TLS ”. Spróbujmy stworzyć własną aplikację internetową i przekonajmy się jak to działa. Po drodze zrozumiemy warunki.
Aplikacja internetowa w Javie
Musimy więc stworzyć bardzo prostą aplikację internetową w Javie. Po pierwsze potrzebujemy samej aplikacji Java. W tym celu użyjemy systemu automatycznego budowania projektu Gradle. Dzięki temu nie będziemy musieli ręcznie tworzyć niezbędnej struktury katalogów + Gradle zajmie się za nas wszystkimi bibliotekami niezbędnymi do realizacji projektu i zadba o to, aby były one dostępne podczas wykonywania kodu. Więcej o Gradle możesz przeczytać w krótkiej recenzji: „
Krótkie wprowadzenie do Gradle ”. Użyjmy
wtyczki Gradle Init i uruchommy polecenie:
gradle init --type java-application
Następnie otwórzmy skrypt kompilacji
build.gradle
, który opisuje z jakich bibliotek składa się nasz projekt, a jakie dostarczy nam Gradle. Dodajmy tam zależność od serwera WWW, na którym będziemy eksperymentować:
dependencies {
implementation 'io.undertow:undertow-core:2.0.20.Final'
testImplementation 'junit:junit:4.12'
}
Aby aplikacja internetowa działała, na pewno potrzebujemy serwera WWW, na którym będzie hostowana nasza aplikacja. Istnieje ogromna różnorodność serwerów internetowych, ale główne z nich to: Tomcat, Jetty, Undertow. Tym razem wybierzemy Undertow. Aby zrozumieć, jak możemy współpracować z naszym serwerem internetowym, przejdźmy do oficjalnej strony
Undertow i przejdź do sekcji
dokumentacji . Ty i ja połączyliśmy zależność od Undertow Core, dlatego interesuje nas rozdział dotyczący właśnie tego
Core , czyli rdzenia, będącego podstawą serwera WWW. Najłatwiej jest użyć API Buildera dla Undertow:
public static void main(String[] args) {
Undertow server = Undertow.builder()
.addHttpListener(8080, "localhost")
.setHandler(new HttpHandler() {
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
exchange.getResponseSender().send("Hello World");
}
}).build();
server.start();
}
Jeśli wykonamy kod, możemy przejść do następującego zasobu internetowego:
To działa po prostu. Dzięki API Undertow Builder dodajemy słuchacza HTTP do localhost i portu 8080. Ten słuchacz odbiera żądania z przeglądarki internetowej i w odpowiedzi zwraca ciąg znaków „Hello World”. Świetna aplikacja internetowa. Ale jak widzimy, używamy protokołu HTTP, tj. Ten rodzaj wymiany danych nie jest bezpieczny. Zastanówmy się, jak przeprowadzana jest wymiana za pomocą protokołu HTTPS.
Wymagania dla protokołu HTTPS
Aby zrozumieć, jak włączyć HTTPS, wróćmy do specyfikacji HTTPS: „
RFC-2818: HTTP Over TLS ”. Zgodnie ze specyfikacją dane w protokole HTTPS przesyłane są za pośrednictwem protokołów kryptograficznych SSL lub TLS. Koncepcje SSL i TLS często wprowadzają ludzi w błąd. W rzeczywistości protokół SSL ewoluował i zmienił swoje wersje. Później TLS stał się kolejnym krokiem w rozwoju protokołu SSL. Oznacza to, że TLS jest po prostu nową wersją protokołu SSL. Specyfikacja tak mówi: „SSL i jego następca TLS”. Dowiedzieliśmy się więc, że istnieją protokoły kryptograficzne SSL/TLS.
SSL to skrót od Secure Sockets Layer i tłumaczy się jako „bezpieczna warstwa gniazd”. Gniazdo przetłumaczone z języka angielskiego to złącze. Uczestnicy transmisji danych w sieci wykorzystują gniazda jako interfejs programistyczny (czyli API) do komunikacji między sobą w sieci. Przeglądarka pełni rolę klienta i korzysta z gniazda klienta, natomiast serwer odbierający żądanie i wysyłający odpowiedź korzysta z gniazda serwera. I to właśnie pomiędzy tymi gniazdami następuje wymiana danych. Dlatego też protokół pierwotnie nosił nazwę SSL. Ale czas mijał, a protokół ewoluował. W pewnym momencie protokół SSL stał się protokołem TLS. TLS jest skrótem od Transport Layer Security. Z kolei protokół TLS bazuje na specyfikacji protokołu SSL w wersji 3.0. Protokół TLS jest tematem odrębnych artykułów i recenzji, dlatego po prostu wskażę materiały, które wydają mi się interesujące:
Krótko mówiąc, podstawą HTTPS jest uzgadnianie TLS i sprawdzenie „Tożsamości serwera” (tj. identyfikacji serwera) przy użyciu jego certyfikatu cyfrowego. To jest ważne. Pamiętajmy o tym, bo... Do tego faktu powrócimy później. Dlatego wcześniej użyliśmy HttpListener, aby poinformować serwer, jak ma działać poprzez protokół HTTP. Jeśli w powyższym przykładzie dodaliśmy HttpListener do pracy przez HTTP, to aby pracować przez HTTPS musimy dodać HttpsListener:
Ale aby go dodać, potrzebujemy SSLContext. Co ciekawe, SSLContext nie jest klasą z Undertow, ale
javax.net.ssl.SSLContext
. Klasa SSLContext jest częścią tzw. „
Java Secure Socket Extension ” (JSSE) – rozszerzenia Java zapewniającego bezpieczeństwo połączenia internetowego. To rozszerzenie opisano w „
Przewodniku informacyjnym dotyczącym rozszerzenia Java Secure Socket Extension (JSSE) ”. Jak widać ze wstępnej części dokumentacji, JSSE zapewnia framework i implementację protokołów SSL i TLS w języku Java. Jak uzyskać SSLContext? Otwórz plik JavaDoc SSLContext i znajdź metodę
getInstance . Jak widać, aby uzyskać SSLContext, musimy podać nazwę „Secure Socket Protocol”. Z opisu parametrów wynika, że nazwy te można znaleźć w „
Dokumentacji nazw algorytmów standardu Java Cryptography Architecture Standard ”. Postępujmy więc zgodnie z instrukcją i przejdźmy do dokumentacji. I widzimy, że możemy wybierać pomiędzy SSL i TLS:
Teraz rozumiemy, że musimy utworzyć SSLContext w następujący sposób:
public SSLContext getSSLContext() {
SSLContext context = null;
try {
context = SSLContext.getInstance("TLS");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
return context;
}
Po utworzeniu nowego kontekstu pamiętamy, że SSLContext został opisany w „
Przewodniku referencyjnym Java Secure Socket Extension (JSSE) ”. Czytamy i widzimy, że „Nowo utworzony SSLContext należy zainicjować wywołując metodę init”. Oznacza to, że stworzenie kontekstu nie wystarczy. Należy go zainicjować. I to jest logiczne, ponieważ jeśli chodzi o bezpieczeństwo, powiedzieliśmy tylko, że chcemy korzystać z protokołu TLS. Aby zainicjować SSLContext, musimy zapewnić trzy rzeczy: KeyManager, TrustManager, SecureRandom.
Menedżer kluczy
KeyManager to menedżer kluczy. Jest on odpowiedzialny za to, jakie „dane uwierzytelniające” należy przekazać osobie, która się z nami kontaktuje. Poświadczenie można przetłumaczyć jako tożsamość. Tożsamość jest potrzebna, aby klient miał pewność, że serwer jest tym, za kogo się podaje i można mu zaufać. Co będzie używane jako identyfikacja? Jak pamiętamy, tożsamość serwera weryfikowana jest za pomocą certyfikatu cyfrowego serwera. Proces ten można przedstawić następująco:
Dodatkowo w „
JSSE Reference Guide: How SSL Works ” podano, że SSL wykorzystuje „kryptografię asymetryczną”, co oznacza, że potrzebujemy pary kluczy: klucza publicznego i klucza prywatnego. Ponieważ mówimy o kryptografii, w grę wchodzi „Java Cryptography Architecture” (JCA). Oracle udostępnia doskonały dokument na temat tej architektury: „
Przewodnik referencyjny architektury Java Cryptography (JCA) ”. Ponadto możesz przeczytać krótki przegląd JCA na JavaRush: „
Architektura kryptografii Java: pierwsza znajomość ”. Tak więc, aby zainicjować KeyManager, potrzebujemy KeyStore, w którym będzie przechowywany certyfikat naszego serwera. Najpopularniejszym sposobem tworzenia magazynu kluczy i certyfikatów jest narzędzie keytool dołączone do pakietu JDK. Przykład można zobaczyć w dokumentacji JSSE: „
Tworzenie magazynu kluczy do użycia z JSSE ”. Musimy więc użyć narzędzia KeyTool, aby utworzyć magazyn kluczy i zapisać tam certyfikat. Co ciekawe, generowanie kluczy było wcześniej określane za pomocą -genkey, ale teraz zaleca się użycie -genkeypair. Będziemy musieli zdefiniować następujące rzeczy:
- alias : Alias lub po prostu nazwa, pod którą wpis zostanie zapisany w Keystore
keyalg : Algorytm szyfrowania klucza. Wybierzmy algorytm RSA, który jest w zasadzie standardowym rozwiązaniem dla naszych celów.
- keysize : Rozmiar klucza (w bitach). Minimalny zalecany rozmiar to 2048, ponieważ... mniejszy rozmiar został już pęknięty. Więcej możesz przeczytać tutaj: „ certyfikat ssl w 2048 bitach ”.
- dname : Nazwa wyróżniająca, nazwa wyróżniająca.
Ważne jest, aby zrozumieć, że żądany zasób (na przykład https://localhost) zostanie z nim porównany. Nazywa się to „dopasowywaniem podmiotu”.
- Ważność : Czas w dniach ważności wygenerowanego certyfikatu, tj. ważny.
- ext : Rozszerzenie certyfikatu określone w „ Nazwanych rozszerzeniach ”.
W przypadku certyfikatów z podpisem własnym (tj. certyfikatów tworzonych niezależnie) należy określić następujące rozszerzenia:
- -ext san:critical=dns:localhost,ip:127.0.0.1 >, aby wykonać dopasowanie tematu według alternatywnej nazwy podmiotu
- -ext bc=ca:false >, aby wskazać, że ten certyfikat nie jest używany do podpisywania innych certyfikatów
Uruchommy polecenie (przykład dla systemu operacyjnego Windows):
keytool -genkeypair -alias ssl -keyalg RSA -keysize 2048 -dname "CN=localhost,OU=IT,O=Javarush,L=SaintPetersburg,C=RU,email=contact@email.com" -validity 90 -keystore C:/keystore.jks -storepass passw0rd -keypass passw0rd -ext san:critical=dns:localhost,ip:127.0.0.1 -ext bc=ca:false
Ponieważ plik zostanie utworzony, upewnij się, że masz wszystkie uprawnienia do utworzenia pliku. Prawdopodobnie zobaczysz także takie porady:
Tutaj powiedziano nam, że JKS jest formatem zastrzeżonym. Zastrzeżony oznacza, że jest prywatną własnością autorów i jest przeznaczony do użytku wyłącznie w Javie. Podczas pracy z narzędziami stron trzecich może pojawić się konflikt, dlatego jesteśmy ostrzegani. Dodatkowo może pojawić się błąd:
The destination pkcs12 keystore has different storepass and keypass
. Ten błąd występuje, ponieważ hasła do wpisu w magazynie kluczy i do samego magazynu kluczy są różne. Jak mówi dokumentacja
narzędzia keytool : „Na przykład większość narzędzi innych firm wymaga, aby hasło magazynu i hasło w magazynie kluczy PKCS #12 były takie same”. Klucz możemy określić sami (na przykład -destkeypass EntryPasw). Ale lepiej nie naruszać wymagań i ustawiać to samo hasło. Zatem import może wyglądać następująco:
keytool -importkeystore -srckeystore C:/keystore.jks -destkeystore C:/keystore.jks -deststoretype pkcs12
Przykład sukcesu:
Aby wyeksportować certyfikat do pliku, możesz uruchomić:
keytool -export -alias ssl -storepass passw0rd -file C:/server.cer -keystore C:/keystore.jks
Dodatkowo możemy uzyskać zawartość magazynu kluczy w następujący sposób:
keytool -list -v -keystore C:/keystore.jks -storepass passw0rd
Świetnie, teraz mamy magazyn kluczy zawierający certyfikat. Teraz możesz pobrać go z kodu:
public KeyStore getKeyStore() {
try(FileInputStream fis = new FileInputStream("C:/keystore.jks")){
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(fis, "passw0rd".toCharArray());
return keyStore;
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
throw new IllegalStateException(e);
}
}
Jeśli istnieje KeyStore, możemy zainicjować KeyManager:
public KeyManager[] getKeyManagers(KeyStore keyStore) {
String keyManagerAlgo = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyManagerFactory = null;
try {
keyManagerFactory = KeyManagerFactory.getInstance(keyManagerAlgo);
keyManagerFactory.init(keyStore, "passw0rd".toCharArray());
return keyManagerFactory.getKeyManagers();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
} catch (UnrecoverableKeyException | KeyStoreException e) {
throw new IllegalStateException(e);
}
}
Nasz pierwszy cel został osiągnięty. Pozostaje dowiedzieć się, czym jest TrustManager. TrustManager jest opisany w dokumentacji JSSE w sekcji „
Interfejs TrustManager ”. Jest bardzo podobny do KeyManagera, ale jego celem jest sprawdzenie, czy osobie żądającej połączenia można zaufać. Mówiąc wprost, jest to KeyManager w odwrotnej kolejności =) Nie potrzebujemy TrustManagera, więc przekażemy wartość null. Zostanie wówczas utworzony domyślny TrustManager, który nie weryfikuje użytkownika końcowego wysyłającego żądania do naszego serwera. Dokumentacja tak mówi: „zostanie zastosowana domyślna implementacja”. To samo z SecureRandom. Jeśli określimy null, zostanie użyta domyślna implementacja. Pamiętajmy tylko, że SecureRandom jest klasą JCA i jest opisana w dokumentacji JCA w rozdziale „
Klasa SecureRandom ”. W sumie przygotowanie uwzględniające wszystkie opisane powyżej metody może wyglądać następująco:
public static void main(String[] args) {
App app = new App();
SSLContext sslContext = app.getSSLContext();
KeyStore keyStore = app.getKeyStore();
KeyManager[] keyManagers = app.getKeyManagers(keyStore);
try {
sslContext.init(keyManagers, null, null);
} catch (KeyManagementException e) {
throw new IllegalStateException(e);
}
Pozostaje tylko uruchomić serwer:
int httpsPort = 443;
Undertow server = Undertow.builder()
.addHttpsListener(httpsPort, "localhost", sslContext)
.setHandler(new HttpHandler() {
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
exchange.getResponseSender().send("Hello World");
}
}).build();
server.start();
}
Tym razem nasz serwer będzie dostępny pod adresem
https://localhost:443
Jednak nadal otrzymamy błąd, że temu zasobowi nie można ufać:
Zastanówmy się, co jest nie tak z certyfikatem i co z tym zrobić.
Zarządzanie certyfikatami
Zatem nasz serwer jest już gotowy do pracy poprzez HTTPS, ale klient mu nie ufa. Dlaczego? Przyjrzyjmy się:
Powodem jest to, że ten certyfikat jest certyfikatem z podpisem własnym. Certyfikat SSL z podpisem własnym oznacza certyfikat klucza publicznego wydany i podpisany przez tę samą osobę, którą identyfikuje. Oznacza to, że nie został wydany przez żaden szanowany urząd certyfikacji (CA, znany również jako urząd certyfikacji). Urząd Certyfikacji pełni funkcję powiernika i na co dzień przypomina notariusza. Zapewnia, że wystawiane przez niego certyfikaty są rzetelne. Usługa wydawania certyfikatów przez takie urzędy certyfikacji jest płatna, więc nikt nie potrzebuje utraty zaufania i ryzyka reputacji. Domyślnie istnieje kilka zaufanych urzędów certyfikacji. Ta lista jest edytowalna. Każdy system operacyjny ma własne zarządzanie listą urzędów certyfikacji. Na przykład o zarządzaniu tą listą w systemie Windows można przeczytać tutaj: „
Zarządzaj zaufanymi certyfikatami głównymi w systemie Windows ”. Dodajmy certyfikat do zaufanych, jak wskazano w komunikacie o błędzie. Aby to zrobić, najpierw pobierz certyfikat:
W systemie operacyjnym Windows naciśnij Win + R i wykonaj,
mmc
aby wywołać konsolę sterowania. Następnie naciśnij Ctrl+M, aby dodać sekcję „Certyfikaty” do bieżącej konsoli. Następnie w podsekcji „Zaufane główne urzędy certyfikacji” wykonamy
Действия / Все задачи / Импорт
. Zaimportujmy wcześniej pobrany plik do pliku. Przeglądarka mogła zapamiętać poprzedni stan zaufania certyfikatu. Dlatego przed otwarciem strony należy ponownie uruchomić przeglądarkę. Na przykład w przeglądarce Google Chrome na pasku adresu musisz uruchomić
chrome://restart
. W systemie operacyjnym Windows można także użyć narzędzia do przeglądania certyfikatów
certmgr.msc
:
Jeśli wszystko zrobiliśmy poprawnie, zobaczymy pomyślne wywołanie naszego serwera poprzez HTTPS:
Jak widać, certyfikat jest teraz uważany za ważny, zasób jest dostępny i nie ma żadnych błędów.
Konkluzja
Ustaliliśmy więc, jak wygląda schemat włączania protokołu HTTPS na serwerze internetowym i co jest do tego potrzebne. Mam nadzieję, że w tym miejscu jest jasne, że wsparcie zapewnia interakcja Java Cryptography Architecture (JCA), która jest odpowiedzialna za kryptografię, i Java Secure Socket Extension (JSSE), które zapewnia implementację TLS po stronie Java. Widzieliśmy, jak narzędzie keytool zawarte w pakiecie JDK jest używane do pracy z magazynem kluczy i certyfikatów KeyStore. Dodatkowo zdaliśmy sobie sprawę, że HTTPS wykorzystuje protokoły SSL/TLS ze względów bezpieczeństwa. Aby to wzmocnić, radzę przeczytać doskonałe artykuły na ten temat:
Mamy nadzieję, że po tej małej recenzji protokół HTTPS stanie się nieco bardziej przejrzysty. A jeśli chcesz włączyć protokół HTTPS, możesz łatwo zrozumieć warunki z dokumentacji serwerów aplikacji i struktur. #Wiaczesław
GO TO FULL VERSION