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

czwartek, 27 listopada 2008

Się działo się... Warsjawa Eclipse DemoCamp 2008

Było minęło... ale, że było fajnie, opiszę pokrótce jak to miało miejsce.

Warsjawa Eclipse DemoCamp 2008, odbyła się w ubiegłą sobotę, 22 listopada na terenie wydziału MIMUW Uniwersytetu Warszawskiego. Swoją obecnością zaszczyciło nas ponad 150 słuchaczy, ale zacznijmy od początku (relacja z czuba i na żywo)...

a na początku był Waldemar Kot. Waldi po nieudanych inwestycjach w przemysł księgarski



powrócił w klimaty bliższe naszemu sercu. Jego prezentację dotyczącą mechanizmu Publish-Subscribe wysłuchała pełna sala.



Było jak zwykle u Waldiego. Ciekawie, fachowo i konkretnie. Po prostu rewelacja. Choć w sobotę rano nikt jeszcze w to nie wierzył, poziom widowiska nie opadł do sameog końca. Kolejna prelekcja "Taking SQL IDEs from the Stone Age to the 21st century" Wassima Mełhema tylko podsyciła apetyt.

Szczęście w nieszczęściu, że zaraz potem był czas na przerwę. Obiadową. Na każdego czekał kotlecik oraz zupka. Mniam...



To co zapowiadała pierwsza część spotkania w drugiej stało się faktem. Worek z bramkami się rozwiązał. Co prawda Eclipse RPC nie do końca zmieścił się w przeglądarce Jacek Pospychały, ale to był dopiero początek emocji. Telekomunikacja w Javie pełna była pełna ostrej walki bark w bark Tomasz Zieleniewskiego oraz Waldemara Kota. Na szczęście sędziowie byli stanowczy i konsekwentni. O żadnych zadymach nie mogło być zatem mowy. W dogrywce 2 krótsze prezentacje - Dojo Toolkit Łukasza Lenarta oraz Spring Dynamic Modules Agaty i Jacka Laskowskich i każdy mógł być zadowolony ze zwycięskiego remisu.

Tenże uczciliśmy w Lolku. Co prawda nikt się nie spodziewał tylu zwycięzców i miejsc brakło, ale cóż. Taka okazja nie trafia się na co dzień...



Było super. Pozostaje mi tylko mieć nadzieję, że to nie pierwszy i nie ostatni raz.

poniedziałek, 17 listopada 2008

Warsjawa Eclipse DemoCamp 2008

Już w tą sobotę, 22 listopada w Warszawie w budynku MIMUW przy ul. Banacha 2 w sali 5440 w godz. 9:00-17:00 odbędzie się konferencja Warsjawa Eclipse DemoCamp 2008 organizowana przez Warszawski JUG. Konferencja dotyczy tego, co w jawie piszczy, a dokładniej:

09:00 - 09:45 Rejestracja (w trakcie: kawa/herbata, stół szwedzki)
09:45 - 11:15 Comet, Bayeux i mechanizm Publish-Subscribe poprzez HTTP - Waldemar "waldi" Kot (Warszawa JUG)
11:30 - 13:00 Taking SQL IDEs from the Stone Age to the 21st century - Wassim Melhem (Eclipse Foundation)
13:00 - 14:00 Przerwa obiadowa (zarejestrowani bezpłatnie!)
14:00 - 14:45 Czy Eclipse RCP mieści się w przeglądarce? - Jacek Pospychała (IBM Eclipse Support Center)
14:50 - 15:35 Telekomunikacja w Javie - kilka słów o konwergencji i usługach w telekomunikacji - Tomasz Zieleniewski (TouK)
15:40 - 16:10 Dojo Toolkit - Łukasz Lenart (Warszawa JUG)
16:15 - 16:45 OSGi + Spring Framework = Spring Dynamic Modules - Jacek Laskowski (Warszawa JUG)
16:45 - 17:00 Losowanie nagrody głównej - Kapituła Warsjavy
17:00 Spotkania integracyjnego czas zacząć! - Kapituła Warsjavy

Zauważyliście pewnie kilka frykasów: stół szwedzki, obiadek, imprezka, tajemnicza nagroda główna... To dzięki naszym wspaniałym sponsorom:
7N, Javatech, e-point, Touk, Eclipse, JetBrains.

Konferencja jest całkowicie bezpłatna, pod warunkiem rejestracji. Osobiście będę sprawdzał listę obecności ;).

Więcej informacji na stronie warsjawa.pl

W imieniu organizatorów serdecznie zapraszam.

sobota, 15 listopada 2008

kolorowanie kodu źródłowego na blogu

Już od moich pierwszych spotkań z blogspotem brakowało mi możliwości ładnej prezentacji kodu źródłowego na jego stronach. Inne serwisy, takie jak wikidot.com pozostawiały (i pozostawiają) go daleko w tyle. W końcu jednak mi się udało coś na to poradzić...

Dokładnie nie mi, tylko twórcom projektu syntaxhighlighter. To narzędzie, to nic więcej jak zestaw skryptów JavaScript, które kolorują i ozdabiają kod źródłowy.

Aby skorzystać z tego wynalazku dołączyłem do swojego bloga dedykowany widget. Na stronie http://fazibear.googlepages.com/blogger.html kliknąłem w ikonkę Add to Blogger. Pasek tytułowy pozostawiłem pusty, dzięki temu widget pozostaje niewidoczny.

Jeśli chodzi o instalację skryptów syntaxhighlighter, to by było na tyle. Uźycie ich, jest równie proste. Kod źródłowy wystarczy otoczyć tagami pre lub textarea o parametrach name="code" class="kod-języka". Obsługiwane języki to:

język kod języka
C++ cpp, c, c++
C# c#, c-sharp, csharp
CSS css
Delphi delphi, pascal
Java java
Java Script js, jscript, javascript
PHP php
Python py, python
Ruby rb, ruby, rails, ror
Sql sql
VB vb, vb.net
XML/HTML xml, html, xhtml, xslt


