Skip to content
Tags

, ,

Spring Security – autoryzacja w aplikacji internetowej

przez Łukasz Picur - Styczeń 6th, 2011

W ostatnim wpisie traktującym o Spring Security poruszyłem temat uwierzytelniania, czyli weryfikacji tożsamości użytkownika na podstawie podanych przez niego informacji. Szczegółowo przedstawiłem proces implementacji tej funkcjonalności we własnej aplikacji, oraz poruszyłem parę kwestii dotyczących bezpieczeństwa przechowywanych w bazie danych haseł. To wszystko było jednak wstępem do tego, co na prawdę chcemy osiągnąć – kontroli dostępu do różnych obszarów aplikacji na podstawie przyznanych użytkownikowi w czasie uwierzytelniania uprawnień (granted authorities). Jest to proces tzw. autoryzacji użytkownika, i to jemu poświęcony jest ten artykuł.

Na czym polega autoryzacja?

Załóżmy, że w naszej aplikacji mamy kilka kontrolerów – jeden wyświetla formularz pozwalający na dodanie nowego wpisu, drugi zapisuje go w bazie, a trzeci wyświetla stronę z najnowszą treścią. Ostatni, czwarty, dodaje wpis do zbioru ulubionych wpisów danego użytkownika. Naturalnie nie chcemy, żeby każdy zalogowany użytkownik miał możliwość dodawania nowej zawartości. Podobnie tylko zalogowani użytkownicy powinni móc oznaczyć wpisy jako ich ulubione. Tutaj do akcji wkracza mechanizm autoryzacji. Spring Security pozwala na skojarzenie w prosty sposób adresów dowolnych zasobów naszej witryny z uprawnieniami, które są wymagane do uzyskania dostępu do nich.

Jakich uprawnień potrzebujemy?

W naszym przykładzie potrzebujemy jednego uprawnienia, ADD_NEWS, pozwalającego użytkownikowi na dodanie nowego wpisu. Powinno być ono wymagane przy próbie zapisu nowej treści oraz wyświetlenia formularzu do tego służącego. Możliwość “polubienia” dowolnego wpisu dajemy każdemu – warunkiem jest jedynie bycie zalogowanym. Bez jakichkolwiek ograniczeń powinny być natomiast serwowane zasoby takie jak obrazki, style, strona z formularzem do logowania i wybrane podstrony aplikacji (chyba, że jej specyfika narzuca co innego). Warto także pamiętać o wyświetleniu użytkownikowi, który próbuje dostać się do zasobu, do którego nie ma uprawnienia, strony z odpowiednią informacją. Wiedząc jakich uprawnień w aplikacji potrzebujemy i kiedy są one wymagane, możemy przystąpić do wprowadzenia niebędnych zmian w przykładowej konfiguracji Spring Security z poprzedniego wpisu. Najpierw jednak warto zapoznać się z ogólnym schematem działania mechanizmu autoryzacji w Spring Security.

Jak Spring Security radzi sobie z autoryzacją?

Jak łatwo można się domyśleć, za autoryzowanie żądań odpowiada jeden z filtrów Spring Security - FilterSecurityInterceptor. Konfigurujemy go poprzez zmianę stworzonego wcześniej na potrzeby uwierzytelniania pliku security.xml. Wspomniany filtr wywoływany jest już po uwierzytelnieniu użytkownika, skutkiem czego ma on dostęp do przyznanych mu uprawnień (List<GrantedAuthority>). Informacje te, oraz wiele innych jak np. adres żądanego zasobu, czy IP użytkownika, przekazywane są do klasy implementującej interfejs AccessDecisionManager. Domyślna implementacja, zupełnie wystarczająca w większości przypadków, działa na zasadzie agregowania obiektów typu AccessDecisionVoter. Każdy z nich ma prawo „głosowania” (TAK / NIE), bądź wstrzymania się od głosu. Na podstawie wyników głosowania dostęp do zasobu jest przyznawany lub nie. To, ile głosów pozytywnych jest wymagane do udzielenia dostępu zależy od wybranej implementacji interfejsu AccessDecisionManager. Spring Security dostarcza trzy, których możemy swobodnie użyć we własnej aplikacji:

  • AffirmativeBased - stosowany domyślnie. Przyznaje dostęp do zasobu, jeśli co najmniej jeden voter oddał głos na TAK.
  • ConsensusBased - prawo dostępu jest udzielane na zasadzie głosu większości.
  • UnanimousBased - wymagane są wszystkie głosy na TAK, aby dostęp do zasobu był przyznany.

