piątek, 26 grudnia 2008

Jazoon09 już w czerwcu, już w Zurychu

Na moim lokalnym, WJUGowym podwórku 2 razy do roku odbwają interesujące konferencje. Wiosenna Jawarsovia i jesienna Warsjawa. Na naszym mniej lokalnym krajowym podwórzu mamy do tego Java Developers' Day oraz nowość, międzynarodowy GeeCON (7-8 Maj). Mnie jak dotąd żadnej pozawarszawskiej konferencji nie udało się odwiedzić. Szkoda, wszak podróże kształcą.

Okazji do zmiany tego stanu rzeczy będzie w nadchodzącym roku co niemiara. Oprócz polskich wydarzeń rokrocznie na jawowym kalendarzu pojawia się kilka europejskich i światowych pozycji. Jedną z nich będzie Jazoon. Trzydniowa, czerwcowa konferencja w Zurychu. Byłem już w Zurychu i było całkiem fajnie. Nie od dziś wiadomo, że "jestem umysł ścisły. Mnie się podobają melodie, które już raz słyszałem." więc i miejsca które już widziałem powinny być warte uwagi. Zatem mam nadzieję pojawić się tamże pomiędzy 22 a 25 czerwca 2009 przy okazji konferencji Jazoon.

Niestety program Jazoon09 jest jeszcze wielką niewiadomą. Znanych jest tylko kilku pierwszych prezenterów: Neal Ford, Danny Coward, Roberto Chinnici, Jérôme Dochez. Pozostali powinni się zgłosić do 15 stycznia, więc pewnie niedługo poinformuję siebie i was bardziej szczegółowo.

Nie do końca bezinteresownie, bo mam za to szansę na darmową wejściówkę. Impreza niestety nie jest bezpłatna, uczestnictwo kosztuje 1450-1810 franków szwajcarskich.

Wybierasz się na Jazoon09? Może byłeś na jakiejś innej konferencji? Podziel się opinią w komentarzu!

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