poniedziałek, 8 grudnia 2008

Bezpieczeństwo szybko, łatwo i przyjemnie czyli wstęp do Spring Security

W znakomitej większości aplikacji internetowych mamy jakiegoś użytkownika, mamy jakieś konto... Gdzieś się rejestrujemy, gdzieś się logujemy i hulaj dusza po serwisie. Podczas projektowania tychże aplikacji, pojawia się dylemat: czy skorzystać z gotowego rozwiązania, czy zaimplementować uwierzytelnianie i autoryzację samemu? Co będzie łatwiejsze, co będzie bezpieczniejsze, co będzie lepsze? Jeśli gotowe rozwiązanie, to jakie? i na co mi taka armata na muchę?
Trudne pytania. Ja na nie odpowiedź mam jednak gotową: Spring Security.

Niezależny od wykorzystywanego szkieletu aplikacji, posiadający ogromne możliwości a przy tym prosty w użyciu.

No ale dość tych peanów. Zobaczmy jak to wygląda w akcji. Kilka tygodni temu opisywałem sposób na uprowadzenie sesji HTTP. Zobaczmy, jak na przykładzie przedstawionej tam aplikacji JSF wprowadzić do projektu Spring Security.

Najpierw trzeba dodać garść bibliotek:
aopalliance-1.0.jar, spring-context-2.0.8.jar, spring-security-core-tiger-2.0.4.jar, aspectjrt-1.5.4.jar, spring-core-2.0.8.jar, spring-web-2.0.8.jar, spring-aop-2.0.8.jar, spring-dao-2.0.8.jar, spring-beans-2.0.8.jar, spring-security-core-2.0.4.jar

Wszystkie są do znalezienia w paczce Spring Security 2.0.4.

Jeżeli chcemy, aby obiekty napisanej przez nas klasy reprezentowały użytkowników aplikacji, klasa ta musi implementować interfejs org.springframework.security.userdetails.UserDetails która zawiera kilka metod, dzięki którym Spring Security orientuje się kto jest kto i co może zrobić w naszym systemie.

 
public interface UserDetails extends Serializable {
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
GrantedAuthority[] getAuthorities();

/**
* Returns the password used to authenticate the user. Cannot return <code>null</code>.
*
* @return the password (never <code>null</code>)
*/
String getPassword();

/**
* Returns the username used to authenticate the user. Cannot return <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
String getUsername();

/**
* Indicates whether the user's account has expired. An expired account cannot be authenticated.
*
* @return <code>true</code> if the user's account is valid (ie non-expired), <code>false</code> if no longer valid
* (ie expired)
*/
boolean isAccountNonExpired();

/**
* Indicates whether the user is locked or unlocked. A locked user cannot be authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
boolean isAccountNonLocked();

/**
* Indicates whether the user's credentials (password) has expired. Expired credentials prevent
* authentication.
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired), <code>false</code> if no longer
* valid (ie expired)
*/
boolean isCredentialsNonExpired();

/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
boolean isEnabled();
}


Na uwagę zasługuje metoda GrantedAuthority[] getAuthorities(). Zwraca ona tablicę obiektów, reprezentujących role.

Zmieniam zatem klasę pl.matt.model.User mojej aplikacji tak, aby implementowała ona wymieniony wyżej interfejs:

 
public class User implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 853438034988558585L;
private String login;
private String password;
private int balance;
private GrantedAuthority[] authorities = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_USER") };

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public int getBalance() {
return balance;
}

public void setBalance(int balance) {
this.balance = balance;
}

@Override
public GrantedAuthority[] getAuthorities() {
return authorities;
}

@Override
public String getUsername() {
return login;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

}


Do działania modelu bezpieczeństwa niezbędna będzie jeszcze jedna klasa usługowa, implementująca interfejs org.springframework.security.userdetails.UserDetailsService.

Zawiera on tylko jedną metodę:
 
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException;


która jak sama nazwa wskazuje, ładuje użytkownika o zadanej nazwie.

Implementacja tego interfejsu w mojej aplikacji to klasa UserDetailsServiceImpl

 
public class UserDetailsServiceImpl implements UserDetailsService {

private UserManager userManager = new UserManager();

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
User user = userManager.loadUser(username);
if (user != null) {
return user;
}
throw new UsernameNotFoundException(username);
}

}


klasę UserManager pozostawiam bez zmian. Z tym, że poprzednio była ona ziarnem zarządzanym JSF. Teraz nie ma już takiej potrzeby.

Spring Security przychodzi z kilkoma implementacjami interfejsu UserDetailsServiceImpl, między innymi z implementacją korzystającą z użytkowników zapisanych w bazie danych lub przechowującą użytkowników w pamięci. Na potrzeby mojej aplikacji żadna z nich nie będzie przydatna.

Oprócz dwóch powyższych klas stworzyłem również klasę UserLoginBean umożliwiającą wyświetlenie zalogowanego użytkownika na stronie JSF.
 
public class UserLoginBean {

private User loggedUser;

public User getLoggedUser() {
if (loggedUser == null) {
HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
UsernamePasswordAuthenticationToken principal = (UsernamePasswordAuthenticationToken) request.getUserPrincipal();
loggedUser = (User) principal.getPrincipal();
}
return loggedUser;
}

}