Sposób zmiany domyślnej implementacji interfejsu AccessDecisionManagerAffirmativeBased - na inny zostanie przedstawiony już za chwilę.

Konfigurujemy…

Istnieją dwie główne metody definiowania wymaganych uprawnień dla zasobów. Pierwszy sposób, mniej skomplikowany, polega na najzwyklejszym wypisaniu listy uprawnień oddzielonych przecinkami dla każdego adresu URL. Konfiguracja autoryzacji, uwzględniając zmianę access decision manager’a na obiekt klasy ConsensusBased, wygląda tak:

<http access-denied-page="/denied/" access-decision-manager-ref="consensusBased" >
    <intercept-url pattern="/login/" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <intercept-url pattern="/denied/" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <intercept-url pattern="/fail/" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <intercept-url pattern="/resources/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <intercept-url pattern="/add/" access="ADD_NEWS"/>
    <intercept-url pattern="/like/" access="IS_AUTHENTICATED_FULLY" />

    <form-login login-page="/login/" authentication-failure-url="/fail/"/>
    <logout logout-success-url="/"/>
</http>
<b:bean class="org.springframework.security.access.vote.ConsensusBased" id="consensusBased">
    <b:property name="decisionVoters">
    <b:list>
        <b:ref bean="roleVoter"/>
        <b:ref bean="authenticatedVoter"/>
    </b:list>
    </b:property>
</b:bean>

<b:bean class="org.springframework.security.access.vote.RoleVoter" id="roleVoter">
    <b:property name="rolePrefix" value="" />
</b:bean>

<b:bean class="org.springframework.security.access.vote.AuthenticatedVoter" id="authenticatedVoter"/>

Atrybut tagu http o nazwie access-denied-page pozwala na zdefiniowanie strony wyświetlanej użytkownikowi w przypadku braku niezbędnych uprawnień do żądanego zasobu. Wewnątrz tego tagu umieszczamy także listę tagów intercept-url, z których każdy odpowiada pewnemu zasobowi dostępnemu pod adresem podanym jako atrybut pattern. Podając adresy możemy oczywiście używać wildcard’ów. Drugi z atrybutów, access, zawiera listę oddzielonych przecinkami uprawnień wymaganych przez dany zasób. Jak widać powyżej, do naszej dyspozycji mamy także kilka predefiniowanych wartości o następujących znaczeniach:

IS_AUTHENTICATED_FULLY Pozwala na sprawdzenie, czy użytkownik został uwierzytelniony przez podanie pełnego zestawu wymaganych informacji.
IS_AUTHENTICATED_REMEMBERED Pozwala na sprawdzenie, czy użytkownik został uwierzytelniony przez mechanizm remember-me.
IS_AUTHENTICATED_ANONYMOUSLY Pozwala na sprawdzenie, czy użytkownik jest anonimowy.

W przykładzie dokonaliśmy dodatkowo zmiany access decision manager’a na inny. Czynność ta wiąże się z zadeklarowaniem bean’a, którego chcemy użyć w tej roli (u nas jest to bean o nazwie consensusBased), a następnie z podaniem tej nazwy jako wartości atrybutu access-decision-manager-ref tagu http. W konfiguracji wspomnianego bean’a musimy własnoręcznie ustawić listę niezbędnych voter’ów. W normalnych okolicznościach dodawane są one automatycznie, jednak przy ręcznej konfiguracji obowiązek ten spada na nas. W przykładzie dodane zostały jedynie vote’ry domyślne, więc uzyskane zachowanie nie będzie różnić się od standardowego. Warto także zwrócić uwagę na konfigurację bean’a o nazwie roleVoter. Atrybut rolePrefix został ustawiony na pusty ciąg znaków, aby można było używać dowolnych nazw uprawnień. Domyślnie prefix ten ma wartość „ROLE_”, co może skutkować powstaniem trudnych do znalezienia błędów. Niestety nie ma róży bez kolców – przez swoją prostotę ta metoda kontroli dostępu do zasobów ma dość ograniczone możliwości. Nie możemy tworzyć np. wyrażeń logicznych postaci

