Wstęp
Bezpieczeństwo jest jednym z najważniejszych aspektów aplikacji i zawsze musimy poświęcać czas i zasoby, aby zapewnić bezpieczeństwo w najlepszy możliwy sposób.
W aplikacji REST tradycyjne sposoby uwierzytelniania i zabezpieczania mogą nie być najlepszym sposobem, dlatego w tym samouczku pokażemy, jak wykonać najczęstszą część uwierzytelniania w aplikacjach REST, czyli uwierzytelnianie oparte na tokenach. aplikacja będzie zbudowana w Javie od początku i szczegółowo opisana będzie każdy krok przeprowadzenia uwierzytelnienia.
Tutorial skupi się wyłącznie na uwierzytelnianiu, dlatego zakładam, że czytelnik ma już wcześniejszą wiedzę na temat działania aplikacji REST przy użyciu Jersey, a także wiedzę na temat protokołu HTTP.
Jeśli czytelnik nie jest zaznajomiony ze wzorcem projektowym REST, oto kilka linków, które mogą pomóc w zrozumieniu tego tematu:
https://www.w3.org/2001/sw/wiki/RESTb(Język angielski)
http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm(Język angielski)
https://becode.com.br/o-que-e-api-rest-e-restful/(Portugalski)
Zalety uwierzytelniania opartego na tokenach?
Jak widać w tym znakomitym artykuleTajniki uwierzytelniania opartego na tokenachPokazano zalety stosowania tokena jako formy uwierzytelnienia, możemy je podsumować w następujących zaletach:
Stabilność:
Sposób zapisywania tokena leży po stronie klienta i możemy umieścić informacje o kliencie w samym tokenie, oszczędzając w ten sposób serwerowi jego przechowywanie.
Bezpieczeństwo:
Z każdym żądaniem wysyłany jest token, a nie plik cookie, i nawet jeśli implementacja po stronie klienta korzysta z pliku cookie, będzie on używany tylko do przechowywania tokenu, a nie sesji, którą można manipulować i zapobiegać atakom CSRF.
Jak działa uwierzytelnianie oparte na tokenach
W przypadku uwierzytelniania tokenowego klient wymienia swoje dane uwierzytelniające (np. login i hasło) na token, po czym zamiast wysyłać dane uwierzytelniające przy każdym żądaniu, klient po prostu wysyła token w celu uwierzytelnienia i autoryzacji.
Podsumowując, są to kroki uwierzytelniania tokena:
1. Klient wysyła dane uwierzytelniające do serwera.
2. Serwer uwierzytelnia poświadczenia i generuje token.
3. Serwer wysyła token do klienta.
4. Klient zapisuje ten token i wysyła go w nagłówku w każdym żądaniu
6. Serwer przy każdym żądaniu pobiera token i sprawdza czy token jest ważny czy nie
- Jeśli token jest ważny, serwer akceptuje żądanie
- Jeśli token jest nieprawidłowy, serwer odrzuca żądanie
7. Serwer może posiadać punkt końcowy odnawiający token
Aplikacja
Aplikacja będzie prostym przelicznikiem odległości w milach na kilometry i odwrotnie.
Aplikacja ma tylko 3 punkty końcowe:
- Punkt końcowy odpowiedzialny za uwierzytelnianie użytkownika
- Punkt końcowy, który przelicza mile na kilometry
- Punkt końcowy, który przelicza kilometry na mile
Sprawimy też, że konieczne będzie uwierzytelnienie w systemie i posiadanie odpowiedniego poziomu dostępu, aby uzyskać dostęp do dowolnego punktu końcowego.
Co zostanie użyte do zbudowania aplikacji
W części REST wykorzystamy implementację koszulki JAX-RS w wersji 2.0.
Jersey jest jedną z różnych implementacji specyfikacji JAX-RS, a JAX-RS jest z kolei specyfikacją Java do korzystania z usług sieciowych Rest. Więcej informacji można znaleźć pod tym linkiem:http://blog.kolaborativa.com/2013/08/jax-rs/.
Aby otrzymać koszulkę i dowiedzieć się, jak ją zainstalować w swoim projekcie, kliknij ten link:https://github.com/jersey
Użyjmy biblioteki GSON Google, aby wykonać część konwersacji z JSON na obiekt Java i Vise i odwrotnie.
Aby uzyskać GSON i dowiedzieć się, jak zainstalować go w swoim projekcie, kliknij ten link:https://github.com/google/gson
Aby przetestować wywołania punktu końcowego naszej aplikacji, użyjemy Postmana.
Można go uzyskać pod następującym linkiem:https://www.getpostman.com/
Tokenem, którego będziemy używać, będzie JWT (JSON Web Token), aby ułatwić rozwój, użyjemy biblioteki o nazwie JJWT, która pomoże wygenerować i zweryfikować token.
Bibliotekę JJWT można uzyskać pod następującym linkiem:https://github.com/jwtk/jjwt
Co to jest JWT?
JWT (JSON Web Token) jest zdefiniowany na oficjalnej stronie internetowej w następujący sposóbhttp://jwt.io: „JWT to otwarty standard, który definiuje kompaktowy, niezależny sposób bezpiecznego przesyłania informacji między dwiema stronami jako obiekt JSON”.
JWT jest podzielony na trzy części oddzielone znakiem „.” te trzy części to nagłówek, ładunek i podpis
nagłówek
Nagłówek jest pierwszą częścią JWT i jest podzielony na dwie części, algorytm kodowania i typ tokena, a te dwie części są kodowane w Base64, wyglądałoby to tak:
{“alg”:„HS256”„typ”:„JWT”}
Właściwość „alg” definiuje algorytm tokena, którym w tym przypadku jest HMAC SHA256, a właściwość „typ” określa typ tokena, czyli JWT.
Po znalezieniu w Base64 nagłówek wygląda następująco:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Ładunek
Ładunek to informacja, która zostanie wysłana, na przykład możemy wysłać nazwę użytkownika i właściwość mówiącą, czy jest on administratorem, czy nie, wyglądałoby to tak:
{"nazwa„:”fullano”,„admin” :PRAWDA}
Po znalezieniu w Base64 ładunek będzie wyglądał następująco:
eyJub21lIjoiRnVsYW5viIiwiYWRtaW4iOnRydWV9
Podpis
Wreszcie mamy podpis, który jest nagłówkiem i ładunkiem zakodowanym za pomocą algorytmu nagłówka wraz z tajnym słowem, które jest używane do kodowania i nie powinno być nikomu udostępniane.
Po znalezieniu w Base64 wyglądałoby to tak:
IShPdPgMqjygLcv6FpePbFuRLJHBTdeKSNDQIpR-X2E
Zatem nasz kompletny token wygląda następująco:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub21lIjoiRnVsYW5vIiwiYWRtaW4iOnRydWV9.ISHPdPgMqjygLcv6FpePbFuRLJHBTdeKSNDQIpR-X2E
Budowanie aplikacji
Po utworzeniu projektu i uwzględnieniu w projekcie zależności jersey, JJWT i Gson, musimy zacząć od utworzenia pakietu i klasy zawierającej punkty końcowe konwersji pomiarów
Najpierw stworzyliśmy pakiet br.com.projetoRest.service i w ramach tego pakietu utworzyliśmy klasę ConversorMedidasService.java.
Po utworzeniu klasy musimy umieścić adnotację @Path na górze definicji klasy, która konfiguruje adres URL punktu końcowego. Powinno to wyglądać tak:
br.com.projetoRest.services.ConversorMedidasService.java;
@Ścieżka("usługi")publiczny klasa Usługa ConversorMedidas {}
Stwórzmy teraz metody, które będą punktami końcowymi konwersji odległości, jedna będzie z kilometrów na mile, a druga z mil na kilometry
Deklaracja metody kilometrów na mile wygląda następująco:
br.com.projetoRest.services.ConversorMedidasService.java
//Określ, który czasownik http będzie używany do wywołania tej metody@DOSTAWAĆ// Zdefiniuj adres URL umożliwiający dostęp do metody, gdzie {kilometers} jest parametrem oznaczającym liczbę kilometrów, które zostaną przeliczone na mile@Ścieżka(„kilometry na mile/{kilometry}”)//Metoda wykonująca prostą konwersję z kilometrów na milepublicznyKilometr odpowiedziToMile(@PathParam(„kilometry”)Podwójne kilometry){kilometry = kilometry /1.6;powrótOdpowiedź.ok(kilometry).build();}
Jak widzimy, istnieje adnotacja @PathParam, która pobiera parametr url {kilometers} i umieszcza go w zmiennej Double kilometr.
Metoda dokonuje prostego podziału w celu ustalenia liczby kilometrów i zwraca odpowiedź ze statusem 200 (OK) i wartością konwersji w treści odpowiedzi.
@Path(“kilometrosParaMilhas/{kilometros}”) definiuje adres URL, pod który uzyskamy dostęp, aby uzyskać wynik konwersacji. {Kilometry} przypomina symbol wieloznaczny, którego używamy do wskazania, że będzie to zmienna, na przykład aby dowiedzieć się, ile mil z 3 kilometrów musimy uzyskać dostęp do następującego adresu URL:
http://localhost:8080/ProjetoRest/servicos/quilometrosParaMilhas/3
Teraz metoda mil na kilometry:
br.com.projetoRest.services.ConversorMedidasService.java
@DOSTAWAĆ@Ścieżka(„mileToKilometry/{miles}”)//Metoda umożliwiająca prostą konwersję mil na kilometrypublicznymile odpowiedzi na kilometry (@PathParam(„mile”)Podwójne mile){mile = mile *1.6;powrótOdpowiedź.ok(mile).build();}
Metoda ta nie różni się zbytnio od metody poprzedniej, działanie jest takie samo, z tą różnicą, że zamiast dzielenia metoda wykonuje mnożenie.
Zatem klasa ConverterMedidasService powinna wyglądać następująco:
br.com.projetoRest.services.ConversorMedidasService.java
pakietbr.com.projetoRest.services;importjavax.ws.rs.GET;importjavax.ws.rs.Ścieżka;importjavax.ws.rs.PathParam;importjavax.ws.rs.core.Response;importbr.com.projetoRest.seguranca.Seguro;@Ścieżka("usługi")publiczny klasa Usługa ConversorMedidas {@DOSTAWAĆ@Ścieżka(„kilometry na mile/{kilometry}”)//Metoda wykonująca prostą konwersję z kilometrów na milepublicznyOdpowiedźkilometr do mili(@PathParam(„kilometry”)Podwójne quilometry){kilometry = kilometry /1.6;powrótOdpowiedź.ok(kilometry).build();}@DOSTAWAĆ@Ścieżka(„mileToKilometry/{miles}”)//Metoda umożliwiająca prostą konwersję mil na kilometrypublicznyOdpowiedźmile na kilometry(@PathParam(„mile”)Podwójne milhy){mile = mile *1.6;powrótOdpowiedź.ok(mile).build();}}
Teraz musimy skonfigurować plik web.xml, aby można było używać koszulki
plik web.xml wygląda następująco:
/ProjetoRest/WebContent/WEB-INF/web.xml
?xml wersja="1.0" kodowanie="UTF-8"?><Aplikacja internetowa xmlns:xsi=„http://www.w3.org/2001/XMLSchema-instance” xmlns=„http://xmlns.jcp.org/xml/ns/javaee” xsi:schematLokalizacja=„http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd” ID=„IDAplikacji internetowej” wersja=„3,1”><wyświetlana nazwa>ProjectoRestwyświetlana nazwa><serwlet><nazwa-serwletu>br.com.projetoRestnazwa-serwletu><klasa serwletu>org.glassfish.jersey.servlet.ServletContainerklasa serwletu>Tutaj definiujesz, który pakiet będzie zawierał punkty końcowe projektu.<parametr cieplny><nazwa-parametru>pakiety. Jersey.config.server.provider.packagesnazwa-parametru><wartość-parametru>br.com.projetoRestwartość-parametru>parametr cieplny><parametr cieplny><nazwa-parametru>jersey.config.server.tracingnazwa-parametru><wartość-parametru>WSZYSTKOwartość-parametru>parametr cieplny><ładowanie przy uruchomieniu>1ładowanie przy uruchomieniu>serwlet>Tutaj definiujesz adres URL, którego Jesery będzie słuchać w celu zarządzania połączeniami<mapowanie serwletów><nazwa-serwletu>br.com.projetoRestnazwa-serwletu><wzór adresu URL>/*wzór adresu URL>mapowanie serwletów>Aplikacja internetowa>
Po uruchomieniu serwera aplikacji możemy otworzyć listonosza i wykonać wywołanie GET na adres:
http://localhost:8080/ProjetoRest/servicos/milhasParaQuilometros/1
Wynik musi być następujący:
Jak widać, odpowiedź brzmiała 1,6, czyli liczba kilometrów na milę. A status odpowiedzi 200 OK.
Zaloguj się i uruchom JWT
Stwórzmy w systemie punkt końcowy logowania, który sprawdzi dane uwierzytelniające użytkownika, jeśli są prawidłowe, zostanie wygenerowany JWT i ten token zostanie zwrócony klientowi, jeśli nie jest ważny, zwrócona zostanie odpowiedź ze statusem 401 NIEAUTORYZOWANY
Na początek utwórzmy klasę Credential w pakiecie br.com.projetoRest.model, będzie to prosta klasa reprezentująca poświadczenia użytkownika i będzie miała tylko dwa pola, login i hasło oraz ich moduły pobierające i ustawiające, klasa będzie wyglądać lubię to:
br.com.projetoRest.model.Credencial.java
pakietbr.com.projetoRest.model;publiczny klasa Poświadczenie {prywatnyLogowanie ciągowe;prywatnyHasło ciągu;//Getters i Setters pominięte}
Stwórzmy teraz klasę LoginService w pakiecie br.com.projetoRest.services.
Po utworzeniu klasy zdefiniujemy @PATH klasy, która będzie „/login” i będzie wyglądać następująco:
br.com.projetoRest.services.LoginService.java
@Ścieżka("/Zaloguj sie")publiczny klasa Usługa logowania {}
Stwórzmy teraz metodę POST, która będzie odpowiedzialna za logowanie. Metoda będzie wyglądać następująco:
br.com.projetoRest.services.LoginService.java
//Metoda POST sprawdzająca poświadczenia przesłane w żądaniu//i jeśli jest prawidłowy, zwraca token klientowi@POST// Zdefiniuj, że metoda oczekuje obiektu typu json w treści żądania@Konsumuje(MediaType.APPLICATION_JSON)publicznyOdpowiedź fazerLogin(String credenciaisJson){próbować{// Tworzy instancję obiektu Gson, który będzie odpowiedzialny za przekształcenie treści żądania znajdującej się w zmiennej CredentialJson w obiekt Java CredentialGson Gson =nowyGson();//tutaj obiekt gson przekształca poświadczenia Json w zmienną Credential typu CredentialDane uwierzytelniające = gson.fromJson(credentialsJson, Credential.klasa);//Sprawdza, czy dane uwierzytelniające są prawidłowe. Jeśli nie, zgłosi wyjątekvalidCredentials(poświadczenia);//Jeśli dane uwierzytelniające wygenerują token i liczba dni, przez które token będzie ważny, w tym przypadku 1 dzieńToken ciągu = gerarToken(credencial.getLogin(),1);//Zwraca odpowiedź o statusie 200 OK z wygenerowanym tokenempowrótOdpowiedź.ok(token).build();}złapać(Wyjątek e) {e.printStackTrace();//Jeśli wystąpi błąd, zwraca odpowiedź ze statusem 401 NIEAUTORYZOWANYpowrótOdpowiedź.status(Status.NIEAUTORYZOWANY).build();}}
Metoda sprawdzania poświadczeń wygląda następująco:
br.com.projetoRest.services.LoginService.java
prywatny próżnia walidacjaCrendenciais(Poświadczenie uwierzytelniające) rzucaWyjątek{próbować{Jeśli(!credential.getLogin().equals("test") || !crendencial.getSenha().equals(„123”))rzucić nowyWyjątek(„Poświadczenia są nieważne!”);}złapać(Wyjątek e) {rzucićmi;}}
Ta metoda polega na przeszukiwaniu bazy danych w celu sprawdzenia, czy istnieje użytkownik z tym loginem i hasłem, ale ponieważ jest to prosta aplikacja bez bazy danych, zrobiłem to tylko wtedy, gdy login jest inny niż „testowy” lub hasło inne niż „123” daje wyjątek, tutaj możesz wprowadzić metodę uwierzytelniania, z której korzysta Twoja aplikacja. Metoda generująca token wygląda następująco:
br.com.projetoRest.services.LoginService.java
prywatnyStrunowywygenerujToken(Strunowylogin,liczba całkowita expiraEmDias ){//Określ jaki będzie algorytm podpisu, w tym przypadku będzie to HMAC SHA512SignatureAlgorithm algoritimoAssinatura = SignatureAlgorithm.HS512;//Bieżąca data wygenerowania tokenaDatateraz =nowy Data();//Definiuje datę tokena na podstawie liczby dni, jakie upłynęły od parametru expEmDiasWygaśnięcie kalendarza = Calendar.getInstance();expira.add(Calendar.DAY_OF_MONTH, expiraEmDias);//Zakoduj tajną frazę do base64, która będzie używana podczas generowania tokenabajt[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(FRASE_SEGREDO);SecretKeySpec klucz =nowySecretKeySpec(apiKeySecretBytes, algoritimoAssinatura.getJcaName());//Na koniec użyj konstruktora JWT, aby wygenerować tokenKonstruktor JwtBuilder = Jwts.builder().setIssuedAt(teraz)//Data wygenerowania tokenu.setIssuer(logowanie)//Wstaw login użytkownika i inne informacje.signWith(algoritimoAssinatura, klucz)//umieść algorytm podpisu i hasło już zakodowane.setExpiration(expira.getTime());// wpisz do jakiego dnia token jest ważnypowrótkonstruktor.compact();//Konstruuje token zwracający go jako ciąg znaków}
W tej metodzie wykorzystujemy zasoby biblioteki JJWT do wygenerowania tokenu, który w swoim ładunku będzie zawierał login użytkownika, a który wygaśnie po upływie określonej liczby dni.
Musimy sprawdzić, czy ta funkcja logowania działa zgodnie z oczekiwaniami, w tym celu zwracamy się do listonosza, wysyłając POST pod następujący adres URL:http://localhost:8080/ProjetoRest/login, przekazując następujący obiekt danych uwierzytelniających w formacie JSON w treści żądania:
{
"Zaloguj sie":"test","hasło":„123”}
Wynik musi być następujący:
Jak widzimy, efektem była odpowiedź ze statusem 200 OK i wygenerowanym tokenem w treści odpowiedzi.
Zróbmy test przekazując nieprawidłowe dane uwierzytelniające, aby sprawdzić, czy aplikacja rzeczywiście nie zwróci tokena.Wyślijmy teraz następujący obiekt danych uwierzytelniających w formacie JSON:
{"Zaloguj sie":"test","hasło":„1234”}
Wynik jest następujący:
W rezultacie otrzymujemy odpowiedź ze statusem 401 UNAUTHORIZED, bez tokena, ale standardową odpowiedź na błąd.
Uwierzytelnianie
Mając gotowy punkt końcowy logowania, musimy upewnić się, że możemy chronić nasze punkty końcowe, umożliwiając dostęp tylko tym, którzy posiadają ważny token.
Token musi być wysyłany w naszych żądaniach poprzez standardowy nagłówek protokołu autoryzacji HTTP, więc nagłówek będzie wyglądał następująco: Autoryzacja: Bearer
JAX-RS zawiera @NameBinding, który jest metaadnotacją używaną do tworzenia adnotacji wiążącej nazwę dla filtrów i przechwytywaczy. Korzystając z niej możemy utworzyć adnotację, która wyodrębni token z nagłówka Authorization i zweryfikuje wyodrębniony token, a my podaj nazwę @Seguro.
Najpierw utwórzmy pakiet br.com.projetoRest.seguranca i utwórzmy klasę Seguro, klasa ta będzie wyglądać następująco:
br.com.projetoRest.seguranca.Seguro.java
pakiet br.com.projetoRest.seguranca;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import javax.ws. rs.NameBinding;import br.com.projetoRest.model.NivelPermissao;@Wiązanie nazw@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE,ElementType.METHOD})public @interfejsBezpieczna {}
Adnotacja @Seguro zostanie użyta do dekoracji klasy filtra o nazwie FiltroAutenticacao, która implementuje klasę ContainerRequestContext umożliwiającą manipulowanie żądaniem, a tym samym uzyskanie nagłówka Authorization oraz wyodrębnienie tokenu i jego walidację.
Aby zaimplementować filtr, utworzymy klasę FiltroAutenticacao w pakiecie br.com.projetoRest.seguranca.
Klasa będzie wyglądać następująco:
br.com.projetoRest.seguranca.FiltroAutenticacao.ja
pakietbr.com.projetoRest.seguranca;importWyjątek java.io.IO;importjava.security.Principal;importjavax.annotation.Priority;importjavax.ws.rs.NotAuthorizedException;importjavax.ws.rs.Priorytety;importjavax.ws.rs.container.ContainerRequestContext;importjavax.ws.rs.container.ContainerRequestFilter;importjavax.ws.rs.core.HttpHeaders;importjavax.ws.rs.core.Response;importjavax.ws.rs.core.SecurityContext;importDostawca javax.ws.rs.ext.;importbr.com.projetoRest.services.LoginService;importio.jsonwebtoken.Claims;// Zdefiniuj @seguro, które będzie używać tej klasy@Bezpieczna//Wskazuje, że ta klasa będzie zapewniać funkcjonalność @seguro, a nie licznika@Dostawca//I priorytet wykonania, ponieważ możemy mieć inne klasy filtrów//które należy wykonać w określonej kolejności@Priorytet(Priorytety.AUTENTYKACJA)publiczny klasa FiltrujAuthenticacao przybory Filtr żądania kontenera{//Tutaj nadpisujemy metodę filter, która ma jako parametr// ContainerRequestContext będący obiektem, którym możemy manipulować żądaniem@Nadpisaniepubliczny próżnia filtr(ContainerRequestContext requestContext) rzucaWyjątek IO{//Sprawdza, czy nagłówek AUTHORIZATION istnieje, jeśli istnieje, wyodrębnia token//jeśli nie, przerwij żądanie, zwracając wyjątek NotAuthorizedExceptionStringauthorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);Jeśli(nagłówek autoryzacji ==zero|| !authorizationHeader.startsWith(„Nosiciel”)) {rzucić nowyNotAuthorizedException („Dokładny nagłówek autoryzacji”);}//wyodrębnij token z nagłówkaToken ciągu = nagłówek autoryzacji.substring("Okaziciel".długość()).przytnij();//sprawdź, czy metoda jest poprawna, czy nie//jeśli jest niepoprawny, żądanie zostaje przerwane i zwraca odpowiedź ze statusem 401 NIEAUTORYZOWANY//jeśli jest prawidłowy, modyfikujemy SecurityContext żądania//aby użycie metody getUserPrincipal zwracało login użytkownikapróbować{// metoda sprawdzająca, czy token jest ważny, czy nieRoszczenia, roszczenia =nowyLoginService().validaToken(token);//Jeśli nie jest prawidłowy, zwróci obiekt o wartości null i zgłosi wyjątekJeśli(twierdzenia==zero)rzucić nowyWyjątek("Nieprawidłowy Token");//Metoda modyfikująca SecurityContext w celu umożliwienia logowania użytkownikamodificarRequestContext(requestContext,claims.getId());}złapać(Wyjątek e) {e.printStackTrace();//Jeśli token jest nieprawidłowy, żądanie zostaje przerwane i zwraca odpowiedź ze statusem 401 NIEAUTORYZOWANYrequestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());}}//Metoda modyfikująca SecurityContextprywatny próżnia modificarRequestContext(ContainerRequestContext requestContext, String login){finałSecurityContext currentSecurityContext = requestContext.getSecurityContext();requestContext.setSecurityContext(nowySecurityContext() {@Nadpisanie publicznyGłównypobierzUserPrincipal() {powrót nowyGłówny() {@NadpisaniepublicznyStrunowypobierzNazwę() {powrótZaloguj sie;}};}@Nadpisaniepubliczny wartość logiczna isUserInRole(rola smyczkowa) {powrót PRAWDA;}@Nadpisaniepubliczny wartość logiczna jest bezpieczny() {powrótcurrentSecurityContext.isSecure();}@NadpisaniepublicznyStrunowygetAuthenticationScheme() {powrót "Okaziciel";}});}}
W klasie LoginService musimy zastosować metodę validToken, która zwaliduje tokeny JWT, biblioteka JJWT udostępnia już pewną funkcjonalność ułatwiającą ten proces, metoda wygląda następująco:
br.com.projetoRest.services.LoginService.java
publicznyRoszczeniavalidaToken(Token ciągu){próbować{//JJWT sprawdzi token, jeśli token jest nieprawidłowy, wykona wyjątek//JJWT używa tajnej frazy do dekodowania tokena, dzięki czemu jest to możliwe//pobierz informacje, które umieściliśmy w ładunkuOświadczenia = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(FRASE_SEGREDO)).parseClaimsJws(token).getBody();//Oto przykład, jeśli token jest ważny i zdekodowany //wydrukuje login, który został wprowadzony w tokenieSystem.na zewnątrz.println(oświadczenia.getIssuer());powrótroszczenia;}złapać(Wyjątek np.){rzucićbyły;}}
Dzięki walidacji tokena możemy użyć @Seguro w metodach, które chcemy udostępnić tylko uwierzytelnionym użytkownikom. Weźmy na przykład metodę przeliczania kilometrów na mile z ConversorMedidasService. Zostaje tak:
br.com.projetoRest.service.ConversorMedidasService.java
@Bezpieczna@DOSTAWAĆ@Ścieżka(„kilometry na mile/{kilometry}”)//Metoda wykonująca prostą konwersję z kilometrów na milepublicznyKilometr odpowiedziToMile(@PathParam(„kilometry”)Podwójne kilometry){kilometry = kilometry /1.6;powrótOdpowiedź.ok(kilometry).build();}
Jedyna różnica polega na tym, że metodę zdobi adnotacja @Seguro.Aby sprawdzić, czy uwierzytelnianie działa, wywołajmy metodę poprzez PostMan. Wynik jest następujący:
Otrzymaliśmy odpowiedź 401 Unauthorized, która jest prawidłowa, ponieważ nie przekazaliśmy do aplikacji żadnego tokena w celu sprawdzenia, teraz przekażemy token, który otrzymaliśmy przez punkt końcowy logowania i umieścimy token w nagłówku Authorization podczas wykonywania wywołania aplikacja.
Pamiętając, że nagłówek Authorization musi zaczynać się od Bearer, a następnie tokena. Wynik wygląda następująco:
Rezultatem była odpowiedź z wartością konwersji i statusem 200 OK wskazującym, że wszystko poszło dobrze z żądaniem.
Rozważania
Metody, które nie są ozdobione @Seguro, nie będą chronione i będą dostępne dla każdego żądania, niezależnie od tego, czy ma ono ważny token, czy nie.
Klasy można również ozdobić adnotacją @Seguro, dzięki czemu wszystkie ich metody będą chronione bez konieczności umieszczania adnotacji na każdej z nich.
Wdrażaj autoryzację według poziomu dostępu
W JAX-RS dopuszczamy, aby metody miały różne poziomy dostępu, więc użytkownik oprócz posiadania ważnego tokena musi także posiadać niezbędny poziom uprawnień do metody, którą chce wykonać.
Aby rozpocząć wdrażanie tego, utwórzmy klasę wyliczeniową NivelPermissao w pakiecie br.com.projetoRest.model, która będzie zawierać poziomy uprawnień aplikacji:
br.com.projetoRest.model.NivelPermissao.java
pakietbr.com.projetoRest.model;publiczny wyliczeniePoziom uprawnień {LEVEL_1,LEVEL_2,LEVEL_3}
Klasa poziomu uprawnień została ukończona, musimy podnieść klasę Safe, aby mogła otrzymywać poziomy uprawnień:
br.com.projetoRest.seguranca.Seguro.java
@Wiązanie nazw@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE,ElementType.METHOD})public @interfejsBezpieczna wartość {LevelPermission[] wartość() domyślna{};}
Dzięki tej zmianie @seguro może otrzymać tablicę z żądanymi poziomami uprawnień.
Przykład: @Seguro({PermissionLevel.NIVEL_1,PermissionLevel.NIVEL_2})
Aby adnotacja @Seguro mogła to kontrolować, utworzymy klasę o nazwie FiltroAutorizacao, która implementuje ContainerRequestFilter, podobnie jak FiltroAutenticacao.
Klasa FiltroAutorizacao przyjmie poziomy uprawnień zdefiniowane w @Seguro znajdującym się w metodzie lub klasie i sprawdzi, czy użytkownik ma ten poziom uprawnień, jeśli tak, żądanie jest kontynuowane normalnie, jeśli nie, zwraca odpowiedź ze statusem 403 ZABRONIONY.
Warto zauważyć, że @Seguro metody zastępuje @Seguro klasy, więc jeśli klasa @Seguro ma poziom dostępu Poziom_1, a metoda ma Poziom_2, metoda będzie dostępna tylko dla użytkowników posiadających Poziom_2.
FiltroAutorizacao będzie zawsze wykonywane po FiltroAutenticacao, jest to definiowane przez @Priority(). Kolejność wykonywania JAX-RS jest zgodna z @Priority.
To powiedziawszy, klasa FiltroAutorizacao wygląda następująco:
br.com.projetoRest.seguranca.FiltroAutorizacao.java
pakietbr.com.projetoRest.seguranca;importWyjątek java.io.IO;importjava.lang.reflect.AnnotatedElement;importmetoda java.lang.reflect.;importjava.util.ArrayList;importjava util Tablice;importjava.util.List;importjavax.annotation.Priority;importjavax.ws.rs.container.ContainerRequestContext;importjavax.ws.rs.container.ContainerRequestFilter;importjavax.ws.rs.container.ResourceInfo;importjavax.ws.rs.core.Context;importjavax.ws.rs.core.Response;importDostawca javax.ws.rs.ext.;importbr.com.projetoRest.model.NivelPermissao;importbr.com.projetoRest.services.LoginService;importjavax.ws.rs.Priorytety;// Zdefiniuj @seguro, które będzie używać tej klasy@Bezpieczna//Wskazuje, że ta klasa zapewni funkcjonalność @seguro, a nie odwrotnie@Dostawca//I priorytet wykonania, ponieważ możemy mieć inne klasy filtrów//które należy wykonać w określonej kolejności//W tym przypadku zostanie ono wykonane po filtrze uwierzytelniającym,//ponieważ priorytet AUTHENTICATION jest wyższy niż AUTHORIZATION@Priorytet(Priorytety. AUTORYZACJA)publiczny klasa Autoryzacja filtra przybory Filtr żądania kontenera {//JAX-RS wstrzykuje ResourceInfo, które będzie zawierać informacje o sprawdzanej metodzie@KontekstprywatnyInformacje o zasobach Informacje o zasobach;@Nadpisaniepubliczny próżnia filtr(ContainerRequestContext requestContext) rzucaWyjątek IO{// Pobierz klasę zawierającą żądany adres URL i wyodrębnij z niej poziomy uprawnieńClass> classe = ResourceInfo.getResourceClass();ListnivelPermissaoClasse = extrairNivelPermissao(classe);// Pobierz metodę zawierającą żądany adres URL i wyodrębnij jej poziomy uprawnieńMetoda metody = ResourceInfo.getResourceMethod();List nivelPermisaoMetodo = extrairNivelPermissao(metodo);próbować{//Jak zmodyfikować kontekst bezpieczeństwa podczas sprawdzania poprawności tokena, abyśmy mogli uzyskać//Logowanie użytkownika, aby sprawdzić, czy posiada on wymagany poziom uprawnień//dla tego punktu końcowegoString login = requestContext.getSecurityContext().getUserPrincipal().getName();// Sprawdza, czy użytkownik ma uprawnienia do wykonania tej metody// Poziomy dostępu metody zastępują poziomy dostępu klasyJeśli(nivelPermisaoMetodo.isEmpty()) {checarPermissoes(nivelPermissaoClasse,login);}w przeciwnym razie{checkPermissions(nivelPermisaoMetodo,login);}}złapać(Wyjątek e) {//Jeśli użytkownik nie ma uprawnień, zostanie podany wyjątek,//e zwraca odpowiedź ze statusem 403 ZABRONIONErequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build());}}//Metoda wyodrębniająca poziomy uprawnień zdefiniowane w @SeguroprywatnyLista ekstraktPoziomPozwolenia(AnnotatedElement adnotatedElement) {Jeśli(element z adnotacją ==zero) {powrót nowyArrayList();}w przeciwnym razie{Ubezpieczenie ubezpieczeniowe = annotatedElement.getAnnotation(Insurance.class);Jeśli(bezpieczny ==zero) {powrót nowyArrayList ();}w przeciwnym razie{LevelPermissao[] poziomyPermitidos = bezpieczna.wartość();powrótArrays.asList(dopuszczalne poziomy);}}}//Sprawdza czy użytkownik ma uprawnienia do wykonania metody, jeżeli w @Seguro nie zdefiniowano poziomu dostępu to każdy będzie mógł ją wykonać o ile posiada ważny tokenprywatny próżnia sprawdź uprawnienia(List nivelPermissaoPermitidos,String login) rzucaWyjątek{próbować{Jeśli(nivelPermissaoPermitidos.isEmpty())powrót;wartość logicznatemPermissao =FAŁSZ;//Sprawdza, jakie poziomy dostępu ma użytkownik.NivelPermissao nivelPermissaoUsuario =nowyLoginService().buscarNivelPermissao(login);Do(NivelPermissao nivelPermissao : nivelPermissaoPermitidos) {Jeśli(nivelPermissao.equals(nivelPermissaoUsuario)){temPermissao =PRAWDA;przerwa;}}Jeśli(!temPozwolenie)rzucić nowyWyjątek(„Klient nie ma poziomu uprawnień dla tej metody”);}złapać(Wyjątek e) {e.printStackTrace();rzucićmi;}}
}
W LoginService istnieje metoda wyszukująca poziomy uprawnień użytkownika, ponieważ skupiamy się na uwierzytelnianiu, będzie to prosta metoda, która zwróci jedynie poziom dostępu niezależny od użytkownika, ale w swojej aplikacji możesz zaimplementować bardziej złożoną metodę, którą sam potrzebować. Metoda wygląda następująco:
br.com.projetoRest.services.LoginService.java
//Prosta metoda, ponieważ nie używamy bazy danych i skupiamy się na części uwierzytelniającej//metoda zwraca tylko jeden poziom dostępu, ale w normalnej aplikacji//tutaj sprawdzamy, jakie poziomy uprawnień posiada użytkownik i zwracamy jepublicznyNivelPermissaoPozwolenie na poziom wyszukiwania(Ciąg logowania) {powrótPermissaoLevel.LEVEL_1;}
Zmodyfikujmy metody klasy ConversorMedidasService kilometroParaMilha i MilesParaQuilometros tak, aby ich @Seguro posiadało poziomy uprawnień w metodzie, którą umieścimy NivelPermissao.Nivel_1, a w metodzie MilesParaQuilometros LevelPermissao.Nivel_2.
Po modyfikacji metody wyglądają następująco:
@Bezpieczna({LevelPermissao.LEVEL_1})@DOSTAWAĆ@Ścieżka(„kilometry na mile/{kilometry}”)//Metoda wykonująca prostą konwersję z kilometrów na milepublicznyKilometr odpowiedziToMile(@PathParam(„kilometry”)Podwójne kilometry){kilometry = kilometry /1.6;powrótOdpowiedź.ok(kilometry).build();}@Bezpieczna({LevelPermissao.LEVEL_2})@DOSTAWAĆ@Ścieżka(„mileToKilometry/{miles}”)//Metoda wykonująca prostą konwersję mil na kilometrypublicznymile odpowiedzi na kilometry (@PathParam(„mile”)Podwójne mile){mile = mile *1.6;powrótOdpowiedź.ok(mile).build();}
Ponieważ metoda wyszukująca poziom uprawnień użytkownika zawsze zwróci NivelPermissao.Nivel_1, metoda kilometroParaMilha zwróci odpowiedź 200 OK z wartością konwersji.
Metoda mileParaQuilometros wymagająca poziomu dostępu LevelPermissao.Nivel_2 zwróci odpowiedź ze statusem 403 ZABRONIONE
Jak pokazano w tych wynikach PostMan:
Kilometr na milę:
Mila na Kilometr:
Uwagi końcowe
W tym samouczku wyjaśnił, jak działa uwierzytelnianie oparte na tokenach i pokazuje krok po kroku, jak przeprowadzić uwierzytelnianie oparte na tokenach w projekcie Java.
Pamiętając, że niezależnie od tego, jaki rodzaj uwierzytelnienia wybierzemy, zawsze musimy skorzystać z połączenia HTTPS, co gwarantuje bezpieczeństwo aplikacji.
Kod źródłowy projektu można pobrać z GitHuba pod tym linkiemhttps://github.com/tarcCar/RestAutenticacaoPorToken
Bibliografia
Tajniki uwierzytelniania opartego na tokenach –https://scotch.io/tutorials/the-ins-and-outs-of-token-based-authentication
Token sieciowy JSON — JWT —https://blog.lucaskatayama.com/posts/2016/03/30/JSON-Web-Token-JWT/#sthash.32ThI6IT.dpbs
Tokeny sieciowe JSON —http://jwt.io
Człowiek w środku -https://en.wikipedia.org/wiki/Man-in-the-middle_attack
Uwierzytelnianie oparte na tokenach dla aplikacji jednostronicowych (SPA) —https://stormpath.com/blog/token-auth-spa
Uwierzytelnianie w oparciu o token - Zabezpieczenie tokena -https://security.stackexchange.com/questions/19676/token-based-authentication-securing-the-token
Odpowiedź użytkownikaCassio Mazzochi Molin-https://stackoverflow.com/a/26778123