wtorek, 18 maja 2010

Spring Framework 3.0 Tutorial – cz 3 – spring security

W drugiej części tutorialu udało nam się stworzyć mechanizm dodawania administratorów do naszego panelu, byłoby nierozsądne by każdy użytkownik miał do niego dostęp, dlatego w tej części zajmiemy się mechanizmem kontroli dostępu do naszej aplikacji. Wpis obejmie konfigurację mechanizmów uwierzytelniania oraz autoryzacji wykorzystujących Spring Security (w tym hasła użytkowników zakodowane algorytmem sha256 + z wykorzystaniem tzw. soli). Miało być też coś o Sitemeshu, ale zrobię to w następnym odcinku który pojawi się na dniach.

Zabezpieczanie aplikacji


Konfiguracja zabezpieczeń obejmuje dwa pliki, web-security.xml oraz web.xml. Pierwszy z nich zawiera już konfigurację obiektu szyfrującego hasła oraz mechanizm soli (z wcześniejszego tutorialu), tym razem musimy dodać obsługę poziomów dostępu oraz mechanizmu uwierzytelniania opartego o bazę danych. Konfiguracja taka prócz skonfigurowanych już wcześniej obiektów zawierać powinna manager uwierzytelniania, dostawcę danych uwierzytelnianych, filtr przechwytujący, oraz listę reguł które będą mówić mechanizmowi, kto gdzie ma dostęp i ew. gdzie ma się zalogować. Zmiany w tym pliku wyglądają następująco

(pełna zawartość pliku w repozytorium – zobacz koniec wpisu):

<authentication-manager alias="authenticationManager">
<authentication-provider ref="authProvider" />
</authentication-manager>

<beans:bean id="authProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="userService" />
<beans:property name="passwordEncoder" ref="passwordEncoder" />
<beans:property name="saltSource" ref="saltSource" />
<beans:property name="includeDetailsObject" value="true" />
</beans:bean>

<beans:bean id="authenticationProcessingFilterEntryPoint"
class="org.springframework.security.web.authentication.AuthenticationProcessingFilterEntryPoint">
<beans:property name="forceHttps" value="false" />
<beans:property name="loginFormUrl" value="/login.html" />
</beans:bean>

<http entry-point-ref="authenticationProcessingFilterEntryPoint" auto-config="true">
<!-- <intercept-url pattern="/**" access="ROLE_ADMIN" /> -->
<intercept-url pattern="/login.html" filters="none" />
<logout logout-url="/logout.html" />
<anonymous granted-authority="ROLE_ANONYMOUS" />
<form-login login-page="/login.html" login-processing-url="/j_spring_security_check.html"
authentication-failure-url="/login.html?error=true" />
</http>

Czyli:

authentication-manager: mechanizm odpowiedzialny za uwierzytelnianie, przekazujemy mu referencje do obiektu odpowiedzialnego za dostarczenie informacji o użytkownikach

authProvider: obiekt odpowiedzialny jest za dostarczenie użytkowników (tj, pobranie użytkownika na podstawie loginu), w naszym wypadku obiekt korzysta z bazy danych oraz dodatkowych obiektów szyfrujących. authProvider posiada również parametr userDetailsService który powinien wskazywać na obiekt odpowiedzialny za wyszukanie użytkownika, obiekt taki wskazujemy poprzez nadanie mu id userService (patrz web-data.xml).

authenticationProcessingFilterEntryPoint: obiekt rozpoczynający proces autoryzacji, przechowuje ścieżkę (w formie URL-a) do strony z formularzem logowania

http: najważniejsza część, tutaj ustawiamy poziomy dostępów do konkretnych zasobów, ustalamy również formularza logowania oraz adres powodujący wylogowanie. (UWAGA! jeśli ustalamy niestandardowy adres logowania należy wyłączyć mu poziom dostepu (filter="none") inaczej dostaniemy pętle przekierowań)

Według powyższej konfiguracji nasza aplikacja jest dostępna tylko dla użytkowników z poziomem dostępu ROLE_ADMIN, jeśli nie jesteś zalogowany zostajesz automatycznie przekierowany na stronę /login.html która wyświetla formularz i wysyła go pod adres /j_spring_security_check.html, jeśli logowanie się nie powiedzie, do adresu logowania zostanie dopisany parametr error=true, gdy natomiast podasz poprawne dane logujące zostaniesz przeniesiony na wcześniej żądaną stronę, lub na stronę główną serwisu. Aby się wylogować musisz wejść pod adres /logout.html.