(POSIADA(UPR_1) OR POSIADA(UPR_2)) AND NIE_POSIADA(UPR_3)

Taką oraz wiele innych opcji daje sposób nr 2 – użycie  języka skryptowego SpEL (Spring Expression Language). Prócz oczywistych zalet, takich jak np. różnego rodzaju operatory, oferuje on szereg predefiniowanych metod znacznie upraszczających kontrolę uprawnień. Jeśli pójdziemy tą drogą, konfiguracja wyglądać będzie jak poniżej. W celu zwiększenia czytelności pozostawiony został domyślny access decision manager.

<http use-expressions="true" access-denied-page="/denied/" >
    <intercept-url pattern="/login/" access="permitAll" />
    <intercept-url pattern="/denied/" access="permitAll" />
    <intercept-url pattern="/fail/" access="permitAll" />
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/add/" access="hasRole('ADD_NEWS') AND !hasRole('SOME_OTHER_PERMISSION')"/>
    <intercept-url pattern="/like/" access="authenticated" />

    <form-login login-page="/login/" authentication-failure-url="/fail/"/>
    <logout logout-success-url="/"/>
</http>

Pierwsza zmiana względem sposobu pierwszego, jaka rzuca się w oczy, to dodatkowy atrybut tagu http o nazwie use-expressions ustawiony na true. Zabieg ten powoduje zamianę pary domyślnych voter’ów na obiekt klasy WebExpressionVoter, potrafiący interpretować wyrażenia języka SpEL. Inna jest też zawartość atrybutów access. To właśnie w nich umieszczamy wspomniane wyrażenia, które to zwracając wartość typu logicznego decydują o przydzieleniu bądź odmowie dostępu. Na podstawie przykładu widać, jak prosto możemy budować dowolnie skomplikowane wyrażenia logiczne używając operatora negacji, koniunkcji, alternatywy i gotowych metod dostarczonych wraz z frameworkiem. Poniżej można znaleźć ich listę wraz z krótkim opisem działania. Warto pamiętać, iż w przypadku metod bez parametrów możemy pominąć nawiasy.

hasIpAddress(’192.168.10.10′) Pozwala na sprawdzenie zgodności IP użytkownika z podaną wartością.
hasRole(‘ADD_NEWS’) Pozwala na sprawdzenie, czy użytkownik posiada podane uprawnienie.
hasAnyRole(‘PERM_1′, ‘PERM_2′) Pozwala na sprawdzenie, czy użytkownik posiada którekolwiek z podanych uprawnień.
permitAll() Zawsze udziela dostępu.
denyAll() Zawsze odmawia dostępu.
anonymous() Pozwala na sprawdzenie, czy użytkownik jest anonimowy.
authenticated() Pozwala na sprawdzenie, czy użytkownik jest uwierzytelniony.
rememberMe() Pozwala na sprawdzenie, czy użytkownik został uwierzytelniony przez mechanizm remember-me.
fullyAuthenticated() Pozwala na sprawdzenie, czy użytkownik został uwierzytelniony przez podanie pełnego zestawu wymaganych informacji.

Podsumowanie

Artykuł ten kończy serię dwóch wpisów poświęconych podstawom Spring Security. Mam nadzieję, że po ich przeczytaniu każdy będzie w stanie wzbogacić swoją aplikację o uwierzytelnianie użytkowników i kontrolę dostępu do zasobów. Nie należy jednak sądzić, że na tym kończą się zagadnienia związane z bezpieczeństwem realizowanym przy użyciu opisywanego frameworka. Jest dokładnie odwrotnie, dlatego też możecie być pewni, iż temat ten szybko powróci na javablog.eu. Do usłyszenia!

Kategoria → Bezpieczeństwo, Java, Web

Zostaw komentarz

Info: XHTML jest dozwolony. Twój adres email nigdy nie będzie opublikowany.

Obserwuj komentarze poprzez RSS