UserLoginBean jest ziarnem zarządzanym JSF o zasięgu żądania. Dzięki temu na stronach JSF uzyskuję dostęp do zalogowanego użytkownika poprzez wyrażenie EL #{userLoginBean.loggedUser}
Oto cały faces-config.xml

 
<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
<managed-bean>
<managed-bean-name>userLoginBean</managed-bean-name>
<managed-bean-class>pl.matt.UserLoginBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<navigation-rule>
<from-view-id>/pages/login.xhtml</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/secure/home.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>faliture</from-outcome>
<to-view-id>/pages/login.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<application>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config>


No i to by było na tyle... gdyby nie potrzeba konfiguracji Spring Security. Dodaję zatem do pliku web.xml po jednym jeden filtrze, słuchaczu i parametrze konfiguracyjnym

 

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

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<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>


Wartość parametru contextConfigLocation wskazuje położenie pliku ze szczegułami konfiguracji Spring Security.
Tworzę zatem i plik /WEB-INF/applicationContext-security.xml


<?xml version="1.0" encoding="UTF-8"?>
<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-2.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">

<global-method-security secured-annotations="enabled">
</global-method-security>

<http auto-config="true">
<intercept-url pattern="/secure/**" access="ROLE_USER" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<form-login login-page="/pages/login.jsf" />
</http>

<beans:bean id="daoAuthenticationProvider"
class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="userDetailsService" />
<custom-authentication-provider />
</beans:bean>

<beans:bean id="userDetailsService" class="pl.matt.security.UserDetailsServiceImpl" />

</beans:beans>


W zasadzie na pierwszy rzut oka wiadomo o co chodzi.

 
<http auto-config="true">
<intercept-url pattern="/secure/**" access="ROLE_USER" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<form-login login-page="/pages/login.jsf" />
</http>


<http auto-config="true"> włącza predefiniowane ustawienia konfiguracyjne, z których zmieniam położenie formularza do logowania na /pages/login.jsf, oraz określam które urle będą dostępne dla użytkowników o określonych uprawnieniach. W moim przypadku strony zaczynające się od /secure/ będą widoczne tylko dla użytkowników o roli ROLE_USER.


W duecie daoAuthenticationProvider i userDetailsService
 
<beans:bean id="daoAuthenticationProvider"
class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="userDetailsService" />
<custom-authentication-provider />
</beans:bean>

<beans:bean id="userDetailsService" class="pl.matt.security.UserDetailsServiceImpl"

który generalnie wskazuje, której usługi będziemy używać do autentykacji użytkowników (można używać wielu usług jednocześnie) na uwagę zasługuje znacznik <custom-authentication-provider />. Imformuje on Spring Security, że nie będziemy używać domyślnej usługi przechowującej użytkowników w pamięci, ale naszej implementacji.

Zobaczmy jeszcze stronę logowania login.xhtml:
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">
<f:loadBundle basename="resources" var="msg" />
<ui:composition template="/templates/common.xhtml">
<ui:define name="pageTitle">Login form</ui:define>
<ui:define name="pageHeader">Proszę się zalogować</ui:define>
<ui:define name="body">
<form action="../j_spring_security_check">
<table>
<tr>
<td>User:</td>
<td><input type='text' name='j_username' /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='j_password' /></td>
</tr>
<tr>
<td><input type="checkbox" name="_spring_security_remember_me" /></td>
<td>Don't ask for my password for two weeks</td>
</tr>

<tr>
<td colspan='2'><input name="submit" type="submit" /></td>
</tr>
</table>
</form>
</ui:define>
</ui:composition>
</html>


Specjalna wartość parametru action formularza, oraz jego nazwy pól pozwalają się zorientować Spring Security, że podjęto próbę logowania.

Całą aplikację można pobrać tutaj.
Dla chętnych pozostawiam sprawdzenie odporności tej wersji na atak uprowadzenia sesji i inne ataki.

To by zatem było na tyle. Niewielkim nakładem pracy dodałem bardzo potężne narzędzie do zarządzania bezpieczeństwem aplikacji. Co prawda wykorzystałem tylko mały fragment jego możliwości, ale muszę jeszcze mieć o czym pisać na blogu...

7 komentarzy:

Tomasz Nurkiewicz pisze...

Świetne wprowadzenie do Spring Security (szkoda, że nie wspomniałeś, że jeszcze niedawno było to Acegi Security), dobry początek dla dalszych eksperymentów. Dzięki!

P.S.: 'połoŻenie' :-)

WooKasZ pisze...

Ciesze się, że nie zapomniałeś o mojej prośbie :)

Dziękuje bardzo:) Świetny wpis!

MZ pisze...

dzięki za uwagi. Orta już poprawiłem ;).

WooKasZ pisze...

dostaje dziwny błąd w klasie UserDetailsServiceImpl. Mianowicie kompilator czepia mi się wyjątku UsernameNotFoundException - "incompatible types" jest ten wyjątek a spodziewany był Throwable. Ponadto nie odnajduje deklaracji wyjątku DataAccessException.

Dołączyłem wszystkie biblioteki z linka podanego we wpisie.

Any ideas ?!

MZ pisze...

tak na szybko mi nic nie przychodzi. Wrzuć gdzieś na sieć projekt, postaram się zobaczyć, co tam nie gra.

WooKasZ pisze...

udało mi się z tym jakoś poradzić. Musiałem dołączyć spring-core.jar do projektu. Dzięki !

bzieba pisze...

Dzięki za wspaniały post, który u każdego złamie "barierę wejścia" w spring-security w 5 min! Well-done! :)

Bogumił