efekt jest mniej więcej taki:



Niezłe... ale to jeszcze nie koniec. Takie bloki kodu można konfigurować. W tym celu kod języka należy uzupełnić opcjami rozdzielonymi dwukropkiem. Dostępnych jest 5 parametrów konfiguracyjnych:



nogutter Ukrywa boczny pasek z numerami linii.
nocontrols Ukrywa górne kontrolki.
collapse Automatycznie wcięcia.
firstline[value] Ustawia numer pierwszej linii value. Domyślna wartość to 1.
showcolumns Pokaże numery kolumn


i tak na przykład


<pre name="code" class="java:nocontrols:firstline[76]"">
public class Hello {
private static final String HELLO_MSG = "Dzień dobry, cześć i czołem!"
}
</pre>


wyświetli blok kodu bez kontrolek, w dodatku pierwsza linia kodu będzie miała numer 76.

No po prostu rewelacja. Niestety nie zupełnie. Syntaxhighlighter nie jest też pozbawiony bugów. Jeden z nich można podziwiać na przykładzie mojego poprzedniego posta "Uprowadzenie sesji". Prezentuję tam klasę:


public class UserManager {

private List users;

public UserManager() {
users = new ArrayList(3);
User user1 = new User();
user1.setLogin("user1");
user1.setPassword("pass1");
user1.setBalance(54321);
User user2 = new User();
user2.setLogin("user2");
user2.setPassword("pass2");
user2.setBalance(65354);
User user3 = new User();
user3.setLogin("user3");
user3.setPassword("pass3");
user3.setBalance(91254);
users.add(user1);
users.add(user2);
users.add(user3);
}

public boolean check(String login, String password) {
User user = loadUser(login);
return user != null && user.getPassword().equals(password);
}

public User loadUser(String login) {
for (User user : users) {
if (user.getLogin().equals(login)) {
return user;
}
}
return null;
}

public void setUsers(List users) {
this.users = users;
}
}


która po upiększeniu przez syntaxhighlighter wygląda tak:



public class UserManager {

private List users;

public UserManager() {
users = new ArrayList(3);
User user1 = new User();
user1.setLogin("user1");
user1.setPassword("pass1");
user1.setBalance(54321);
User user2 = new User();
user2.setLogin("user2");
user2.setPassword("pass2");
user2.setBalance(65354);
User user3 = new User();
user3.setLogin("user3");
user3.setPassword("pass3");
user3.setBalance(91254);
users.add(user1);
users.add(user2);
users.add(user3);
}

public boolean check(String login, String password) {
User user = loadUser(login);
return user != null && user.getPassword().equals(password);
}

public User loadUser(String login) {
for (User user : users) {
if (user.getLogin().equals(login)) {
return user;
}
}
return null;
}

public void setUsers(List users) {
this.users = users;
}
}


Skąd na końcu </user></user></user>?
Nie wiadomo...

wtorek, 4 listopada 2008

uprowadzenie sesji

Dawno nic nie napisałem ciekawego. Ostatnio same zawirowania. Do tego zainstalowałem StarCrafta na Ubuntu oraz znalazłem sposób na kolorowanie kodu w tym oto blogu. Wydaje mi się on teraz nieco bardziej czytelny. Pomału na szczęście wychodzę na prostą, przynajmniej jeśli chodzi o bloga...

Jako młode pachole, prawie każdy z nas chciał być strażakiem, policjantem, pilotem... Ja chciałem być pilotem, a najlepiej astronautą. Nic z tego niestety nie wyszło.

Jako starsze pachole, już nie każdy, ale przynajmniej niektórzy chcieli być hakerami. Łamać hasła, włamywać się do NASA, wykradać materiały o UFO. Wszystkich, którzy wciąż mają takie zamiary, uprzejmie informuję, że ich do tego broń boże nie zachęcam. Jednak jak coś już wykradniesz z NASA, pochwal się w komentarzu.

No ale do rzeczy. Dziś wpis z gatunku bezpieczeństwa. Pomysł nań nasunął mi się podczas rozważań co do mechanizmu logowania w prostej aplikacji WWW. Aplikacja prosta, więc możnaby autoryzację zaimplementować samemu. Można też użyć czegoś gotowego, np. SpringSecurity. Rozwiązanie to raczej pewne i sprawdzone, ale czy nie będzie to strzelanie armatą do muchy?

Rozważmy zatem pierwsze podejście.

Stworzyłem przykładową aplikację, która przechowuje poufne informacje o zarobkach pracowników. Na aplikację składają się 3 klasy:
User, która zawiera informacje o pracowniku

public class User {
private String login;
private String password;
private int balance;

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

}

UserManager to klasa, która umożliwia proste operacje na użytkownikach. W prawdziwej aplikacji klasa zapewne będzie ukryta za interfejsem, korzystając do tego z obiektów DAO i inne takie. Na potrzeby jednak naszej aplikacji poniższy menadżer wystarczy:

public class UserManager {

private List<User> users;

public UserManager() {
users = new ArrayList<User>(3);
User user1 = new User();
user1.setLogin("user1");
user1.setPassword("pass1");
user1.setBalance(54321);
User user2 = new User();
user2.setLogin("user2");
user2.setPassword("pass2");
user2.setBalance(65354);
User user3 = new User();
user3.setLogin("user3");
user3.setPassword("pass3");
user3.setBalance(91254);
users.add(user1);
users.add(user2);
users.add(user3);
}

public boolean check(String login, String password) {
User user = loadUser(login);
return user != null && user.getPassword().equals(password);
}

public User loadUser(String login) {
for (User user : users) {
if (user.getLogin().equals(login)) {
return user;
}
}
return null;
}

public void setUsers(List<User> users) {
this.users = users;
}
}

Pozostała jeszcze klasa UserLoginBean, która odpowiada za zalogowanie użytkownika oraz sprawdza czy i jaki użytkownik jest w danym momencie zalogowany.