W pliku web.xml ustawiamy filtr który każde żądanie kończące się rozszerzeniem .html będzie "testował" pod względem bezpieczeństwa (tzn. czy użytkownik wywołujący żądanie ma prawo je wykonać), zdefiniowany filtr wygląda tak (pełna zawartość pliku w repozytorium – zobacz koniec wpisu):

<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>*.html</url-pattern>
</filter-mapping>

Jak wspomniałem wcześniej gdy użytkownik nie ma praw do wykonania danego żądania, bądź nie jest zalogowany zostaje automatycznie przekierowany do strony loguj.html. Ponieważ nie jest to standardowa strona musimy sami obsłużyć to żądanie, na szczęście nie jest to trudne, wymaga tylko 2 elementów, metody w kontrolerze która obsłuży żądanie oraz widoku formularza który wyświetli pola do podania loginu i hasła. Akcja w kontrolerze dodatkowo przyjmie parametr error (o czym mówiłem wcześniej) który posłuży do wykrycia czy akcja logowania się nie powiodła i jeśli jest to prawdą wyświetli odpowiedni komunikat.

Metodę obsługującą logowanie zawarłem w kontrolerze IndexController i wygląda ona następująco (pełna zawartość pliku w repozytorium – zobacz koniec wpisu):

@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(@RequestParam(value = "error", required = false, defaultValue = "false") boolean error) {
ModelAndView mav = new ModelAndView("index/login");
mav.addObject("isError", error);
return mav;
}

@RequestParam pobiera parametr przekazywany w żądaniu i przekazuje go w zmiennej error, parametr ten nie jest wymagany, więc jeśli nie istnieje do zmiennej error zostanie przypisana wartość domyślna false. Parametr error przekazywany jest do widoku który prezentuje się następująco:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<%@ taglib prefix="tag" uri="http://www.springframework.org/tags" %>
<div></div>
<div>
<form action="<c:url value=">
" method="POST">
<div>
<label for="login"></label>
<input id="login" name="j_username" /></div>
<div>
<label for="password"></label>
<input id="password" name="j_password" type="password" /></div>
<div>
<input type="submit" /></div>
</form></div>

Jak widzimy powyżej, najpierw sprawdzamy czy zmienna isError jest równa true co by oznaczało, że podaliśmy błędne dane podczas logowania, jeśli warunek jest spełnony zostaje wyświetlony komunikat. Następnie tworzymy formularz który metodą POST przesyła dane pod adres /j_spring_security_check.html który jak pamiętacie ustawiliśmy wcześniej w pliku web-security.xml. Formularz musi zawierać przynajmniej dwa pola (login i hasło) które dodatkowo powinny przyjmować odpowiednie nazwy: j_username dla nazwy użytkownika oraz j_password dla jego hasła.

Byłoby wspaniale gdyby na tym zakończyła się konfiguracja, niestety musimy zmodyfikować klasę encji Account by mogła być poprawnie przetwarzana przez Spring Security. Aby nasza klasa była poprawnie obsługiwana musi rozszerzać klasę User (z pakietu org.springframework.security.core.userdetails) oraz implementować interfejs UserDetails (z tego samego pakietu).
W naszym wypadku implementacja wygląda następująco (pełna zawartość pliku w repozytorium – zobacz koniec wpisu):

public class Account extends User implements UserDetails {

@Override
public Collection getAuthorities() {
Set ga = new HashSet();
for (AccountRole ar : this.accountRole) {
ga.add(new GrantedAuthorityImpl(ar.getRole()));
}
return ga;
}
<pre> @Override
public boolean isEnabled() {
if (this.accountRole.size() > 0) {
return true;
}
return false;
}</pre>
@Override
public boolean isAccountNonExpired() {
return this.isEnabled();
}

@Override
public boolean isAccountNonLocked() {
return this.isEnabled();
}

@Override
public boolean isCredentialsNonExpired() {
return this.isEnabled();
}

}


Zostały zaimplementowane metody:

