#16.2 – zabezpieczanie aplikacji z użyciem Spring Security

By 10 stycznia 2015Kurs Javy
Wpis-Header (4)

Dziś kolejna część z cyklu o Spring Security – dodawanie i konfiguracja w projekcie

Jednocześnie przepraszam bardzo za opóźnienie, wiele się ostatnio dzieje i niestety ma to wpływ na pisanie na bieżąco :( Obiecuję, że spróbuje się ogarnąć i pisać na czas w przyszłości :)

Ale przejdźmy do rzeczy – konfiguracji Spring Security w projekcie.

Lekcja

Dodawanie SpringSecurity do projektu

Zależności Maven

Aby dodać Spring Security do projektu, dodajemy w pliku pom.xml zależności do modułów org.springframework.security:spring-security-config oraz org.springframework.security:spring-security-taglibs .

Uwaga! W momencie pisania tego kursu najnowsza wersja Spring Security to 3.2.5, która współpracuje ze Spring Framework w wersji 3.2.8 (jeśli będziesz kiedykolwiek potrzebowała to sprawdzić – zerknij na stronę z opisem artefaktu – na dole strony znajdziesz artefakty, z których korzysta wraz z wersjami, które są używane). Z tego powodu do czasu wydania wersji 4.0.0, konieczne jest obniżenie wersji Spring Framework w plikach pom.xml do 3.2.8.RELEASE oraz spring-data do wersji 1.5.2.RELEASE (wersja zgodna ze Spring Framework 3.5.8). W przyszłości po wydaniu wersji, ta lekcja zostanie zaktualizowana.

Konfiguracja XML – uproszczona

W następnym kroku, do katalogu /src/main/resources dodajemy plik security-context.xml o następującej treści:

<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">

    <http auto-config="true">
    </http>

    <authentication-manager>
        <authentication-provider>
            <user-service>
                <user name="kobietydokodu" password="jakieshaslo" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>

Uwaga! Powyższy plik zawiera statyczną listę użytkowników wraz z ich hasłami – w dalszej części zamienimy to w taki sposób, aby korzystał z utworzonej przez nas bazy danych. Niemniej łatwiej jest skonfigurować i rozwiązywać ewentualne problemy w takiej konfiguracji – z tego powodu będziemy korzystali najpierw ze statycznej listy użytkowników, którą następnie podmienimy na dynamiczną, z użyciem bazy danych.

Dodaliśmy dwa elementy – pierwszy pozwala nam na uproszczoną konfigurację (atrybut auto-config=”true”), dzięki czemu dostajemy automatycznie:

  • formularz logowania
  • operacje wylogowywania
  • możliwość uwierzytelniania za pomocą nagłówków protokołu HTTP (nie będziemy z tego korzystać, ale może się to przydać np. jeśli tworzymy API)

Konfiguracja web.xml

W pliku tym dokonujemy 2 zmian – po pierwsze, modyfikujemy poniższy fragment:

<context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>

Wskazując teraz lokalizację naszego pliku security-context.xml:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/classes/security-context.xml</param-value>
</context-param>

Spowoduje to, że plik ten będzie tzw. głównym kontekstem (więcej na ten temat możesz poczytać w lekcji 9) i będzie dotyczył wszystkich innych kontekstów (w tym naszej aplikacji webowej).

Ponadto dodajemy poniższe elementy:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Powodują one, że wszystkie zapytania HTTP będą ‘weryfikowane’ za pomocą Spring Security.

W tym miejscu warto upewnić się, czy wszystko działa poprawnie – czy możemy uruchomić naszą aplikację oraz czy po wejściu na adres (adres serwera i kontekstu aplikacji)/spring_security_login, możemy się zalogować za pomocą podanych wcześniej loginu i hasła.

Korzystamy z użytkowników w bazie danych

Ten krok pozwoli nam logować się za pomocą danych użytkowników z bazy danych – a więc jak w prawdziwej, komercyjnej aplikacji :) W tym miejscu zakładamy, że samodzielnie utworzyłaś w swojej aplikacji funkcjonalność rejestracji – czyli tak naprawdę formularz oraz klasy z adnotacjami JPA pozwalające dodawać użytkowników do bazy danych (jeśli nie jesteś pewna, jak to zrobić, przypomnij sobie z lekcji 10 (o tworzeniu formularzy i odbieraniu danych) oraz 14 (o używaniu bazy danych w aplikacji Spring MVC) ). Alternatywą jest ręczne utworzenie rekordów w bazie danych, ale prawie każda aplikacja potrzebuje możliwość rejestracji się przez użytkowników, więc lepiej zrobić to od razu.

Pierwszym krokiem jest przeniesienie połączenia z bazą danych z pliku applicationContext.xml do pliku security-context.xml . Przenosimy więc cały poniższy tag z jednego pliku do drugiego (oczywiście, w Twojej aplikacji mogą być to nieco inne dane):

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/koty" />
    <property name="username" value="login" />
    <property name="password" value="haslo" />
</bean>