public class UserLoginBean {
private String login;
private String password;
private User loggedUser;
private UserManager userManager;

public String login() {
if (userManager.check(login, password)) {
loggedUser = userManager.loadUser(login);
login = null;
password = null;
return "success";
}
return "faliture";

}

public String logout() {
userManager = null;
return null;
}

public boolean isUserLogged() {
return loggedUser != null;
}

public User getLoggedUser() {
return loggedUser;
}

public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}

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

Informacje o zalogowanym użytkowniku przechowywane są w sesji, stąd też taki zasięg ziarna UserLoginBean. Widać to w poniższym pliku 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>userManager</managed-bean-name>
<managed-bean-class>pl.matt.UserManager</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>userLoginBean</managed-bean-name>
<managed-bean-class>pl.matt.UserLoginBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>userManager</property-name>
<value>#{userManager}</value>
</managed-property>
</managed-bean>
<navigation-rule>
<from-view-id>/pages/login.xhtml</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/pages/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>

do kompletu aplikacji brakuje jeszcze pliku web.xml

<?xml version="1.0"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
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-app_2_5.xsd">
<display-name>sessionHijacktion</display-name>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<context-param>
<param-name>facelets.REFRESH_PERIOD</param-name>
<param-value>2</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
</web-app>

oraz kilku stronek JSF. Dwie pierwsze, nie są specjalnie ciekawe. Strona login.xhtml zawiera prosty formularz, umożliwiający zalogowanie użytkownika do aplikacji

<!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">
<h:message showSummary="true" showDetail="false" style="color: red; font-weight: bold;" for="name" />
<h:form id="helloForm">
<h:panelGrid columns="2">
<h:outputText value="login" />
<h:inputText required="true" id="name" value="#{userLoginBean.login}" />
<h:outputText value="password" />
<h:inputSecret required="true" id="password" value="#{userLoginBean.password}" />
<h:outputText value="" />
<h:commandButton id="submit" action="#{userLoginBean.login}" value="zaloguj" />
</h:panelGrid>
</h:form>
</ui:define>
</ui:composition>
</html>

Natomiast strona home.xhtml przedstawia poufne informacje o każdym z użytkowników

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

<f:loadBundle basename="resources" var="msg" />
<ui:composition template="/templates/common.xhtml">
<ui:param name="loggedOnly" value="true" />
<ui:define name="pageTitle">Greeting to User</ui:define>
<ui:define name="pageHeader">Greeting Page</ui:define>
<ui:define name="body">
#{msg.greeting} #{userLoginBean.loggedUser.login} masz na koncie #{userLoginBean.loggedUser.balance}
</ui:define>
</ui:composition>
</html>

Na uwagę zasługuje parametr

<ui:param name="loggedOnly" value="true" />

dzięki któremu treść strony home.xhtml jest widoczna tylko dla zalogowanych użytkowników.
Jak to działa? Wszystko staje się jasne, gdy obejrzymy plik /templates/common.xhtml zawierający szablon każdej strony aplikacji:

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

<f:loadBundle basename="resources" var="msg" />
<head>
<title><ui:insert name="pageTitle">Page Title</ui:insert></title>
</head>

<body bgcolor="#ffffff">
<table style="border:1px solid #CAD6E0" align="center" cellpadding="0" cellspacing="0" border="0" width="400">
<tbody>

<tr>
<td class="header" height="42" align="center" valign="middle" width="100%" bgcolor="#E4EBEB">
<ui:insert name="pageHeader">Page Header</ui:insert>
</td>
</tr>
<tr>
<td height="1" width="100%" bgcolor="#CAD6E0"></td>
</tr>

<tr>
<td width="100%" colspan="2">
<table width="100%" style="height:150px" align="left" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td align="center" width="100%" valign="middle">
<ui:fragment rendered="#{!loggedOnly || userLoginBean.userLogged}">
<ui:insert name="body">Page Body</ui:insert>
</ui:fragment>
<ui:fragment rendered="#{loggedOnly and !userLoginBean.userLogged}">
Proszę się zalogować
</ui:fragment>
</td>
</tr>
</tbody>
</table>
</td>
</tr>

<tr>
<td colspan="2" valign="bottom" height="1" width="100%" bgcolor="#CAD6E0"></td>
</tr>
</tbody>
</table>
</body>

</html>

a dokładnie jego fragment:

<ui:fragment rendered="#{!loggedOnly || userLoginBean.userLogged}">
<ui:insert name="body">Page Body</ui:insert>
</ui:fragment>
<ui:fragment rendered="#{loggedOnly and !userLoginBean.userLogged}">
Proszę się zalogować
</ui:fragment>

Jeżeli parametr loggedOnly ustawiony jest na wartość true, zawartość strony zostanie wyświetlona, jedynie zalogowanemu użytkownikowi.

Prosta aplikacja, proste zabezpieczenie, ale działają.
Czy na pewno?

Odpalam aplikację na serwerze JBoss 4.2.1.GA.
Wyłączam w przeglądarce obsługę ciasteczek. Próbuję się zalogować, ale nie znam hasła.


mogę się więc jedynie obejść smakiem:


Niby nic. Dla mojego żądania serwer otworzył nową sesję, a jej identyfikator dokleił do adresu URL. Nie ma się co dziwić, w końcu moja przeglądarka nie obsługuję ciasteczek.

Teraz, jako, że nie znam hasła, a chcę się zalogować, kopiuję URL z identyfikatorem sesji i przesyłam mojej ofierze. Oczywiście proszę ją podstępnie o zalogowanie się.


Ofiara moja grzecznie klika w URL i się loguje jako użytkownik "user3".

A ja jeszcze grzeczniej, zmieniam w moim URLu adres z login.jsf na home.jsf (zostawiam identyfikator sesji) i...



Bingo. Też jestem zalogowany!

Jak to się stało? Ano przesyłając adres z dopiętym identyfikatorem sesji, ofiara ataku zalogowała się do aplikacji korzystając mojej sesji. Ja następnie otworzyłem zabezpieczoną stronę, korzystając już z naszej wspólnej sesji, w której właśnie się ktoś zalogował.

