spring-security-json

Spring Security – uwierzytelnienie przy pomocy jsona

Spring Security to narzędzie, które pomaga uporządkować kwestie związane z uwierzytelnieniem i autoryzacją. Generalnie robi to wszystko za nas. Jednak jedynym z jego minusów jest to, że nie do końca jest przystosowane do pracy z usługami restowymi z obsługą jsona. W tym artykule zajmę się właśnie tą kwestią.

Spring security domyślnie obsługuje uwierzytelnienie przy pomocy formularza (application/x-www-form-urlencoded). Jednak gdy chcemy użyć jsona w usługach restowych, trzeba to odpowiednio skonfigurować. Spring nie ma niestety żadnego magicznego przełącznika dla takich sytuacji.

 

Cały artykuł opiera się o Spring Boot. Na końcu podlinkuję końcowy rezultat, w postaci projektu na githubie.

 

Domyślna konfiguracja Spring Security

Zacznijmy od dodania dwóch prostych endpointów. Jeden z nich będzie ogólnie dostępny, drugi dostępny po zalogowaniu.

MessageDto to prosty dtos, który ma tylko jedno pole message wypełniane przez konstruktor.

Następnie dodajmy zależność do Spring Security.

W Spring Boot dodanie zależności w gradlew wystarczy, żeby uruchomiła się autokonfiguracja. Od tego momentu, po wejściu na jakikolwiek endpoint, Spring będzie nas przekierowywał na domyślny formularz logowania (/login), który dostarcza biblioteka.

sing in form

Do tego formularza można zalogować się używając użytkownika user i hasła wygenerowanego przez waszą aplikację. Będzie ono widoczne w logach w takiej formie:

Po zalogowaniu, aplikacja powinna przekierować Cię do root contextu (/) i powinieneś zobaczyć komunikat:

 

Logowanie przy użyciu wygenerowanego hasła jest mało wygodne, ponieważ zmienia się ono po każdym restarcie aplikacji. Dodajmy więc użytkownika i hasło, tak by testowanie było wygodniejsze.

Dodaj klasę konfiguracyjną SecurityConfig, która dziedziczy po WebSecurityConfigurerAdapter i zawiera metodę, konfigurującą nam w pamięci bazę użytkowników (możemy w ten sposób konfigurować dowolną ilość użytkowników):

Konfigurujemy użytkowników pamięci tylko na potrzeby testów. W produkcyjnej aplikacji użytkownicy najczęściej będą przechowywani w bazie danych.

I tutaj możesz natknąć się na taki błąd (to istotne, bo różne źródła podają różny sposób konfiguracji Spring Security):

Błąd ten oznacza, że musisz jawnie podać, w jaki sposób jest zakodowane twoje hasło (w Spring Security 5 zmienił się format przechowywania hasła i teraz przed hasłem trzeba podać algorytm kodowania hasła). Na potrzeby testów korzystamy z inMemoryAuthentication, a dla wygody nie będziemy kodować hasła, więc potrzebujemy przed hasłem dodać {noop}:

Możesz także skorzystać z różnego rodzaju algorytmów hashujących takich jak: {bcrypt}, {pbkdf2}, {scrypt}, {sha256}

Dostosowanie konfiguracji do własnych potrzeb

Jak już napisałem wcześniej, po wejściu na dowolny endpoint zostaniesz przekierowany na formularz logowania. Przekierowanie to nie jest nam potrzebne, bo chcemy logować się do aplikacji za pomocą jsona wysyłanego postem, a zamiast przekierowania chcemy zawsze otrzymywać błąd 401 Unauthorized. Dodajmy więc odpowiednią konfigurację, która pozwoli wyłączyć przekierowanie, a także pozwoli wejść na root context (/) bez logowania.

Warto także włączyć debugowanie w security(@EnableWebSecurity(debug = true) – tylko na czas developmentu). Dodajemy kolejną metodę configure(HttpSecurity http) wraz z konfiguracją:

  1. Wyłączamy obsługę csrf tokena – nie będzie nam tutaj potrzebna.
  2. Pozwalamy wchodzić na root context (/) aplikacji bez logowania.
  3. Pozwalamy wchodzić bez logowania na formularz – co jest odwzorowaniem domyślnej konfiguracji.

Po tych zmianach, jedyna różnica jest taka, że root context (/) aplikacji jest dostępny bez logowania.

Teraz musimy dodać dodatkową konfigurację, która sprawi, że zamiast przekierowania na zabezpieczonych endpointach dostaniesz błąd 401 Unauthorized.

  1. Te dwie linijki sprawiają, że po wejściu na zabezpieczony endpoint, otrzymasz błąd 401 Unauthorized, zamiast przekierowania na formularz logowania.

 

W tym miejscu warto zaopatrzyć się w jakiegoś desktopowego klienta http, ponieważ domyślny formularz przestanie już działać i nie będziesz mógł na niego wejść. Ja użyłem Postmana (jako alternatywy możesz użyć innego klienta http, np. SoupUI, Insomnia REST Client, a nawet klienta konsolowego cUrl).

Nadal możemy  zalogować się za pomocą endpointu /login.

postman

Po zalogowaniu się możemy już wejść na endpoint secured (Postman zajmie się obsługą cookie za ciebie):

postman-secured

 