W przeciwnym wypadku Spring Security nie ‘widziało’ by połączenia z bazą danych, a jest ono potrzebne do weryfikacji loginu i hasła.

Kolejnym krokiem jest podmiana statycznej listy użytkowników na taką, która obsługuje bazy danych. W pliku security-context.xml zamieniamy więc fragment:

<authentication-manager>
    <authentication-provider>
        <user-service>
            <user name="kobietydokodu" password="jakieshaslo" authorities="ROLE_USER" />
        </user-service>
    </authentication-provider>
</authentication-manager>

Na następujący:

<authentication-manager>
   <authentication-provider>
 <jdbc-user-service data-source-ref="dataSource"
   users-by-username-query=
     "SELECT username, password, enabled FROM users WHERE username=?"
   authorities-by-username-query=
     "SELECT username, ‘ROLE_USER’ FROM users WHERE username =?  " />
   </authentication-provider>
 </authentication-manager>

W tym fragmencie dzieją się następujące rzeczy:

  • tworzymy jdbc-user-service – jest to gotowa implementacja, która pozwala obsługiwać uwierzytelnianie z użyciem bazy danych ustawiając jedynie kilka niezbędnych atrybutów
  • wskazujemy połaczenie do bazy danych (atrybut data-source-ref=”dataSource” – wskazuje nam na beana o id dataSource – tego, którego w poprzednim kroku przenieśliśmy do naszego pliku security-context.xml)
  • ustawiamy zapytanie SQL, które pobierze nam użytkownika, hasło oraz informacje, czy użytkownik jest aktywny dla podanego loginu (atrybut users-by-username-query) – dzięki temu Spring pobierze te dane i zweryfikuje, czy podane login i hasło są prawidłowe
  • ustawiamy zapytanie SQL, które dla określonego użytkownika pobierze nam listę ról, które on posiada – ponieważ w naszej aplikacji nie będziemy korzystać z ról (powiemy o nich w osobnym wpisie jako materiał rozszerzający w przyszłości), podane zapytanie pobiera statyczny ciąg znaków ‘ROLE_USER’ dla wybranego użytkownika (jest to uproszczone podejście, które nie do końca jest poprawnym, ale na ten moment będziemy z niego korzystać w celu pokazania idei)

Oczywiście w zależności od tego, jak wygląda Twoja baza, być może będziesz musiała zmodyfikować zapytania SQL tak, aby odpowiadały Twoim nazwom tabel i pól. Powyższy przykład można bezpośrednio użyć np. z tabelą przedstawioną na poniższym diagramie:

model

Kod SQL do utworzenia takiej tabeli znajdziesz poniżej:

CREATE TABLE IF NOT EXISTS `users` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NOT NULL,
  `password` VARCHAR(63) NOT NULL,
  `enabled` TINYINT(1) NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`))
ENGINE = InnoDB

Tak skonfigurowana aplikacja powinna pozwolić na logowanie się za pomocą nazwy użytkownika i hasła zapisanych w bazie danych.

PS. Oczywiście musisz jeszcze dodać rekord do bazy danych, możesz do tego użyć poniższego zapytania SQL:

INSERT INTO users (`username`, `password`, `enabled`) VALUES ('kobietydokodu', 'jakieshaslo', 1);

Podsumowanie

W tej lekcji nauczyliśmy się dodawać Spring Security do naszego projektu – w kolejnej części będziemy zabezpieczać poszczególne czynności w naszej aplikacji, korzystać z informacji o zalogowanym użytkowniku i wyświetlać treści w zależności od tego, kim jest zalogowana osoba. Dodamy też własny formularz logowania zamiast domyślnego.

Licencja Creative Commons

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!

  •  
  •  
  •  
  •  
  •  
  • gosc

    Dobrze by było, jakbyśmy zobaczyli bazę danych pasującą do danych ustawień, ja mam kilka problemów w zw. z sql db.

    • Cześć,
      zaktualizowaliśmy lekcje – znajdziesz w niej teraz przykładowy kod SQL i model tej tabeli.
      Pozdrawiamy!

  • piotrowicki

    Czy jest możliwość dostosowaniu zapytania na własne? Rozumiem że komponując zapytanie „SELECT email, password FROM users where email=?” formularz odwołujący się do „/login” powinien zawierać pola email i password. Jednak ten sposob nie działa, jak to zdebugować?

    • Nazwy pól w formularzu nie mają znaczenia, jeśli chodzi o zapytania SQL. Przykład podawania własnych zapytań możesz znaleźć powyżej, na przykładzie poniższego:

      SELECT username, password, enabled FROM users WHERE username=?

      Co ważne, nasze zapytanie musi zwracać 3 kolumny (i tylko jeden wiersz). Jeśli nie masz w swojej bazie danych kolumny ‚enabled’, możesz ją zastąpić stałą wartością, np:

      SELECT email, password, 1 FROM users WHERE email=?

      Jeśli chodzi o debugowanie to można to zrobić standardowo (zerknij do lekcji #26: http://kobietydokodu.pl/26-debugowanie-aplikacji/ ), ustawiając breakpointy na klasach Spring’a (np. http://docs.spring.io/autorepo/docs/spring-security/current/apidocs/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.html). Szybszą i prawdopodobnie łatwiejszą na początku metodą może być jednak analiza logów – SpringSecurity wypluwa logi także na poziomie Debug, można dość czytelnie zobaczyć, co się tam dzieje.

  • Manio

    „Pier­wszym krok­iem jest prze­niesie­nie połączenia z bazą danych z
    pliku applicationContext.xml do pliku security-context.xml . Przenosimy
    więc cały poniższy tag z jed­nego pliku do drugiego.”

    Czyli to ma być w pliku security-context czy applicationContext?

    • Wcześniej te informacje były w applicationContext. Teraz musimy przenieść je do pliku security-context, dzięki czemu będą ‚widocze’ w obu miejscach. Oba pliki są jednak wymagane.

      • Manio

        Hmmm, ok dzięki.. Coś nie bardzo mogę przenieść tagu pomiędzy . Coś robię źle?

        • tag … powinieneś przenosząc zamienić na …

          Jest to związane z zapisem XMLowym, a dokładniej przestrzeniami nazw (namespace, lub też xmlns).

          Zwróć uwagę na poniższe dwa fragmenty:
          applicationContext.xml:

          oraz security-context.xml:

          xmlns:abcde=”http://url” mówi o tym, że przedrostek ‚abcde’ jest powiązany z adresem URL ‚http://url’ . Z kolei zapis xmlns=”http://url2″ mówi, że domyślny przedrostek (czyli jego brak) jest powiązany z ‚http://url2’ . Odnosząc to do powyższych przykładów, w pliku applicationContext, domyślnym url’em (przestrzenią nazw) jest http://www.springframework.org/schema/beans, natomiast w pliku security-context.xml, ta sama przestrzeń nazw jest zapisana jako xmlns:beans=”http://www.springframework.org/schema/beans” . Oznacza to, że przenosząc dowolny tag z applicationContext.xml do security-context.xml, jeśli ten tag nie miał przedrostka, musimy dodać przedrostek ‚beans:’ – to dlatego … staje się … (dlatego też ten plik zaczyna się tagiem zamiast po prostu ).

          Zdaje sobię sprawę, że troche to zawiłe, ale mam nadzieję, że przybliżyłem trochę skąd takie, a nie inne zachowanie.

  • Arek

    Hej, ćwiczę sobie Springa (używam najnowszej możliwej wersji, w chwili pisania komentarza jest to 4.2.3.RELEASE oraz 4.0.3.RELEASE, problem jaki napotkałem tu to error 404 przy próbie wejścia w moja_aplikacja/spring_security_login. Naprawdę nie miałem pojęcia o co chodzi, okazało się że poprawnym adresem jest moja_aplikacja/login (miałem ustawioną konsolę w log4.j na error i nie widziałem żadnych informacji, po przełączeniu na info zobaczyłem o co chodzi). Czy jest jakieś miejsce gdzie mogę sobie takie różnice między wersjami springa wychwycić ? lub czy macie jakieś rady odnośnie tego jak unikać podobnych zmartwień w przyszłości ? (Różnice między wersjami chociażby). Pozdrawiam !

  • Dawid

    Mam problem z drugim zapytaniem z pliku security-context.xml, podczas próby logowania pojawia się następujący komunikat „Reason: PreparedStatementCallback; SQL [SELECT ‚ROLE_USER’ FROM users WHERE username = ?]; Column Index out of range, 2 > 1. ; nested exception is java.sql.SQLException: Column Index out of range, 2 > 1”. O co może chodzić i w jaki sposób to naprawić? Używam Springa w wersji 4.2.3 natomiast Spring Security w wersji 4.0.3.

    • Do lekcji zakradł się drobny błąd – zapytanie powinno brzmieć ‚SELECT username, ‚ROLE_USER’ FROM users WHERE username = ?’ (dodana pierwsza kolumna; ta zmiana powinna trafić do pliku security-context.xml, do tagu ).
      Sam błąd oznacza tyle, że Spring próbuje pobrać drugą kolumnę, podczas gdy tylko jedna była dostępna (stąd 2>1).
      Przepraszamy za błąd i pozdrawiamy!

  • MILO

    Cały czas dostaję error org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named ‚springSecurityFilterChain’ is defined Zrobiłem tak jak napisaliscie w web.xml zmieniłem na /WEB-INF/classes/security-context.xml i dodałem filtr oraz utowrzyłem plik security-context jaka może być przeczyna? Przy okazji mam pytanie. Skąd spring ma wiedzieć ze applicationContex tez istnieje przecież w nim mamy zdefiniwany dispatcherservlet itp?

    • MILO

      Udało mi się samemu rozwiązać problem ;) Jeśli ktoś miałby podobny to polecam zobaczyć czy wszystkie wasze dependency się zgadzają w moim przypadku Java Servlet API było zbyt nowe.