Jak się przed takim atakiem bronić? Najlepszym rozwiązaniem jest przydzielanie nowej sesji użytkownikowi przy logowaniu. Tak robi właśnie SpringSecurity. Przy okazji kopiuje on z automatu wszystkie obiekty ze starej sesji, do nowej. Tak więc fakt całej operacji jest dla użytkownika jest niezauważalny.
Więcej o tym ataku znajdziesz na Wikipedii.
Co ciekawe, przesiadka na JBossa 4.2.2.GA uniemożliwia już ten atak, przynajmniej w takiej prostej formie, jak ta opisana powyżej.

Pozostaje tylko się domyślać, przed iloma jeszcze nieznanymi mi typami ataków chroni mnie i moją aplikację SpringSecurity.

czwartek, 16 października 2008

Moc dialektów Hibernate

Od czasu do czasu mam przyjemność przeprowadzania rozmów rekrutacyjnych z kandydatami do pracy. Osobom, które deklarują znajomość Oracle PL/SQLa i przebrną przez pierwsze pytanie dotyczące sekwencji (a 2/3 znawców PL/SQLa nie wie, co to jest sekwencja) zadaję następujące zadanko:

Przedstawiam encję Osoby:


i proszę o napisanie zapytania pokazującego 3 najmniej zarabiające osoby. Jeżeli kandydat zna również MySQL, proszę o napisanie tego zapytania w 2 wersjach. Bajer polega na tym, że w MySQLu do ograniczenia listy wyników do trzech wystarczy wykorzystać klauzulę limit:

SELECT * FROM OSOBY ORDER BY OSOBY.PENSJA LIMIT 3;


podczas gdy w Oracle takiej magicznej klauzuli nie ma. Jest tam za to możliwość wyświetlenia wierszy o określonym numerze (indeksie):

SELECT * FROM OSOBY WHERE NUMROW <= 3 ORDER BY OSOBY.PENSJA;

ale numer wiersza jest sprzed sortowania, zatem powyższe zapytanie wyświetli trzech pierwszych użytkowników posortowanych po wielkości pensji. Aby osiągnąć zamierzony cel, trzeba skorzystać z podzapytania:

SELECT * FROM (SELECT * FROM OSOBY OSOBY ORDER BY OSOBY.PENSJA ) WHERE NUMROW <= 3;

Zapytanie o to samo, a jednak inaczej skonstruowane (a niby SQL to język deklaratywny).
Zaczęło mnie zastanawiać, co na to Hibernate. Czy jest na tyle mądry, że potrafi to samo zapytanie HQLowe zapisać na 2 zupełnie różne sposoby? Czy jego dialekty, ograniczają się jedynie do podmiany słów kluczowych i nazw typów, czy są mechanizmem dużo potężniejszym? Przekonajmy się.

Na bazie aplikacji Hibernate Getting Started - Hello World stworzyłem prosty projekt w środowisku Eclipsie. Skasowałem pliki Message.java oraz Message.hbm.xml.

Stworzyłem za to klasę Person:

public class Person {
private Long id;
private String name;
private int salary;

public Person() {

}

public Person(String text, int salary) {
this.name = text;
this.salary = salary;
}

public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}
public void setName(String text) {
this.name = text;
}

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}

}

oraz plik Person.hbm.xml odzwierciedlający zawartość klasy Person w bazie danych:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

<class name="hello.Person" table="PEOPLE">

<id name="id" column="PERSON_ID">
<generator class="native">
</generator>
</id>

<property name="name" column="NAME" type="string" />

<property name="salary" column="SALARY" type="integer" />

</class>

</hibernate-mapping>

Skonfigurowałem w pliku hibernate.cfg.xml połączenie z bazą danych MySQL:

<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>

<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>

<property name="hibernate.connection.username">test</property>
<property name="hibernate.connection.password">test</property>

<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.timeout">300</property>
<property name="hibernate.c3p0.max_statements">50</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>

<!-- SQL to stdout logging -->
<property name="show_sql">true</property>

<property name="hibernate.hbm2ddl.auto">update</property>
<mapping resource="hello/Person.hbm.xml" />
</session-factory>
</hibernate-configuration>

Dzięki parametrowi
<property name="hibernate.hbm2ddl.auto">update</property>
schemat bazy danych zostanie dostosowany do aplikacji (zostaną utworzone lub zmodyfikowane odpowiednie tabele) przy jej starcie.
Natomiast parametr
<property name="show_sql">true</property>
pozwala wyświetla w konsoli zapytań SQL, które wykonuje Hibernate.

Parametr
<property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
określa tzw. dialekt Hibernate, który odpowiada za tłumaczenie zapytań Hibernate na zapytania SQLowe zrozumiałe dla konkretnego systemu zarządzania bazami danych.

Do tego zmieniłem zawartość metody main() klasy HelloWorld:

public static void main(String[] args) {
Session session = HibernateUtil.getSessionFactory().openSession();
Collection people = session.createCriteria(Person.class).list();
if (people.size() < 5) {
Transaction tx = session.beginTransaction();

Person p1 = new Person("Adam", 100);
Person p2 = new Person("Marek", 200);
Person p3 = new Person("Adrian", 150);
Person p4 = new Person("Konrad", 180);
Person p5 = new Person("Romek", 120);

session.persist(p1);
session.persist(p2);
session.persist(p3);
session.persist(p4);
session.persist(p5);

tx.commit();
}

Query query = session.createQuery("FROM Person p ORDER BY p.salary").setMaxResults(3);
List list = query.list();
for (Person person : list) {
System.out.println(person.getName() + " " + person.getSalary());
}

session.close();
HibernateUtil.shutdown();
}

Na początek prostsze zadanie - zapytanie do bazy MySQL. Odpalenie aplikacji kończy się spodziewanym wynikiem:

Hibernate: select person0_.PERSON_ID as PERSON_ID0_, person0_.NAME as NAME0_,
person0_.SALARY as SALARY0_ from PEOPLE person0_ order by person0_.SALARY limit ?
Adam 100
Romek 120
Adrian 150

