W tej lekcji skonfigurujemy autoryzację użytkowników naszej aplikacji z użyciem mechanizmu OAuth oraz bazy danych.
Najprostszym podejściem, jeśli chodzi o rejestrację i uwierzytelnianie użytkowników z punktu widzenia programisty jest po prostu przechowywanie loginu (lub adresu email) oraz hasła (a raczej jego skrótu) w bazie danych. Czasem takie podejście ma też uzasadnienie biznesowe (np. aplikacja bankowa raczej nie powinna pozwalać na logowanie się za pomocą konta Google), ale w większości przypadków jest po prostu utrudnieniem dla naszych użytkowników i może negatywnie wpłynąć na ich wrażenia i ilość odwiedzin.
Z tego powodu w projekcie bilet wykorzystamy technologię OAuth, dzięki której nie będzie trzeba wymyślać loginów i haseł oraz przechodzić przez proces rejestracji — wystarczy być zalogowanym np. w Google i kliknąć ‘OK’ na odpowiednim ekranie. Taki proces wymaga nieco więcej konfiguracji po stronie aplikacji, ale dzięki temu nasza aplikacja będzie bezpieczniejsza (nie musimy martwić się o bezpieczeństwo haseł przechowywanych w bazie danych, roboty tworzące wiele kont z automatu itp)
Autoryzacja użytkowników z użyciem OAuth
Uwaga! Ta sekcja nie ma na celu szczegółowe omówienie specyfikacji OAuth i w pewnych miejscach stosuje uproszczenia i skróty myślowe — chodzi o to, aby przekazać ‘koncept’ i ogólny mechanizm działania, szczegóły implementacji są na tym etapie mniej istotne. Zainteresowanych szczegółami technicznymi oraz dokładnym opisem odsyłamy do dokumentacji źródłowej.
Co to jest?
W najprostszym podejściu proces uwierzytelniania użytkownika oparty jest o parę login (lub adres email) oraz hasło. Założeniem w tym wypadku jest to, że hasło znamy tylko my i trudno jest je odgadnąć. Niestety, takie podejście współcześnie nie jest wygodne dla użytkowników — biorąc pod uwagę ilość serwisów i aplikacji z których korzystamy na codzień lub okazyjnie, ilość unikalnych haseł do zapamiętania przerasta możliwości większości osób. Jednocześnie większość osób ma konto u jednego z ‘zaufanych’ dostawców — np. Google, Yahoo, GitHub, Microsoft itp. A gdyby tak móc użyć tego konta z ‘zaufanego’ serwisu w innych aplikacjach?
Takie podejście miałoby wiele korzyści — użytkownik musiałby pamiętać tylko jedno hasło, przez co mogłoby ono być bardziej skomplikowane lub wsparte dodatkowymi mechanizmami (np. kodem SMS czy tokenem sprzętowym). Potencjalnie moglibyśmy także korzystać z informacji o użytkowniku zgromadzonych w tym centralnym miejscu — adres email, numer telefonu czy ‘avatar’ i strona internetowa, nie byłoby potrzeby wpisywania tych danych wile razy i aktualizowania ich wszędzie. Brzmi kusząco i niestety dość utopijnie :) Powstało wiele standardów mniej lub bardziej związanych z tym konceptem — OpenID, Oauth, SAML i inne. W przeszłości integracja była dość problematyczna i nie było prostego rozwiązania powszechnie akceptowanego przez różnych dostawców. Na szczęście współcześnie mamy OAuth, który jest implementowany praktycznie przez każdą liczącą się organizację.
Jak to działa?
Jak wiele współczesnych technologii i standardów, także OAuth bazuje na pomysłach, które sprawdzają się od wielu lat. Mowa o systemie Kerberos — opracowanym na MIT systemie pozwalającym na centralne uwierzytelnianie użytkowników oraz wykonywanie przez systemy akcji w ich imieniu. Co ciekawe, jest to jeden z niewielu standardów, które Microsoft zaimplementował w swoich systemach — oprogramowanie ActiveDirectory, które jest używane w praktycznie każdej firmie do zarządzania kontami użytkowników opiera się o LDAP oraz właśnie protokół Kerberos.
Kerberos bazuje na tzw. grantach — grant może zostać wydany na podstawie np. loginu i hasła użytkownika lub innego grantu (o takich samych lub mniejszych uprawnieniach). System, który taki grant posiada, może wykonywać operacje w imieniu danego użytkownika. Granty wygasają po określonym czasie o ile nie zostaną odnowione, można je także anulować.
OAuth to właściwie uproszczenie i przeniesienie tej idei do realiów internetu — głównym konceptem są tokeny. Token jest generowany za każdym razem, kiedy dana aplikacja otrzymuje dostęp do określonych zasobów. Tokeny mogą być wystawione na określony czas, mogą być przedłużane oraz używane do wykonywania operacji ‘w imieniu’ użytkownika. OAuth określa także jak należy przekazywać token pomiędzy systemami z użyciem protokołu HTTP (w nagłówku Authentication z prefixem Bearer), jakie uprawnienia są z nim powiązane (np. pozwala poznać tylko adres email lub pozwala na wysyłanie/czytanie wiadomości w GMail) a także sposoby uzyskania takiego tokenu (nie będziemy się na tym skupiać, ponieważ leży to w kompetencjach dostawcy usług — jeśli chciałabyś poznać szczegóły poszukaj np. ‘OAuth flow types’).
W najprostszym przypadku korzystanie z OAuth przez naszą aplikację może przebiegać następująco:
- Użytkownik aplikacji klika w link ‘zaloguj z Goolge’ i jest przekierowany na stronę Google
- Użytkownik potwierdza udzielenie uprawnień dla aplikacji
- Użytkownik jest przekierowany z powrotem do aplikacji, wraz z tokenem przekazanym w parametrach
- Przeglądarka użytkownika zapamiętuje token np. w ciasteczku i dołącza go do każdego zapytania do naszej aplikacji
- Aplikacja po otrzymaniu zapytania może zapytać dostawcę (np. Google) prosząc o potwierdzenie, czy token jest nadal ważny oraz zwrócenie jakichś informacji (np. adresu email). Korzystając z pozyskanego w ten sposób adresu email, aplikacja identyfikuje użytkowników.
Prawda, że proste? A co najważniejsze, wygodne dla naszych użytkowników :)
Przystępny opis jak działa OAuth znajdziesz np. na stronie Aarona Pareckiego.
Mały haczyk, czyli o nadużywaniu standardów słów kilka
Wiesz już czym jest OAuth i mniej więcej jak działa. Zanim przejdziemy do wykorzystywania go do uwierzytelniania użytkowników w naszej aplikacji, powinnaś wiedzieć że OAuth to skrót od Open Authorization, a więc służy do autoryzacji, a nie uwierzytelniania (uwierzytelnianie to potwierdzanie tożsamości, a autoryzacja to udzielenie uprawnień do czegoś, np. jakichś danych)! Jak to działa w takim razie? Otóż korzystając z OAuth do uwierzytelniania większość serwisów w rzeczywistości prosi o dostęp (autoryzację) do adresu email. Dostawcy usług (np. Google) wymagają od użytkownika zalogowania się, aby móc takiego dostępu udzielić naszej aplikacji. W praktyce więc, otrzymanie autoryzacji (dostępu) do adresu email aplikacje utożsamiają z poprawnym uwierzytelnieniem (co my także wykorzystamy).
Niemniej trzeba wiedzieć, że takie podejście może nas narażać na problemy z bezpieczeństwem — dostawcy usług nie muszą (wg protokołu) uwierzytelniać użytkowników, mogą nam udostępnić np. pusty adres email dla nieuwierzytelnionych użytkowników (jednocześnie zwracając ‘sukces’ jako wynik naszego zapytania). Jeśli integrujesz się ze znanymi dostawcami usług (jak np. Google czy GitHub), ryzyko jest minimalne, jeśli jednak planujesz integrację z ‘dowolnym’ dostawcą, musisz mieć powyższe na uwadze (i o ile to możliwe zrezygnować z OAuth na rzecz OpenID — opartego o OAuth protokołu uwierzytelniania). Ciekawy artykuł w tym temacie znajdziesz na oficjalnej stronie OAuth. Krótki opis różnic znajdziesz też na anglojęzycznej wikipedii.
Pozostaje kwestia dlaczego tak robimy, skoro jest to nieprawidłowe podejście? Projektując aplikację wiele decyzji, które będziesz musiała podjąć jest z kategorii ‘zrobić coś dobrze, czy wystarczająco dobrze i szybko?’. Ponieważ wiemy, z jakimi dostawcami usług się integrujemy, jakie są tego ryzyka oraz co (potencjalnie) tracimy, możemy wybrać prostszą drogę (która jednak nie jest w 100% tą prawidłową). Implementacja zgodna z OpenID jest nietrywialna (krótka notka na StackOverflow z opisem doświadczeń z tym związanych) i przy pewnych założeniach rozwiązanie oparte o OAuth jest ‘wystarczająco dobre’ (i znacznie szybsze w implementacji).
Podstawowa konfiguracja Spring Security
Ponieważ podczas tworzenia projektu wybraliśmy także moduł Spring Security, wystarczającym będzie dodanie najprostszej konfiguracji. Spring Boot ponownie wprowadza znaczące uproszczenia w stosunku do ‘standardowego’ Springa MVC (dla porównania oraz przypomnienia, możesz prześledzić lekcję 16 naszego kursu Javy, w której konfigurowaliśmy Spring Security) — teraz wystarczy rozszerzyć bazową konfigurację i zmodyfikować to, czego potrzebujemy. Tworzymy więc nową klasę konfiguracji, która dziedziczy po WebSecurityConfigurationAdapter. Dodajemy także adnotacje @Configuration (ponieważ jest to bean konfiguracji) oraz @EnableWebSecurity (o tej adnotacji za chwilę), a także implementujemy metodę configure(AuthenticationManagerBuilder auth) tak jak na przykładzie poniżej:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("[email protected]")
.password("admin")
.roles("USER");
}
}
W tym krótkim fragmencie kodu zrobiliśmy następujące rzeczy:
- Uruchomiliśmy mechanizmy Spring Security i zintegrowaliśmy je ze Spring MVC (to wszystko dzięki adnotacji @EnableWebSecurity)
- Zastosowaliśmy domyślne ustawienia (m.in. umożliwiliśmy logowanie za pośrednictwem formularza oraz zabezpieczyliśmy wszystkie zapytania — dostęp do jakiejkolwiek części naszej aplikacji będzie wymagał uwierzytelnienia) dzięki rozszerzeniu WebSecurityConfigurerAdapter
- Skonfigurowaliśmy użytkownika, którym można się logować w aplikacji (przesłaniając metodę protected void configure(AuthenticationManagerBuilder auth) )
Uruchamiając aplikację (pamiętaj o uruchomieniu jej z użyciem profilu development!) w tym momencie i wpisując w przeglądarce adres http://localhost:8080/greet/Jakub zostaniesz przekierowana na stronę logowania — możesz tam użyć danych z powyższego fragmentu (login to [email protected]; hasło to admin). Po zalogowaniu się aplikacja będzie działała normalnie.
Jak na pewno pamiętasz, w poprzedniej lekcji utworzyliśmy już tabelę w bazie danych do przechowywania danych użytkowników, a także testowego użytkownika. Podmieńmy więc naszego użytkownika zdefiniowanego na sztywno na konfigurację z użyciem bazy danych. Jest to na tyle częsty przypadek, że Spring Security posiada wbudowane mechanizmy, które mogą nam pomóc — wystarczy podać zapytania SQL dopasowane do naszego modelu. Wystarczy podmienić metodę configure(…) na następującą (oraz dodać pole typu DataSource z adnotacją @Autowired):
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("SELECT `email`,`passwordHash`,`active` FROM `users` WHERE `email`=?")
.authoritiesByUsernameQuery("SELECT 'USER' FROM `users` WHERE `email`=?")
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
W powyższym fragmencie konfigurujemy także domyślny PasswordEncoder — czyli sposób, w jaki hasła będą hashowane aby zapobiec ich kradzieży. W tym wypadku użyjemy algorytmu BCrypt, który jest jedną z bezpieczniejszych opcji.
Jeśli czytałaś nasz kurs Javy, być może skojarzysz zapytania SQL — byłoby to skojarzenie bardzo słuszne, ponieważ konfigurujemy dokładnie te same mechanizmy, tyle tylko, że w bardziej przystępny sposób. Po ponownym uruchomieniu aplikacji będzie ona zachowywać się dokładnie tak samo jak poprzednio, ale użyte przez nas login i hasło zostaną porównane z tymi znajdującymi się w bazie danych.
Konfiguracja serwera autoryzacji OAuth2
Serwer autoryzacji to jeden z dwóch ‘składowych’ aplikacji OAuth2 — jest to ta część, która odpowiada za autoryzację użytkowników. Innymi słowy, odpowiada za uwierzytelnianie (sprawdzenie loginu oraz hasła), generowanie i odświeżanie (wydłużanie czasu ważności) tokenów. Sam standard (jak i implementacja Spring’owa) pozwala korzystać z niego jako modułu aplikacji ale także jako zewnętrznego systemu (dzięki temu nasza aplikacja może wykorzystywać autoryzację np z Facebookiem — tym zajmiemy się w przyszłości ;) ).
Aby skonfigurować nasz serwer autoryzacji, potrzebujemy kilku rzeczy:
- sposób na przechowywanie tokenów — tzw. Token Store (my skorzystamy z bazy danych)
- informacji dla Springa żeby wykonał za nas większość pracy ;)
- konfiguracja klienta (clientId oraz secret), z którego będzie korzystał nasz frontend
Ale wszystko po kolei, zacznijmy więc od …
Konfiguracja Token Store
Podobnie jak w każdej innej sytuacji i tutaj Spring daje nam wiele możliwości i opcji konfiguracyjnych — możemy np. wykorzystać gotowy mechanizm przechowywania tokenów w pamięci, ale możemy też skonfigurować do tego bazę danych. Ta druga opcja jest zdecydowanie wygodniejsza — w przypadku aktualizacji aplikacji na serwerze czy konieczności dodania kilku dodatkowych serwerów, użytkownicy nie zostaną wylogowani. Jednocześnie jest to na tyle proste, że nie przysporzy nam sporo pracy.
Niestety konfiguracja bazy danych pod Token Store jest jednym z niewielu przykładów, kiedy dokumentacja Springa jest niezbyt pomocna (wynika to z faktu, że Spring pozwala nam zdefiniować bazę danych samodzielnie i skorzystać z własnych zapytań SQL — my jednak zdamy się na ‘domyślne’ ustawienia). Nie oznacza to jednak, że jesteśmy zupełnie pozbawieni informacji — bazując na domyślnych zapytaniach SQL oraz schematach używanych do testów możemy łatwo napisać odpowiedni skrypt:
CREATE TABLE oauth_access_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication LONG VARBINARY,
refresh_token VARCHAR(255)
);
CREATE TABLE oauth_refresh_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication LONG VARBINARY
);
To wystarczy, aby przechowywać nasze tokeny w bazie danych. W tym momencie zastanawiasz się zapewne po co dwie tabele i czym się różni token od refresh_token. Otóż głównym elementem, który wykorzystujemy i przesyłamy w każdym zapytaniu jest token. Token ma określony czas ważności, liczony od czasu jego wygenerowania — np. poprzez logowanie (przeważnie wyrażony w minutach, do godziny). Po upływie czasu ważności tokenu jest on unieważniany, z punktu widzenia użytkownika następuje wylogowanie. W tym miejscu pojawia się pewnego rodzaju konflikt interesów — z jednej strony wygodnym dla użytkownika byłoby utrzymanie tokenu ‘ważnym’ jak najdłużej, aby nie irytować kogoś koniecznością ponownego logowania się; z drugiej strony mamy bezpieczeństwo — token może zostać wykradziony, ktoś może przejąć komputer i pozostać ‘zalogowanym’ itp — w tym wypadku token powinien żyć jak najkrócej. Pewnego rodzaju kompromisem jest wprowadzenie dodatkowego specjalnego tokenu (refresh_token), który musimy użyć aby przedłużyć ważność naszego tokenu. Ponieważ jest on używany tylko do tej jednej czynności (czyli nie jest przesyłany z każdym zapytaniem), jest mniejsze prawdopodobieństwo jego kradzieży czy przechwycenia; w przypadku nieaktywności (np. zamknięcia komputera) standardowy token się po prostu względnie szybko przeterminuje, więc maleje też ryzyko niepowołanej osoby będącej zalogowanej na naszym koncie. Z tego powodu też czas życia refresh_token jest zwykle znacznie dłuższy (mierzony w dniach, np. tydzień czy miesiąc). Oba tokeny otrzymujemy w momencie wygenerowania tokenu (zalogowania się), co zobaczymy już za chwilę :)
Konfiguracja Springa
Aby skonfigurować serwer autoryzacji, dodajmy klasę Oauth2AuthServerConfig:
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("frontendClientId")
.secret("frontendClientSecret")
.authorizedGrantTypes("password","authorization_code", "refresh_token")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(28*24*3600)
.scopes("read");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
}
Voilà! W tym momencie jesteś już autorką działającej aplikacji wykorzystującej OAuth! Ale znając Twoją dociekliwość, chciałabyś wiedzieć co tam się dzieje, przyjrzyjmy się więc nieco bliżej ;)
Adnotacja @EnableAuthorizationServer — ta adnotacja informuje Springa, że ma włączyć obsługę OAuth oraz że stosowną konfiguracje dostarczymy z beanem typu AuthorizationServerConfigurerAdapter. W naszym przypadku adnotację tą umieściliśmy dokładnie nad definicją tego beana, aby zachować powiązane rzeczy w jednym miejscu.
Rozszerzenie klasy AuthorizationServerConfigurerAdapter — w ten sposób możemy skonfigurować poszczególne elementy serwera autoryzacji, dopasowując je do naszej aplikacji.
Konfiguracja klienta (metoda o sygnaturze public void configure(ClientDetailsServiceConfigurer clients) throws Exception) — w tej metodzie możemy skonfigurować klienta (lub klientów) OAuth. W uproszczeniu, klient to aplikacja korzystająca z naszego API, a więc taka, która może generować tokeny dla użytkowników, prosić o ich dane czy sprawdzać ważność tokenów (oczywiście wszystkie te uprawnienia aplikacji można konfigurować). W naszej aplikacji będzie tylko jeden klient — czyli frontend (choć będzie on w jednym repozytorium, to uruchamia się on jednak w przeglądarce użytkownika, traktujemy go więc jako oddzielną aplikację). Dzięki temu możemy skonfigurować go w kodzie, nie ma potrzeby tworzenia bazy danych (ta byłaby potrzebna, gdybyś chciała umożliwić innym integrację z Twoją aplikacją — tak jak możliwe jest to np. w przypadku Google, Facebooka czy GitHuba). Dwa ważne parametry w tym wypadku to clientId oraz secret — każde zapytanie związane z OAuth (np. generowanie tokenu, sprawdzenie jego ważności czy odswieżenie) będzie musiało być uwierzytelnione właśnie tą parą informacji. W naszym przypadku informacje te będą względnie jawne (muszą się one znajdować gdzieś w kodzie FrontEndu), dlatego ważne jest jakie uprawnienia nadajemy temu klientowi — dobrą praktyką jest nadawanie ich jak najmniej (w naszym przypadku będzie to password oraz refresh_token, jeśli serwer autoryzacji działałby jako oddzielna aplikacja, potrzebny mógłby być także authorization_code — przyjazne wyjaśnienie różnic można znaleźć np na stronie oauth2.thephpleague.com)
TokenStore — czym jest TokenStore omówiliśmy sobie już powyżej, tutaj jedynie skonfigurujemy go i ‘udostępnimy’ Springowi jako beana. Wystarczy utworzyć obiekt typu JdbcTokenStore — ponieważ naszą bazę danych modelowaliśmy na podstawie domyślnych zapytań Springa, nie musimy konfigurować specyficznych zapytań.
Warta uwagi jest także metoda public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception — służy ona konfiguracji uprawnień do funkcjonalności OAuth2. Zapewne pytasz w tym momencie — ale jak to, czy nie konfigurowaliśmy zabezpieczeń w innej klasie? I będziesz miała rację, jednak wszystkie kwestie związane z OAuth2 są wykonywane ‘przed’ tradycyjną konfiguracją Spring Security (dzięki temu w jednej aplikacji możemy obsługiwać zarówno OAuth2, jak i ‘tradycyjne’ logowanie za pomocą sesji).
Testujemy nasze API
Do testowania skorzystamy z klienta Restlet — jest on bezpłatny i dostępny jako plugin do przeglądarki, przez co jest wygodny w użyciu. Dodatkowo możemy zapisywać wywoływane zapytania i grupować je w scenariusze, co pozwala szybko zweryfikować rzeczy podczas rozwijania aplikacji.
Przede wszystkim musimy wygenerować token. W tym celu wysyłamy zapytanie POST na domyślny endpoint OAuth dla Springa (czyli /oauth/token), jako parametry podając grant_type (wartość: password), username (nasza nazwa użytkownika) oraz password (nasze hasło) tak jak na poniższym obrazku:
Do tego zapytania musimy także ustawić autoryzację (czyli podać dane klienta) — w tym celu klikamy na ‘Add authorization’ po lewej stronie i uzupełniamy formularz naszymi danymi:
Teraz już możemy wykonać zapytanie (przycisk ‘SEND’), w odpowiedzi powinnaś zobaczyć coś podobnego:
Oczywiście tokeny będą inne, ale struktura obiektu pozostanie taka sama. Teraz możemy przejść do naszego API — zmodyfikuj zapytanie aby wyglądało tak jak na poniższym ekranie, w nagłówkach wpisz jedynie Authorization jako nazwę oraz ‘Bearer {token}’ jako wartość (bez apostrofów, podmieniając za {token} to, co otrzymałaś w odpowiedzi na poprzednie zapytanie).
Po wykonaniu tego zapytania niestety nie otrzymamy oczekiwanej odpowiedzi:
Wynika to z tego, że standardowa konfiguracja Spring Security nie ‘rozumie’ tokenów OAuth2 i nie potrafi zweryfikować naszej tożsamości. Zaradzimy temu jednak w kolejnym kroku :)
Zwróć uwagę, że jeżeli jesteś już zalogowana, zwrócony zostanie dokładnie ten sam token (wraz z odpowiednim czasem życia) — aby przedłużyć ważność tokena, musisz użyć endpointu do odświeżania. Pamiętaj także, że token będzie inny za każdym razem kiedy uruchomisz aplikację / poprzedni wygaśnie! Dlatego testując musisz skopiować odpowiedź z pierwszego zapytania do nagłówka z drugiego zapytania przy każdej zmianie w aplikacji.
Jako że jesteśmy dobrymi programistami, a więc naszą wartością wyższą jest lenistwo, powyższy proces możemy także skonfigurować automatycznie w kliencie Restlet (dawniej DHC) — szczegóły znajdziesz opisane na blogu restlet.com. Gotowy scenariusz do pobrania dla naszej aplikacji (uruchomionej na Twoim komputerze na porcie 8080) możesz pobrać tutaj.
Konfiguracja serwera zasobów (Resource Server) OAuth2
Serwer zasobów w nomenklaturze OAuth2 to nic innego jak nasze API (‘zasób’ bierze się stąd, że w założeniu rozmawiamy o RESTowych API). W praktyce tą część już mamy (czy raczej będziemy mieli w przyszłości), a jedyne co musimy zrobić to poinformować Springa, które zasoby będą chronione przez OAuth. Dzięki temu, że nasz serwer autoryzacji działa w tej samej aplikacji, zadzieje się automagia — gdyby tak nie było, musielibyśmy podać kilka dodatkowych informacji (a dokładniej adresy URL do serwera autoryzacji, id oraz sekret klienta). W przyszłości połączymy nasz wewnętrzny serwer z zewnętrznym (np. Facebookiem czy GitHubem), póki co uprościmy sobie sprawę i skorzystamy z wbudowanych mechanizmów.
Aby skonfigurować nasz serwer zasobów, podobnie jak poprzednio potrzebujemy tylko kilku rzeczy:
- sposób na weryfikację tokenów — to już mamy za darmo!
- informacji dla Springa żeby wykonał za nas większość pracy ;)
Bez przedłużania, przejdźmy do naszego kodu, a dokładniej nowej klasy konfiguracji:
@Configuration
@EnableResourceServer
public class Oauth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/greet/*")
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
Nie za wiele, prawda? Podobnie jak ostatnio — Spring przyjmie sensowne wartości domyślne i wykona całą ciężką pracę za nas. Zerknijmy jednak nieco między wiersze, co tak naprawdę napisaliśmy powyżej:
Adnotacja @EnableResourceServer — podobnie jak w przypadku serwera autoryzacji, ta adnotacja mówi Springowi aby uruchomił mechanizmy związane z serwerem zasobów oraz poszukał beana typu ResourceServerConfigurer.
Rozszerzenie klasy ResourceServerConfigurerAdapter — w ten sposób zapewniamy beana, który implementuje ResourceServerConfigurer; klasa ResourceServerConfigurerAdapter pozwala nam użyć Springowych wartości domyślnych i nie implementować wszystkich metod interfejsu.
Także tutaj mamy dodatkową metodę public void configure(HttpSecurity http), która związana jest z konfiguracją zabezpieczeń. Konfiguracja Spring Security, którą zdefiniowaliśmy na początku lekcji, nadal będzie obowiązywała, ale tylko dla ‘normalnych’ sesji — tj takich, które powstały poprzez zalogowanie się za pomocą formularza (jak ze wszystkim innym, Spring pozwala nam łączyć wiele różnych mechanizmów — taki podział byłby użyteczny jeśli mielibyśmy część aplikacji ‘standardowej’ (np. aplikacja webowa z widokami JSP czy Tiles tak, jak robiliśmy to w naszym kursie Javy) i chcielibyśmy do niej powoli dołączać kolejne moduły z autoryzacją OAuth2). Ta metoda pozwala nam określić zabezpieczenia dla sesji OAuth2 — wskazać, które ścieżki mają być obsługiwane oraz jakie ewentualnie dodatkowe warunki musi spełniać użytkownik, aby uzyskać dostęp do zasobu (trochę szerzej porozmawiamy o tym przy okazji integracji z innymi dostawcami OAuth — na ten moment wystarczy nam, ze użytkownik jest uwierzytelniony).
Testujemy!
Testować możemy dokładnie w ten sam sposób jak poprzednim razem — z tym wyjątkiem, że teraz zadziała ;)
Sprzątamy konfiguracje Spring Security
W teorii moglibyśmy całkowicie usunąć naszą pierwotną konfigurację Spring Security i manualnie stworzyć beany wymagane przez OAuth2 (AuthenticationManager itp). W praktyce, dużo wygodniej będzie po prostu wyłączyć ‘standardowe’ mechanizmy Spring Security (takie jak formularz logowania czy ogólnie mechanizm logowania w aplikacji webowej) oraz domyślnie odrzucać wszystkie zapytania. Do naszej klasy dodajmy więc metodę protected void configure(HttpSecurity http) throws Exception :
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.denyAll()
.and()
.formLogin()
.disable();
}
w ten sposób wyłączyliśmy standardowe mechanizmy logowania (poprzez formularz) oraz blokujemy dostęp do wszystkich zapytań (pamiętaj, że przed tą konfiguracją uruchomi się konfiguracja OAuth2 — jeśli zapytanie HTTP będzie miało odpowiednie nagłówki i zostanie pozytywnie uwierzytelnione za pomocą OAuth2, ta konfiguracja zostanie w efekcie pominięta — nie zostanie uruchomiona).
Podsumowanie
Uff, w dzisiejszej lekcji było niemało materiału, ale mamy nadzieję, że udało nam się przedstawić go w przystępny sposób :) To oczywiście tylko przysłowiowy ‘wierzchołek góry lodowej’, jednak zupełnie wystarczający do posługiwania się OAuth2 na codzień w swojej aplikacji. Poniżej podajemy także linki do zasobów, gdzie można zapoznać się z samym standardem OAuth2, wyjaśnieniem różnych typów grantów i przykładowymi flow. Do konfiguracji OAuth2 powrócimy jeszcze w przyszłości, kiedy będziemy umożliwiali logowanie się za pomocą konta Google czy Facebook.
Jak pewnie zauważyłaś, granice pomiędzy ‘uwierzytelnianiem’ i ‘autoryzacją’ czy ‘logowaniem’ i ‘rejestracją’ zaczynają się zacierać — i faktycznie w tym kierunku zmierzają standardy. Wiąże się to przede wszystkim z wygodą użytkownika — po co wymagać rejestracji (i pamiętania haseł) do kolejnego serwisu, skoro na 99% użytkownik ma już konto w Google / Facebook / GitHub itp. Jest to także bezpieczniejsze podejście — nie musimy się martwić o luki w naszym systemie rejestracji czy przechowywania haseł, nie stracimy masy czasu na poprawną implementację 2FA; zamiast tego wystarczy nam integracja z istniejącym dostawcą i możemy poświęcić naszą energię na implementację funkcjonalności. Użytkownicy także docenią takie podejście — dzięki temu zamiast wpisywać po raz kolejny swoje dane i pamiętać kolejne hasło, wystarczy że klikną jeden przycisk i potwierdzą uprawnienia drugim kliknięciem. Ot, postęp!
Więcej informacji i linki
- OAuth.com — bardzo pomocna strona z wytłumaczonymi różnymi aspektami OAuth’a
- Oficjalny tutorial Spring Boot i OAuth
- Tutorial na stronie baeldung.com o budowaniu RESTowego API ze Spring Boot i Angularem
- Dokumentacja modułu Spring Security Oauth
- Wstęp do OAuth na stronie digitalocean.com
- Tutorial Spring Security + OAuth na stronie Gigsterous Blog
- Sekcja o OAuth2 w reference manual’u Spring Boot
Kod źródłowy
Kody źródłowe są dostępne w serwisie GitHub — użyj przycisków po prawej aby pobrać lub przejrzeć kod do tego modułu. Jeśli masz wątpliwości, jak posługiwać się Git’em, instrukcje i linki znajdziesz w naszym wpisie na temat Git’a.
Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!