public Collection<GrantedAuthority> getAuthorities() - zwraca listę poziomów dostępu jakie posiada użytkownik, czyli nasze AccountRole,
public boolean isEnabled() - zwraca true jeśli konto jest aktywne, w naszym wypadku kierujemy się zasadą "Jeśli użytkownik ma przypisaną rolę to konto jest aktywne",
public boolean isAccountNonExpired() - zwraca true jeśli konto nie wygasło,
public boolean isAccountNonLocked() - zwraca true jeśli konto nie jest zablokowane,
public boolean isCredentialsNonExpired() - zwraca true jeśli poziomy dostępów nie wygasły

Musimy również dodać metodę loadUserByUsername która będzie wykorzystywana przez dostawcę autoryzacji do znalezienia użytkownika. Funkcja ta wykorzystuje metodę findUsername z klasy AccountDao którą wykorzystujemy podczas rejestracji, i prezentuje się ona następująco (pełna zawartość pliku w repozytorium – zobacz koniec wpisu):

@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
try {
return (UserDetails) accountDao.findUsername(username);
} catch (UserNotFoundException e) {
throw new UsernameNotFoundException(username);
}
}


Metodę tą dodajemy do obiektu który jest wskazany w obiekcie authProvider jako parametr userDetailsService, w naszym wypadku było to dodanie identyfikatora userService do obiektu AccountServiceImpl w pliku web-data.xml (pełna zawartość pliku w repozytorium – zobacz koniec wpisu):

<bean id="userService" class="com.darekzon.bookstore.service.AccountServiceImpl" />

I to wszystko, tak skonfigurowany mechanizm powinien działać poprawnie, pamiętajcie tylko, żeby włączyć zabezpieczenia po dodaniu użytkownika.

Jak pobrać projekt


Projekt został umieszczony na serwerach GitHub.com. Adres bezpośredni do projektu: https://github.com/darek/bookstore.
Dodatkowo zostało uruchomione repozytorium GIT dla wszystkich chętnych i dostępny jest pod adresem: git://github.com/darek/bookstore.git

9 komentarzy:

  1. [...] http://darekzon.com/2010/05/spring-framework-3-0-tutorial-%E2%80%93-cz-3-%E2%80%93-spring-security [...]

    OdpowiedzUsuń
  2. Hej, chyba wcięło Ci dwa posty o tutorialu springa

    OdpowiedzUsuń
  3. Witam,
    korzystałem z Twojego tutoriala ale mam problem z authProviderem. W bazie danych mam szyfrowane hasła i mimo że mam dodanego passwordEncoder i saltSource do authProvidera to nie chce się zalogować mimo że hasło jest poprawne. Gdy usunąłem passwordEncoder i saltSource oraz podawałem na sztywno niezakodowane hasło to logował bez problemów. Wiesz może czemu tak się dzieje? Masz jakieś pomysły? Pozdrawiam.

    OdpowiedzUsuń
  4. Ciężko mi powiedzieć, sprawdź długość pola w bazie danych, możliwe, że nie zapisuje się cały zakodowany ciąg (zostaje ucięty) przez co przy sprawdzaniu wychodzą inne ciągi

    OdpowiedzUsuń
  5. Super tutorial fajnie, że ci się chciało to wszystko opisać :)

    Hmm a jak zasłonić przed niezalogowanym użytkownikiem część strony np. pojedynczy div ?

    OdpowiedzUsuń
  6. Ok już się z tym problemem uporałem:

    spring-security-taglibs

    http://static.springsource.org/spring-security/site/docs/3.1.x/reference/taglibs.html

    A i tak btw niektóre property są już deprecated w 3.1
    np.

    A i miałem podobny problem do kolegi wyżej z kodowaniem hasła.
    Rozwiązałem zmieniając PasswordEncoder na StandardPasswordEncoder.

    Przydatny link na SackOverflow:

    http://stackoverflow.com/questions/7658853/spring-security-custom-authentication-and-password-encoding

    Pozdrawiam

    OdpowiedzUsuń
  7. Możliwe, że niektóre property są deprecated, sam projekt jest już bardzo stary, teraz zamiast jednego dużego wolę napisać kilka mniejszych, łatwiej się to objaśnia, i ew. utrzymuje

    OdpowiedzUsuń
  8. Używasz tagów springa, z tego co pamiętam userInRole, czy coś takiego, zobacz link:

    http://static.springsource.org/spring-security/site/petclinic-tutorial.html

    OdpowiedzUsuń