Czas więc na uruchomienie aplikacji z bazą Oracle. Na początku, trzeba zmienić odrobinę plik Person.hbm.xml tak, aby unikalny identyfikator osoby generowany był z sekwencji:

<id name="id" column="PERSON_ID">
<generator class="sequence">
<param name="sequence">people_seq</param>
</generator>
</id>

oraz plik hibernate.cfg.xml, konfigurując połączenie z bazą Oracle:

<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
<property name="hibernate.connection.url">jdbc:oracle:thin:@127.0.0.1:1521/xe</property>
<property name="dialect">org.hibernate.dialect.OracleDialect</property>

<property name="hibernate.connection.username">test</property>
<property name="hibernate.connection.password">test</property>


<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.timeout">300</property>
<property name="hibernate.c3p0.max_statements">50</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>

<!-- SQL to stdout logging -->
<property name="show_sql">true</property>

<property name="hibernate.hbm2ddl.auto">update</property>

<mapping resource="hello/Person.hbm.xml" />

</session-factory>
</hibernate-configuration>

Uruchamiam aplikację i ...

Hibernate: select * from
( select person0_.PERSON_ID as PERSON_ID0_, person0_.NAME as NAME0_,
person0_.SALARY as SALARY0_ from PEOPLE person0_ order by person0_.SALARY )
where rownum <= ?
Adam 100
Romek 120
Adrian 150

Czyli bez niespodzianki. Dialekty Hibernate są więc dość potężnym mechanizmem. Pozwalają nie tylko na zmianę pojedynczych słów kluczowych, ale także konstrukcji całych zapytań. Trudno się zresztą dziwić. Hibernate to już bardzo dopracowany produkt.

Niech no mi się teraz nawinie ktoś na rozmowie rekrutacyjnej, ze znajomością Oracle PL/SQLa i Hibernate...

niedziela, 21 września 2008

Mój ulubiony Exception - java.lang.ArrayIndexOutOfBoundsException

a wszytko zaczęło się od popełnienia przeze mnie aplikacji. JSF, Facelets, Spring, JPA. Nic specjalnego... Poza tym, że na owej aplikacji od czasu do czasu (co kilka dni) wyskakiwał wyjątek java.lang.ArrayIndexOutOfBoundsException, a dokładniej:

09:17:54,784 ERROR [ajp-0.0.0.0-8009-11] (LifecycleDecorator.java:75) - javax.faces.FacesException: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:306)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
at pl.matt.common.LifecycleDecorator.render(LifecycleDecorator.java:71)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at pl.matt.common.filter.ProcessingTimeFilter.doFilter(ProcessingTimeFilter.java:29)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:241)
at org.apache.coyote.ajp.AjpProcessor.process(AjpProcessor.java:437)
at org.apache.coyote.ajp.AjpProtocol$AjpConnectionHandler.process(AjpProtocol.java:381)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.renderkit.RenderKitUtils.buildTypeArrayFromString(RenderKitUtils.java:719)
at com.sun.faces.renderkit.RenderKitUtils.determineContentType(RenderKitUtils.java:572)
at com.sun.faces.renderkit.RenderKitImpl.createResponseWriter(RenderKitImpl.java:219)
at com.sun.facelets.FaceletViewHandler.createResponseWriter(FaceletViewHandler.java:380)
at com.sun.facelets.FaceletViewHandler.renderView(FaceletViewHandler.java:550)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
... 24 more


Hmm... O co chodzi? Co ciekawe, błąd występował zawsze, gdy na stronę wchodziła przeglądarka Yanga WorldSearch Bot v1.1/beta (http://www.yanga.co.uk/). Robi się coraz bardziej interesująco, prawda? Przy wystąpieniu błędu, miałem też ustawione logowanie nagłówków HTTP. Oto one:

user-agent: Yanga WorldSearch Bot v1.1/beta (http://www.yanga.co.uk/)
accept: text/html;q=1.0, text/plain;q=1.0, text/;q=0.5, */*;q=0.1
accept-charset: utf-8;q=1.0, windows-1251;q=0.8, cp1251;q=0.8, koi8-r;q=0.8, *;q=0.5
accept-encoding: gzip;q=1.0, deflate;q=1.0, identity;q=0.5, *;q=0
content-length: 0


Niby nic specjalnego (no poza przeglądarką) ale sytuacja dziwna. Aplikacja odpalana jest na JBossie 4.2.1.GA, gdzie implementacją JSFów jest implementacja referencyjna SUNa w wersji 1.2.04-p02. Pobrałem źródła, aby podejrzeć 719 linię klasy RenderKitUtils


private final static String CONTENT_TYPE_SUBTYPE_DELIMITER = "/";

//...

// now split type and subtype
if (typeSubType.indexOf(CONTENT_TYPE_SUBTYPE_DELIMITER) >= 0) {
String[] typeSubTypeParts = Util.split(typeSubType.toString(), CONTENT_TYPE_SUBTYPE_DELIMITER);
type = typeSubTypeParts[0].trim();
subtype = typeSubTypeParts[1].trim();
} else {
type = typeSubType.toString();
subtype = "";
}


linia 719 to
subtype = typeSubTypeParts[1].trim();

Klasa parsuje zatem jeden z nagłówków HTTP gdzie spodziewa się dwóch napisów rozdzielonych "/". Od razu moją uwagę zwrócił nagłówek:
accept: text/html;q=1.0, text/plain;q=1.0, text/;q=0.5, */*;q=0.1 a dokładniej jego część: text/;.

Trop był, trzeba było go jeszcze sprawdzić. Zainstalowałem zatem w FireFoxie wtyczkę Modify Headers która umożliwia... zmodyfikowanie nagłówków HTTP. Skonfigurowałem ją tak, aby zastępowała nagłówek accept wartością text/html;q=1.0, text/plain;q=1.0, text/;q=0.5, */*;q=0.1.



uruchomiłem aplikację, wszedłem na nią Firefoksem i...
bingo. Aplikacja się wywala. Zaglądam w logi:

00:03:13,862 ERROR [LifecycleDecorator] javax.faces.FacesException: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:306)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
at pl.matt.common.LifecycleDecorator.render(LifecycleDecorator.java:71)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at pl.matt.common.filter.ProcessingTimeFilter.doFilter(ProcessingTimeFilter.java:29)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:241)
at org.apache.coyote.ajp.AjpProcessor.process(AjpProcessor.java:437)
at org.apache.coyote.ajp.AjpProtocol$AjpConnectionHandler.process(AjpProtocol.java:381)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.renderkit.RenderKitUtils.buildTypeArrayFromString(RenderKitUtils.java:719)
at com.sun.faces.renderkit.RenderKitUtils.determineContentType(RenderKitUtils.java:572)
at com.sun.faces.renderkit.RenderKitImpl.createResponseWriter(RenderKitImpl.java:219)
at com.sun.facelets.FaceletViewHandler.createResponseWriter(FaceletViewHandler.java:380)
at com.sun.facelets.FaceletViewHandler.renderView(FaceletViewHandler.java:550)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
... 24 more

Jest mój ulubiony Exception.

Tak jak w tenisie stołowym serwis to pół punktu, tak w programowaniu powtórzenie błędu to połowa jego poprawienia. Teraz tylko... no właśnie...

odpaliłem aplikację w trybie odpluskiwania (debug).



Analizując stos wywołań natrafiłem na 203. linię klasy RenderKitImpl:
String[] typeArray = context.getExternalContext().getRequestHeaderValuesMap().get("Accept");

O co chodzi? Przydała się wiedza z zakresu... serwletów! Na obiekcie HttpServletRequest wywołana jest metoda getHeaders(), która zwraca obiekt klasy Enumeration reprezentujący nagłówki HTTP o zadanej nazwie, w tym przypadku "Accept".

Jak teraz podmienić nagłówek w obiekcie HttpServletRequest? Nie da się...
Da się natomiast, przefiltrować żądanie HTTP, podmieniając obiekt je reprezentujący na obiekt innej klasy (oczywiście implementującej interfejs HttpServletRequest). Brzmi skomplikowanie? Chyba nie jest tak źle.

Zatem po kolei:

tworzę klasę filtra:

public class ModifyRequestHeaderFilter implements Filter {

public void init(FilterConfig filterConfig) throws ServletException {
}

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws java.io.IOException,
ServletException {
if (req instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) req;
req = new HttpServletRequestWrapper(request);
}

chain.doFilter(req, res);
}

public void destroy() {
}
}


metoda
chain.doFilter(req, res);
przekazuje żądanie do dalszego przetworzenia, ale wcześniej, żądanie jest opakowywane:

HttpServletRequest request = (HttpServletRequest) req;
req = new HttpServletRequestWrapper(request);

w obiekt mojej klasy, która przeciąża metodę getHeaders() klasy HttpServletRequest.

Klasa HttpServletRequestWrapper rozszerza klasę javax.servlet.http.HttpServletRequestWrapper, która w zasazdie tylko opakowuje żądanie HTTP. Delegując wywołania wszystkich metod do opakowanego obiektu, właśnie po to, żeby można było przeciążyć wybrane z nich. To przy okazji świetny przykład wzorca dekorator .Tworzę zatem swoją klasę HttpServletRequestWrapper która przy żądniu nagłówków o nazwie "Accept", podmienia w ich treści "text/;" na "text/*;".


public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper {

private String substitutedHeaderName = "Accept";
private String toReplace = "text/;";
private String replacement = "text/*;";

public HttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}

@SuppressWarnings( { "unchecked" })
@Override
public Enumeration getHeaders(String name) {
if (substitutedHeaderName.equals(name)) {
Enumeration headers = super.getHeaders(name);
List out = new ArrayList();
while (headers.hasMoreElements()) {
String header = (String) headers.nextElement();
if (header.contains(toReplace)) {
header = header.replaceAll(toReplace, replacement);
}
out.add(header);
}
return new IteratorEnumeration(out.iterator());
}
return super.getHeaders(name);
}

}


Trzeba jeszcze zarejestrować całe rozwiązanie w deskryptorze wdrożenia - pliku web.xml. Filtr konfiguruję tak, aby obsługiwał wszystkie żądania odwołujące się do stron JSF.


<filter>
<filter-name>ModifyRequestHeaderFilter</filter-name>
<filter-class>pl.matt.common.filter.ModifyRequestHeaderFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>ModifyRequestHeaderFilter</filter-name>
<url-pattern>*.jsf</url-pattern>
</filter-mapping>


Restart aplikacji i... działa. Serwlety wiecznie żywe. Mateusz wiecznie zadowolony.

czwartek, 4 września 2008

JBoss Tools - nowa jakość w tworzeniu aplikacji opartych o JSF

Całkiem niedawno, Piotrek Buczek, jeden z moich zafascynowanych Seamem kolegów pokazał mi kilka sztuczek związanych z tą technologią. Oprócz sztuczek, bardzo zresztą widowiskowych uwagę moją przykuło środowisko na jakim pracował. Był to Eclipse 3.3 z wtyczką JBoss Tools. Wsparcie dla Seama - bardzo pierwsza liga, jak to mawia pan Kazimierz. Jak się ma wsparcie dla samych JSFów, zaraz zobaczymy. Zdradzę tylko, że wcale nie gorzej...

Odkąd dostałem olbrzymią emkę za udział w konkursie Ganymede Around the World Contest nie przystoi mi używać innej wersji Eclipse. Na nim zatem zainstaluję wtyczkę JBoss Tools. Z Ganymede działa tylko rozwojowa wersja wtyczki (JBossTools 3.0.0.Alpha1) więc tą właśnie zainstaluję, ale po kolei...

Wtyczkę można pobrać ze strony projektu JBoss Tools lub zainstalować wykorzystując Eclipsowego zarządcę wtyczek. Wybieram to drugie rozwiązanie. Otwieram menu
Help / Software Updates...
i wybieram zakładkę
Avaliable Software.
Dodaję nową stronę o adresie:
http://download.jboss.org/jbosstools/updates/development



Rozwijam nowo utworzony węzeł drzewka, zaznaczam
JBossTools Development Release 3.0.0.Alpha1
i klikam magiczny przycisk
Install



Eclipse warczy, ściąga, restartuje się... i gotowe. Wypada jeszcze tylko skonfigurować w Eclipse JBossa. Odpalam zatem menu
Window / Preferencess. i tam wybieram pozycję
Server / Runtime Environments następnie klikam
Add... i rozwijam gałąź JBoss, a division of Red Hat.


Kilka razy klikam Next wskazuję położenie na dysku JBossa i gotowe.

Czas przyjrzeć się lepiej możliwościom wtyczki.Tworzę nowy projekt JSF (File / New... / Project... / JSF Project).


Na następnej zakładce wybieram wersję JSF 1.2 z Faceletami oraz szablon FaceletsKickStartWithoutLibs, jako, że biblioteki mam w JBossie.




Zobaczmy co oferuje wtyczka. Otwieram plik WebContent/pages/greeting.xhtml. Pierwsze zaskoczenie - oprócz kodu pliku xhtml, mam zgrabny podgląd.



Klikam dowolne miejsce na podglądzie, podświetla mi się odpowiednie miejsce w kodzie.



Ładnie...
Dodaję nowy tag, zmiany na bieżąco widzę w oknie podglądu.



Jak dla mnie, rewelacja...
Czas na zabawy z klawiszem CTRL. Przyciskam go, najeżdżam myszą na pole name wyrażenia #{person.name}. Klikam..,. i już widzę metodę getName() klasy Person, która w pliku faces-config.xml skonfigurowana jest jako ziarno zarządzane JSF.



Klikam w słówko greeting wyrażenia #{msg.greeting}. No i bomba, jestem już przy edycji pliku messages.properties i jego właściwości greeting.



Plik oczywiście jest skonfigurowany odpowiednio w faces-config.xml.
Przejdźmy zatem do tego pliku. Już pierwszy rzut oka, pozwala nam się zorientować, że mamy możliwość graficznej edycji nawigacji w aplikacji JSF.



Sympatycznie. Tu niestety mały minus... Wtyczka automatycznie formatuje zawartość xmlową pliku faces-config.xml, więc nie bardzo można mieć kontrolę nad wcięciami i kolejnością tagów w pliku. Przynajmniej ja nie potrafię tego osiągnąć.

Wrócę jeszcze na moment do pliku WebContent/pages/greeting.xhtml. Oprócz nawigacji z klawiszem CTRL. Wtyczka JBoss Tools oferuje całkiem niezłe podpowiadanie. Zarówno tagów (wystarczy wpisać <f: i nacisnąć CTRL+spację)



jak i właściwości ziaren zarządzanych



oraz wpisów w pliku messages.properties.



Mało? Zobaczmy, co jeszcze oferuje JBoss Tools. Odpalam projekt (na zakładce JBoss Server View klikam Start the server.



Serwer startuje...
Wchodzę na stronę
http://localhost:8080/jsfProject/

otwieram w Eclipsie plik
WebContent/pages/inputname.xhtml. Zauważyliście, że wykorzystywana jest tu, trochę zapomniana funkcja Faceletów, która umożliwia takie konstruowanie stron JSF, aby były one poprawnie wyświetlane w przeglądarce WWW bez uruchamiania ich na serwerze aplikacji?
Zmieniam tytuł Facelets Hello Application na Witaj!. Zapisuję stronę. Przeładowuję ją w przeglądarce... i gotowe, zmiany od razu są widoczne.



Niestety zmiany w kodzie źródłowym, w plikach messages*.properties lub konfiguracja w faces-config.xml wymagają już restartu serwera.



To zapewne tylko niektóre możliwości wtyczki. Jak dla mnie, są one rewelacyjne. Coś czuję, że JBoss Tools dołączy do mojego zestawu ulubionych wtyczek środowiska Eclipse.

niedziela, 31 sierpnia 2008

koszulka Eclipse, bug naprawiony

Stało się...
Eclipse 3.4.0 ujrzał światło dzienne, a Ganymede Around the World Contest dobiegł końca. Wziąłem udział w tym konkursie, co na szczęście nie umknęło uwadze organizatorów, o czym przekonałem się odbierając przesyłkę:


Niby emka, ale jakaś duża. Przynajmniej nie zrobiona w Chinach a w Kanadzie.

Jakby tego było mało, zgłoszony przeze mnie błąd został już naprawiony. Dołączony zostanie prawdopodobnie do wersji 3.4.1 środowiska.
Poprawiony błąd i koszulka. Chyba warto brać udział w konkursach spod stajni Eclipse.

środa, 13 sierpnia 2008

testy java

Zawsze mnie zastanawiało, skąd się biorą podchwytliwe pytania dotyczące programowania w Javie. Czy wymyślają je programiści, na zasadzie "sztuka dla sztuki", czy pochodzą one z prawdziwych projektów. Moja zagadka pochodzi z życia.

Co będzie wynikiem wywołania funkcji:


public static void main(String[] args) {
BigDecimal arg1 = new BigDecimal("10");
BigDecimal arg2 = new BigDecimal("3");
System.out.println(arg1.divide(arg2).toPlainString());
}


a) na konsole zostanie wypisane: 3,333333333
b) na konsole zostanie wypisane: 3,33
c) na konsole zostanie wypisane: 3
d) zostanie rzucony wyjątek w linii System.out.println(arg1.divide(arg2).toPlainString());
e) na konsole zostanie wypisane: null

no i odpowiedź:
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
następnym razem dzieląc obiekty klasy BigDecimal będę uważał.

poniedziałek, 11 sierpnia 2008

Java EE 5 Development using GlassFish Application Server

Korzystając z biblioteki warszawskiego JUGa miałem okazję zapoznać się z książką
"Java EE 5 Development using GlassFish Application Server"
autorstwa Davida R. Heffelfingera. Poniżej przedstawiam recenzję tej pozycji. Recenzja jest po angielsku - takie wymogi wydawnictwa, które wspomaga bibliotekę książkami.

I newer used GlassFish application server so I was really curious about it's features. Reading a book about GlassFish could have been a great possibility to familiarize with it's capabilities. Could have been, but haven't. After first look on the table of contents I realized, that this book is rather about Java EE 5 Development than about Development using GlassFish Application Server. The glass fish on the cover misled me.

So instead of improving my knowledge about GlassFish features I revised my J2EE skills. Hibernate, JDBC, JSP, JSF, EJB, Web Services weren't novelties for me, but I've founded some interesting fragments. Did you know, that you may use Facelets to make JSF-xml pages viewable directly in browser. I didn't. The book contains quite a lot similar more or less useful tricks with example code. Unfortunately it also contains some bugs, like getting new ID by executing
"select max(customer_id) + 1 from customers"
statement.

Generally speaking Java EE 5 Development using GlassFish Application Server by David R. Heffelfinger is a solid 400 pages book about J2EE. If you want to know GlassFish Application Server in details, you will be disappointed, but if you are a newbie in J2EE you will spent a lot of interesting evenings with it. Even more than with one of your girlfriends.

piątek, 1 sierpnia 2008

zdalne debugowanie aplikacji na serwerze aplikacyjnym JBoss

Istnieją jeszcze starej daty programiści którzy lubią pracę z konsolą. Nie uruchamiają oni serwerów aplikacyjnych z poziomu Eclipse, korzystają za to z terminala. Zaliczam się do nich i ja. Podejście takie ma jedną zasadniczą wadę. W Eclipse, aby uruchomić serwer aplikacyjny w trybie debug wystarczy na zakładce Servers kliknąć odpowiednią ikonkę. Z poziomu konsoli jest nieco trudniej. Ale tylko troszeczkę...

Ręczne uruchamianie serwera w trybie debug może się także przydać przy debugowaniu serwera uruchomionego na innym komputerze, niż komputer programisty. Instrukcję jak to zrobić, na przykładzie Eclipse 3.4.0 i serwera JBoss 4.0.5.GA przedstawiam poniżej.

W przykładzie, korzystam z aplikacji akwarium, którą od czasu do czasu posiłkuję się na blogu.

Najpierw zajmę się JBossem. W katalogu bin znajduje się plik run.sh. Kopiuję go na debug.sh. Na początku pliku dodaję:

JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n"

To polecenie modyfikuje zmienną JAVA_OPTS, której zawartość dalej w skrypcie staje się parametrami uruchomieniowymi wirtualnej maszyny (JVM) z którą wystartuje serwer. Dobrze jest zapamiętać wartość wpisaną jako address i ustawić suspend=n. W przeciwnym wypadku JBoss nie uruchomi się dopóki nie podłączymy się do niego z IDE.

W JBossie to już wszystko. Czas na Eclipse.

W klasie AquariumManagerImpl ustawiam pułapkę, klikając na lewym marginesie.



Tutaj zatrzyma się aplikacja aby umożliwić śledzenie jej wykonania krok po kroku.

Następnie wybieram z menu Debug pozycję Debug Configurations....




Wybieram z listy po lewej stronie Remote Java Application podając odpowiedni port i nazwę.



Klikam Apply i Debug. Wyskoczy błąd, ponieważ serwer nie jest jeszcze włączony.

Czas zatem go uruchomić, tym razem nie poleceniem
./run.sh
a
./debug.sh

JBoss powienien wypluć z siebie:
Listening for transport dt_socket at address: 8787

Następnie, w Eclipse z menu Debug wybieram pozycję aquarium - debug.

Wchodzę na stronę:
http://localhost:8080/aquarium/info.htm.
Strona się nie ładuje, przechodzę do Eclipse. Zatwierdzam zmianę perspektywy i już mogę przechodzić kod aplikacji linijka po linijce za pomocą klawisza F6
F8 natomiast wznawia dalszy bieg programu.

wtorek, 29 lipca 2008

Sakiewka na Ubuntu 8.04

Witam ponownie. Dawno nic nie pisałem, a to wszystko przez moje przygody z wine. Otóż udało mi się odpalić za jego pomocą Civilization IV wraz z dodatkami. Świetna muzyka i świetna zabawa na długie miesiące...

Jak się okazuje, wstęp tego posta nie jest całkiem od parady... albowiem dziś też poeksperymentuję trochę z wine. Poniżej przedstawię sposób, na zainstalowanie programu Sakiewka na Ubuntu 8.04. Sakiewka, to program Jerzego Olszewskiego wspomagający inwestowanie na giełdach papierów wartościowych. Program napisany jest pod Windows, więc bez wine się nie obejdzie;

~$ sudo apt-get install wine
Czytanie list pakietów... Gotowe
Budowanie drzewa zależności
Odczyt informacji o stanie... Gotowe
wine jest już w najnowszej wersji.
0 aktualizowanych, 0 nowo instalowanych, 0 usuwanych i 0 nieaktualizowanych.

ja już mam wine zainstalowane...

Czas pobrać wersję instalacyjną Sakiewki.

rozpakować:

$ unzip DISK1.zip
Archive: DISK1.zip
creating: DISK1/
inflating: DISK1/setup.exe
inflating: DISK1/bdemerge.ini

i odpalić z wine
wine DISK1/setup.exe

Powita nas instalator:



klikam kilka razy dalej i wskazuję katalog instalacyjny



Obyło się bez przeszkód. Czas na odpalenie programu.

~$ cd /opt/sakiewka/Sakwa
:/opt/sakiewka/Sakwa$ wine Sakiewka.exe

No proszę, wszystko działa.



W sumie to dobrze, bo najwyższy czas spać...