Wszystko działa, ale nadal endpoint /login konsumuje dane w formacie application/x-www-form-urlencoded i nie jest to spójne z usługami restowymi, które konsumują i produkują jsona (application/json). Kolejna kwestią jest to, że ciągle po zalogowaniu mamy przekierowanie na root context (/) aplikacji i usługa logowania zwraca {"message": "Hello world"}. Wszystkich te problemy rozwiążemy w następnym kroku.

Żeby zmienić opisane powyżej zachowania, musisz nadpisać domyśli filtr UsernamePasswordAuthenticationFilter, który jest odpowiedzialny za odczytywanie danych z formularza, tak żeby odczytywał dane z jsona.

Klasa przechowująca dane logowania:

Dodatkowo, musisz także odpowiednio skonfigurować instancję filtra tak, żeby korzystała z domyślnego authentication managera oraz musisz ustawić własną implementację successHandlera i failureHandlera, co pozwoli na pozbycie się przekierowania po poprawnym zalogowaniu:

  1. Dodajemy successHandler, który zaimplemntujemy tak, by nie przekierowywał.
  2. Następnie dodajemy standardowy failureHandler.
  3. Ustawiamy domyślny authenticationManager.

Poniżej kod successHandler:

i failureHandler:

 

Ostatnią rzeczą jest dodanie filtra do głównej konfiguracji Spring Security:

  1. Konfigurujemy filtr
  2. Usuwamy .formLogin().permitAll()

W tym miejscu wszystko powinno już działać. Nie zapomnij w Postmanie zmienić Content-Type na application/json. I koniecznie wyłącz debugowanie Spring Security @EnableWebSecurity(debug = false).

 

Aktualizacja: A jeżeli potrzebujesz innej metody uwierzytelnienia np. JWT, to możesz też zajrzeć do kolejnego z tej serii artykułu Spring Security i Json Web Token. Artykuł ten jest kontynuacją obecnego (w dużej mierze bazuje na tym artykule) i przedstawiam w nim jak skorzystać z wspomnianej metody autoryzacji czyli Json Web Token. Ta metoda uwierzytelnienia jest ostatnio bardzo popularna, więc zachęcam także do zapoznania się z nią.

 

 

Podsumowanie

Muszę przyznać, że już kilka razy implementowałem logowanie w usługach restowych ze Spring Security. I za każdym razem wygląda to trochę inaczej. Ale ten sposób, który tu przedstawiłem jest chyba najbardziej zwarty, więc może przyda mi się kolejnym razem 😉 Mam nadzieję, że tobie też.

Link do projektu na githubie: security-example

Żeby być na bieżąco nie zapomnij zapisać się na newsletter.

 

Żródła:

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.html

 

✅ Zapisz się na Newsletter

Mini kurs testy jednostkowe

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<

10 thoughts to “Spring Security – uwierzytelnienie przy pomocy jsona”

    1. Tak, jest. Cały ten przykład jest oparty o tradycyjną sesję http (klient przesyła cookie).

      1. A, spoko. Uczę się dopiero Springa i zmyliło mnie połączenie wzmianki o „usługach restowych” i formlogin w jednym wpisie i myślałem, że coś źle rozumiem. Dzięki za szybką odpowiedź!

        1. Jasne. W artykule opisuje tylko jak to zrobić jsonem. Nie poruszam innych problemów. Chciałem to opisać najprościej jak się da, więc dlatego jest na sesji http.

          I dopiero teraz przyszło mi do głowy, że linijka .formLogin().permitAll() w przykładzie na samym końcu artykułu jest zbędna 😉 Poprawię to. Dzięki za komentarz.

  1. Dodałem z artykułu klasę SecurityConfig z metodą konfigurującą w pamięci bazę użytkowników. Coś mi to nie działa. Dalej muszę wpisywać hasło wygenerowane przez aplikacje w momencie startu. A powinno logować z hasłem password.

    1. Na githubie jest przykładowy kod (na końcu artykułu jest link). Zajrzyj do przykładu, może gdzieś jest jakiś mały błąd.

      Edit: Był mały błąd, zamiast super.authenticationManagerBean() trzeba użyć super.authenticationManager(). Poprawiłem w tekście.

  2. Hej zastanawia mnie jedna rzecz, jak powiązać sceurity z JWT – przy aplikacji z react/angularem ?
    Tzn. mam oczywiście core – w oparciu o CRUD i springboot, ale mam też warstwę UI – opartą o angular lub react js, zastanawiam się czy implementacja autoryzacji wystarcza po stronie serwera? Chyba nie bo np chcąc tworzyć componenty – w react musze jakoś wpleść logowanie etc również w frontend?
    Jakiś tutorial znacie dobry do tego ? Pozdrawiam

    1. Implementacja logowania tylko po stronie backendu nie zawsze wystarczy. Jeśli backend korzysta z sesji http, to obsługą ciasteczek zajmie się przeglądarka i w zasadzie nie wiele musisz robić poza obsługą samego formularza logowania i przekierowywanie użytkownika na niego jeśli nie jest zalogowany.

      Jeśli chcesz korzystać z tokena (co opisałem w kolejnym artykule), to musisz już zając się obsługą tego tokena w aplikacji React/Angular (w Angularze robi się to zwykle przez interceptory). Tokena możesz zapisać np. w local storage i wysyłać z każdym requestem do serwera w nagłówku Authorize.

      Nie znam żadnych ciekawych tutoriali frontendowych na ten temat.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *