Kurs Java

Uwierzytelnienie i autoryzacja

Autentykacja (uwierzytelnianie) i autoryzacja w Springu, mimo że są mechanizmami całkiem dobrze zaprojektowanymi we frameworku, potrafią przysporzyć wielu problemów, szczególnie na początku drogi ze Springiem.
Appa Notka. Celowo użyliśmy słowa autentykacja, ponieważ raz, że staje się ono coraz popularniejsze w branży i powoli wchodzi do kanonu słów branżowych, a dwa, że dzięki takiemu bezpośredniemu tłumaczeniu łatwiej jest zapamiętać różnicę między ang. authentication a authorization. Angielskie "authentication" brzmi zupełnie inaczej niż polskie "uwierzytelnienie" i jest mu zdecydowanie bliżej do polskiego słowa autoryzacja. Na rozmowach kwalifikacyjnych często jest tak, że progamiści potrafią powiedzieć, czym różni się "uwierzytelnianie" od "autoryzacji", ale już zapytani z ang. o to "czym jest authentication?", przytaczają definicję "autoryzacji".

Warto jednak wiedzieć, że wśród znawców językowych można spotkać się z opinią, iż nie jest to poprawna forma (źródło Wikipedia, https://pl.wikipedia.org/wiki/Uwierzytelnianie). W kursie słowa uwierzytelnienie i autentykacja stosujemy zamiennie.
W rozdziale omówimy stworzoną przez nas mini aplikację służącą do logowania użytkownika i autoryzowania jego dostępu do zasobów. Aplikacja zawiera wszystkie typowe elementy security:
  • Klasę konfiguracyjną SecurityConfig.
  • Klasy odpowiedzialne za logowanie i dostęp do roli (User, Role, Privilege).
  • Generowanie hasła metodą bCrypt.
  • Klasy listenerów dla akcji poprawnego i niepoprawnego logowania oraz akcji wylogowania.
  • Klasę UserDetailsService zapewniającą pobranie użytkownika wraz z rolami.
  • Inicjalizacja bazy danymi początkowymi w postaci dwóch userów z różnym zestawem uprawnień.
  • Zezwolenie na uruchomienie konkretnej metody w Springu w zależności od roli (@PreAuthorize, @Secured, @RolesAllowed).
  • Bean sesyjny przechowujący dodatkowe informacje o zalogowanym użytkowniku.
  • Krótki formularz logowania w Thymeleaf.

Projekt security-app

Na początek szybki rzut oka na projekt aplikacji. Na poniższym zdjęciu widzimy zbiór 20 klas i interfejsów, które wykonują wszystkie założone zadania. Oczywiście jest to projekt Spring Boot.
Java Spring Security

Model bezpieczeństwa

Implementując zabepieczenia aplikacji zwykle działamy na dwóch płaszczyznach:
  • Uwierzytelnienie - potwierdzenie tożsamości użytkownika, najczęściej wykonywane za pomocą formularza logowania, w którym użytkownik wpisuje nazwę użytkownika oraz hasło. Czasem występuje również drugi czynnik uwierzytelniający, w postaci maila lub smsa z kodem, który musi być dodatkowo wpisany podczas logowania. Ten sposób jest znany jako Two-Factor Authentication.
  • Autoryzacja - określa prawo dostępu do zasobu, które jest weryfikowane niezależnie lub nawet po wcześniejszym poprawnym uwierzytelnieniu użytkownika w aplikacji. Innymi słowy, możemy zalogować się do aplikacji, ale to nie znaczy, że będziemy posiadać prawa dostępu do wszystkich obiektów zapisanych w bazie lub wszystkich funkcjonalności uruchamianych przez konkretne metody.
W security-app realizujemy obydwa rodzaje zabezpieczeń (z pominięciem Two-Factor Authentication). Uwierzytelnienie jest wykonywana za pomocą formularza z polami username i password. Po naciśnięciu przycisku Sign in dane te są wysyłane na serwer pod postacią url-a z końcówką login.
Spring Java Login
Po stronie backendu działa Spring (konkretnie Spring Boot) uzbrojony dodatkowo w zależność spring-boot-starter-security:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>    
Powoduje to, że nie tylko mamy dostęp do klas i interfejsów Spring Security, ale również otrzymujemy gotowe funkcjonalności upraszczające proces logowania. Nie musimy na przykład sami tworzyć kontrolera do przechwytywania requesta wysłanego na url /login.

Uwierzytelnienie

Spring automatycznie przyjmie request przychodzący na url /login. Po odebraniu żądania zapisane w nim dane (użytkownik oraz hasło) są weryfikowane przez framework z danymi w bazie. Kod weryfikujący dostarczamy sami poprzez zaimplementowanie odpowiedniej metody w klasie CustomUserDetailsService. To tam następuje przeszukanie bazy danych w celu znalezienia pasującego użytkownika i hasła. Mogą tutaj wystąpić dwa scenariusze:
  • Użytkownik i hasło przesłane w requeście pasują do jednego z użytkowników w bazie i wtedy zostaje uruchomiony kod świadczący o sukcesie autentykacji (CustomAuthSuccessHandler).
  • Żaden użytkownik zapisany w bazie danych nie posiada nazwy oraz hasła pasującego do tych z requestu i wówczas zostaje uruchomiony kod obsługujący niepowodzenie autentykacji (CustomAuthFailureHandler).
W obu wspomnianych klasach możemy sami dostarczyć implementację kluczowych metod, czyli to my decydujemy, co chcemy zrobić po poprawnym lub nieudanym logowaniu. Co ważne, klasy te, aby mogły zostać użyte w procesie autentykacji, muszą zostać zarejestrowane. Rejestracji dokonujemy w klasie konfiguracyjnej SecurityConfig. Jest to centralne miejsce naszego modelu bezpieczeństwa. To tam definiujemy istotne dla nas ustawienia, takie jak czas trwania sesji, czy wzorce ścieżek dla requestów, które mają być chronione przed niepowołanym dostępem. Wszystkie te elementy omówimy w kolejnych rozdziałach.

Warto podkreślić, że hasło nie jest przechowywane w aplikacji w otwartej formie, tylko jest kodowane za pomocą konkretnego algorytmu. To jakiego algorytmu użyjemy (np. MD5, czy bcrypt) zależy od nas. Możemy to ustawić w prosty sposób również w klasie SpringSecurity.

Autoryzacja

Dostęp do zasobów ograniczymy za pomocą określenia restrykcji dla metod pozwalających na dostęp do tych zasobów, na przykład uruchomienie metody tylko wtedy, gdy zalogowany użytkownik należy do roli ROLE_API_READ_PRIVILEGE:
Priviliges in Spring Java
Stworzyliśmy taką rolę w celu sprawdzenia, czy użytkownik ma prawo wywoływać metody api. W tym konkretnym przypadku, jeśli użytkownik nie ma takich praw (zapisanych wcześniej w bazie), nie pobierze on listy wszystkich użytkowników.

Wygląda to tak, że nasz kontroler Springa o nazwie UserApi przyjmie request /api/users i spróbuje uruchomić powyższą metodę getUsers. Jeśli użytkownik nie należy do roli ROLE_API_READ_PRIVILEGE, zostanie rzucony wyjątek AccessDeniedException:
org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84)
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation...
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke...
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed...
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed...
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept...
    at com.javappa.securityapp.security.service.UserService$$EnhancerBySpringCGLIB$$89ce66ff.getUsers()
    at com.javappa.securityapp.security.api.UserApi.getUsers(UserApi.java:22)   
oraz nastąpi wygenerowanie odpowiedzi http o kodzie 403 (Forbidden):
Priviliges in Java
W przypadku gdy dodamy użytkownikowi przywilej odczytu danych z api, wykonanie tego requestu powiedzie się i serwer zwróci w odpowiedzi listę użytkowników:
Spring privileged users

Model danych

Na koniec przyjrzyjmy się jeszcze modelowi danych, jaki jest typowy dla większości aplikacji. Mamy trzy podmioty reprezentowane przez trzy dedykowane klasy:
  • User - dane użytkownika, w tym nazwa i hasło służące do logowania
  • Role - role, na przykład ROLE_ADMIN oraz ROLE_USER (takie role dodaliśmy do security-app)
  • Privilege - przywileje, na przykład ROLE_READ_PRIVILEGE, ROLE_WRITE_PRIVILEGE oraz ROLE_API_READ_PRIVILEGE (takie przywileje dodaliśmy do security-app), które są przypięte do roli, przez co użytkownik należący do tej roli automatycznie posiada wszystkie przywileje związane z rolą.
W security-app wygląda to tak:
Spring privileged users with roles
Dzięki takiemu trójwarstwowemu modelowi możemy łatwo określać restrykcje dla metod lub klas. Na przykład, jeśli zależy nam by dostęp do danej metody miała tylko rola ROLE_ADMIN, wystarczy, że właśnie tak określimy to w adnotacji @PreAuthorize, czyli hasRole('ROLE_ADMIN').

Jednak gdy chcemy by dostęp do danej funkcjonalności miał tylko użytkownik z określonym przywilejem, na przykład ROLE_READ_PRIVILEGE, wówczas nie musimy wykazywać listy ról, które mają ten przywilej i ich używać w metodzie hasRole.

Jest to o tyle istotne, że o ile w naszym przypadku występują tylko dwie role, o tyle w prawdziwym systemie może istnieć dziesięć albo i więcej ról zawierających ten sam przywilej. Wtedy należałoby umieścić wszystkie te role w adnotacji @PreAuthorize. A tak wystarczy użyć samej nazwy przywileju, a więc: hasRole('ROLE_READ_PRIVILEGE').

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE