Deklaratywne bezpieczeństwo w JSF z JAAS i Jetty
Z Jacek Laskowski - Wiki Projektanta Java EE
Podczas kolejnej wycieczki technologicznej przyszło mi się zmierzyć z tematem, o który byłem pytany wielokrotnie, ale nie znałem odpowiedzi - deklaratywne uwierzytelnianie i autoryzacja w aplikacjach webowych z JSF. W ogóle temat bezpieczeństwa w aplikacjach korporacyjnych był traktowany przeze mnie po macoszemu i nigdy nie doczekał się odpowiedniego traktowania. Aż do dzisiaj, kiedy to postanowiłem nie odpuszczać, aż do znalezienia rozwiązania.
Zadanie: Zabezpieczenie aplikacji webowej opartej o JavaServer Faces (JSF) przed nieuprawnionym dostępem, czyli wprowadzenie mechanizmu deklaratywnego uwierzytelnienia i autoryzacji.
Utworzenie aplikacji webowej JSF z archetypem myfaces-archetype-trinidad
Zacząłem jak zwykle korzystając z mavenowego polecenia mvn archetype:generate -DarchetypeCatalog=http://myfaces.apache.org -DgroupId=pl.jaceklaskowski.jsf -DartifactId=jsf-jaas-jetty -Dversion=1.0.
jlaskowski@work /cygdrive/c/projs/sandbox
$ mvn archetype:generate -DarchetypeCatalog=http://myfaces.apache.org \
-DgroupId=pl.jaceklaskowski.jsf -DartifactId=jsf-jaas-jetty -Dversion=1.0
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO] task-segment: [archetype:generate] (aggregator-style)
[INFO] ------------------------------------------------------------------------
...
[INFO] [archetype:generate]
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: remote -> myfaces-archetype-helloworld (Simple Web application using Apache Myfaces)
2: remote -> myfaces-archetype-helloworld-facelets (Simple Web application using Apache Myfaces and Facelets)
3: remote -> myfaces-archetype-helloworld-portlets (Simple Web application using Apache Myfaces and Portlets)
4: remote -> myfaces-archetype-jsfcomponents (Simple JSF Component using Apache Myfaces)
5: remote -> myfaces-archetype-trinidad (Simple Web application using Apache Myfaces and Trinidad)
Choose a number: (1/2/3/4/5): 5
Confirm properties configuration:
groupId: pl.jaceklaskowski.jsf
artifactId: jsf-jaas-jetty
version: 1.0
package: pl.jaceklaskowski.jsf
Y: : <ENTER>
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating OldArchetype: myfaces-archetype-trinidad:1.0.1
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: pl.jaceklaskowski.jsf
[INFO] Parameter: packageName, Value: pl.jaceklaskowski.jsf
[INFO] Parameter: basedir, Value: c:\projs\sandbox
[INFO] Parameter: package, Value: pl.jaceklaskowski.jsf
[INFO] Parameter: version, Value: 1.0
[INFO] Parameter: artifactId, Value: jsf-jaas-jetty
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] OldArchetype created in dir: c:\projs\sandbox\jsf-jaas-jetty
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
Sprawdzam, czy aplikacja działa poprawnie zanim wprowadzę mechanizm uwierzytelnienia użytkowników, który potencjalnie mógłby ją zepsuć. Przechodzę do katalogu jsf-jaas-jetty i wydaję polecenie mvn jetty:run.
jlaskowski@work /cygdrive/c/projs/sandbox/jsf-jaas-jetty $ mvn jetty:run [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'jetty'. [INFO] ------------------------------------------------------------------------ [INFO] Building A custom project using myfaces [INFO] task-segment: [jetty:run] [INFO] ------------------------------------------------------------------------ ... [INFO] [jetty:run] [INFO] Configuring Jetty for project: A custom project using myfaces [INFO] Webapp source directory = C:\projs\sandbox\jsf-jaas-jetty\src\main\webapp [INFO] web.xml file = C:\projs\sandbox\jsf-jaas-jetty\src\main\webapp\WEB-INF\web.xml [INFO] Classes = C:\projs\sandbox\jsf-jaas-jetty\target\classes 2008-05-24 21:42:34.031::INFO: Logging to STDERR via org.mortbay.log.StdErrLog [INFO] Context path = /jsf-jaas-jetty [INFO] Tmp directory = determined at runtime [INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml [INFO] Web overrides = none [INFO] Webapp directory = C:\projs\sandbox\jsf-jaas-jetty\src\main\webapp [INFO] Starting jetty 6.1.8 ... 2008-05-24 21:42:34.109::INFO: jetty-6.1.8 ... 2008-05-24 21:42:35.844::INFO: Started SelectChannelConnector@0.0.0.0:8080 [INFO] Started Jetty Server [INFO] Starting scanner at interval of 10 seconds.
Przechodzę na stronę http://localhost:8080/jsf-jaas-jetty/faces/index.jspx (nie zapominam o obowiązkowym faces) i...
działa!
Zmiany wersji Jetty i specyfikacji Java Servlets
Najpierw kilka usprawnień, które warto wprowadzić jeszcze przed zabezpieczeniem aplikacji. Nie są one konieczne, ale pozwalają korzystać z dobrodziejstw najnowszych specyfikacji i wersji oprogramowania. Najpierw podniosę wersję Jetty do 6.1.10 w pom.xml.
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.10</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
</configuration>
</plugin>
Kolejną zmianą jest podniesienie wersji wykorzystywanej specyfikacji Java Servlets do 2.5 przez zmianę w deskryptorze wdrożenia aplikacji webowej /WEB-INF/web.xml. Plik znajduje się w projekcie w katalogu src/main/webapp/WEB-INF/web.xml. Aktualny element główny web.xml - web-app - podmieniamy na następujący:
<web-app 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" version="2.5">
Sprawdzam poprawność działania aplikacji po zmianach z Jetty 6.1.10 i Java Servlets 2.5 ponownie z mvn jetty:run.
jlaskowski@work /cygdrive/c/projs/sandbox/jsf-jaas-jetty $ mvn jetty:run [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'jetty'. [INFO] ------------------------------------------------------------------------ [INFO] Building A custom project using myfaces [INFO] task-segment: [jetty:run] [INFO] ------------------------------------------------------------------------ ... [INFO] Starting jetty 6.1.10 ... 2008-05-24 21:53:33.294::INFO: jetty-6.1.10 ... [INFO] Started Jetty Server [INFO] Starting scanner at interval of 10 seconds.
Zabezpieczenie aplikacji webowej w deskryptorze wdrożenia WEB-INF/web.xml
Zabezpieczenie dostępu do wybranych części aplikacji webowej odbywa się deklaratywnie (zamiast oprogramowywania) przez deskryptor wdrożenia /WEB-INF/web.xml za pomocą elementów security-constraint, login-config oraz security-role.
Modyfikuję deskryptor wdrożenia /WEB-INF/web.xml (plik src/main/webapp/WEB-INF/web.xml) o dodanie następującej sekcji:
<security-constraint>
<web-resource-collection>
<web-resource-name>Wszystkie pliki jspx</web-resource-name>
<url-pattern>*.jspx</url-pattern>
<http-method>DELETE</http-method>
<http-method>GET</http-method>
<http-method>POST</http-method>
<http-method>PUT</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>Obszar zabezpieczony</realm-name>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login.html</form-error-page>
</form-login-config>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</login-config>
<security-role>
<role-name>USER</role-name>
</security-role>
<security-role>
<role-name>ADMIN</role-name>
</security-role>
Umieszczam zmiany po elemencie welcome-file-list. Jej celem jest zabezpieczenie wszystkich plików o rozszerzeniu jspx (element url-pattern w security-constraint/web-resource-collection), które są "dotykane" za pomocą różnych poleceń HTTP (element http-method) i udostępnienie ich jedynie użytkownikom z rolami wskazanymi przez role-name w security-constraint/auth-constraint. Specjalna wartość * (gwiazdka) wskazuje na wszystkie role zdefiniowane w deskryptorze w elementach security-role - w tym przypadku będą to użytkownicy z rolami USER lub ADMIN. W ten sposób deklaratywnie konfiguruję autoryzację w aplikacji. Ostatnim elementem konfiguracji bezpieczeństwa jest login-config, która określa sposób uwierzytelnienia użytkowników - auth-method o wartości FORM to formularz ze strony form-login-page, a w razie błędu uwierzytelnienia wyświetli się strona wskazana przez form-error-page, która jest identyczna do strony formularza. W ten sposób deklaratywnie obsłużyłem wymaganie uwierzytelniania użytkowników.
Konfiguracja wtyczki maven-jetty-plugin
Cała konfiguracja bezpieczeństwa aplikacji webowej określa jej wymagania deklaratywnie, które muszą być spełnione przez kontener servletów (Jetty). Za pomocą mechanizmów specyficznych dla kontenera servletów następuje określenie jacy użytkownicy faktycznie zostaną przypisani do ról USER oraz ADMIN. Kombinacja Jetty i Maven daje nam możliwość zdefiniowania tej konfiguracji w pliku konfiguracyjnym projektu mavenowego w sekcji konfigurującej wtyczkę maven-jetty-plugin. Możliwe parametry konfiguracyjne wtyczki opisane są w Maven Jetty Plugin.
Bazując na aplikacji przykładowej test-jaas-webapp dystrybuowanej z Jetty 6.1.10 oraz dokumentacji Jetty JAAS skorzystam z następującej konfiguracji wtyczki maven-jetty-plugin.
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.10</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<systemProperties>
<systemProperty>
<name>jetty.home</name>
<value>./src</value>
</systemProperty>
<systemProperty>
<name>java.security.auth.login.config</name>
<value>src/etc/login.conf</value>
</systemProperty>
</systemProperties>
<userRealms>
<userRealm implementation="org.mortbay.jetty.plus.jaas.JAASUserRealm">
<name>Obszar zabezpieczony</name>
<loginModuleName>modul-uwierzytelniajacy</loginModuleName>
</userRealm>
</userRealms>
</configuration>
</plugin>
Zdecydowałem się skorzystać z Java Authentication and Authorization Service (JAAS) przede wszystkim, dlatego że jest to standard Javy odnośnie uwierzytelniania i autoryzacji użytkowników w dowolnych aplikacjach javowych (nie tylko webowych). Innych powodów nie było, poza nieznajomością innych, alternatywnych rozwiązań.
Ważnym elementem konfiguracji maven-jetty-plugin jest sekcja systemProperty z parametrem java.security.auth.login.config, który wskazuje na plik konfiguracyjny modułów uwierzytelniających JAAS - src/etc/login.conf.
modul-uwierzytelniajacy {
org.mortbay.jetty.plus.jaas.spi.PropertyFileLoginModule required debug=true
file="${jetty.home}/etc/login.properties";
};
gdzie nazwa modułu wskazanego przed otwierającym nawiasem klamrowym { musi być wskazana w loginModuleName w userRealms/userRealm konfiguracji wtyczki maven-jetty-plugin.
Dodatkowo istotnym elementem konfiguracji bezpieczeństwa opartej o JAAS w Jetty jest związanie realm-name z deskryptora wdrożenia WEB-INF/web.xml z elementem name (podelement userRealms/userRealm). W moim przypadku nazwą przestrzeni bezpieczeństwa jest Obszar zabezpieczony.
Konfiguracja JAAS kończy się utworzeniem pliku konfiguracyjnego modułu src/etc/login.properties:
admin=admin,ADMIN
gdzie definiuje się użytkowników, hasła i ich role. Format pliku jest zależny od implementacji modułu JAAS i dla org.mortbay.jetty.plus.jaas.spi.PropertyFileLoginModule będzie to
uzytkownik=haslo,rola1,rola2,...
Oczywiście istnieje możliwość definiowania wielu modułów uwierzytelniających o różnych plikach konfiguracyjnych, gdzie definiuje się repozytoria użytkowników, grup i ich rolach, np. opartych o bazę danych, czy serwery LDAP.
Strona formularza uwierzytelniającego - login.html
Na zakończenie konfiguracji należy stworzyć stronę formularza login.html, którą wskazałem w deskryptorze wdrożenia aplikacji webowej WEB-INF/web.xml w sekcji form-login-config. Za pomocą tej strony użytkownik będzie podawał swoje dane - identyfikator i hasło. Jest to zwykła strona HTML (mogłaby być opcjonalnie z elementami JSP), gdyż przede wszystkim nie udało mi się rozwiązać tematu budowania interfejsu użytkownika formularza z wykorzystaniem kontrolek JSF i wskazania akcji zatwierdzającej dane wprowadzone przez użytkownika na j_security_check, która jest wymagana przez specyfikację Java Servlets jako uaktywniająca infrastrukturę deklaratywnego uwierzytelniania i autoryzacji w kontenerze servletów. Poza wymaganym elementem j_security_check formularza wymagane przez specyfikację Java Servlets są pola j_username oraz j_password.
Plik login.html umieszczam w katalogu src/main/webapp/.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Formularz uwierzytelniający</title>
</head>
<body>
<form method="POST" action="j_security_check">
<b>Identyfikator:</b><input type="text" name="j_username">
<p>
<b>Hasło: </b><input type="password" name="j_password">
<p>
<input type="submit" value="Wchodzę">
</form>
</body>
</html>
Finalne uruchomienie z mvn jetty:run
Mając tak przygotowaną konfigurację aplikacji wystarczy uruchomić ją ponownie z mvn jetty:run (opcjonalnie odczekać 10 sekund na odświeżenie konfiguracji przez Jetty zgodnie z parametrem scanIntervalSeconds wtyczki maven-jetty-plugin - często jednak takie uruchomienie aplikacji kończyło się komunikatem No realm w przeglądarce) i spróbować uruchomić stronę http://localhost:8080/jsf-jaas-jetty/faces/index.jspx. Nastąpi przechwycenie żądania i przekierowanie do strony z formularzem uwierzytelniania.
i po podaniu danych użytkownika - identyfikator: admin z hasłem admin wciskam przycisk Wchodzę i...
Obsługa HTTPS w Jetty - src/etc/jetty.xml
Jako, że przesyłanie hasła bez zabezpieczenia kanału komunikacji nie jest zalecane można zdefiniować port obsługujący HTTPS w pom.xml w pliku src/etc/jetty.xml wskazanym przez jettyConfig w sekcji configuration wtyczki maven-jetty-plugin zgodnie z dokumentem Jetty - How to configure SSL.
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.10</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<systemProperties>
<systemProperty>
<name>jetty.home</name>
<value>./src</value>
</systemProperty>
<systemProperty>
<name>java.security.auth.login.config</name>
<value>src/etc/login.conf</value>
</systemProperty>
</systemProperties>
<jettyConfig>src/etc/jetty.xml</jettyConfig>
<userRealms>
<userRealm implementation="org.mortbay.jetty.plus.jaas.JAASUserRealm">
<name>Obszar zabezpieczony</name>
<loginModuleName>modul-uwierzytelniajacy</loginModuleName>
</userRealm>
</userRealms>
</configuration>
</plugin>
z plikiem src/etc/jetty.xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.1//EN" "http://jetty.mortbay.org/configure_1_2.dtd">
<Configure id="Server" class="org.mortbay.jetty.Server">
<Call name="addConnector">
<Arg>
<New class="org.mortbay.jetty.security.SslSocketConnector">
<Set name="Port">8443</Set>
<Set name="maxIdleTime">30000</Set>
<Set name="keystore"><SystemProperty name="jetty.home" default="src" />/etc/keystore</Set>
<Set name="password">password</Set>
<Set name="keyPassword">password</Set>
<Set name="truststore"><SystemProperty name="jetty.home" default="src" />/etc/keystore</Set>
<Set name="trustPassword">password</Set>
</New>
</Arg>
</Call>
<Call name="addConnector">
<Arg>
<New class="org.mortbay.jetty.nio.SelectChannelConnector">
<Set name="port">8080</Set>
<Set name="maxIdleTime">30000</Set>
<Set name="Acceptors">2</Set>
<Set name="confidentialPort">8443</Set>
</New>
</Arg>
</Call>
</Configure>
Uruchomienie polecenia keytool -keystore keystore -alias jetty -genkey -keyalg RSA musi być wykonane w katalogu src/etc projektu, tak aby plik keystore został odnaleziony przez Jetty. Jawnie podałem hasła, gdyż w przeciwnym przypadku otrzymywałem komunikaty błędu Keystore was tampered with, or password was incorrect (osoby zainteresowane udoskonaleniem artykułu i przesłanie do mnie wymaganych kroków do poprawnej konfiguracji jetty.xml z zakodowanymi hasłami proszeni są o kontakt jacek@laskowski.net.pl).
Poprawne uruchomienie Jetty z powyższą konfiguracją HTTPS poleceniem mvn jetty:run będzie widoczne na konsoli jako
2008-05-24 23:32:36.672::INFO: Started SslSocketConnector@0.0.0.0:8443 2008-05-24 23:32:36.719::INFO: Started SelectChannelConnector@0.0.0.0:8080
Wchodząc na stronę http://localhost:8080/jsf-jaas-jetty/faces/index.jspx następuje przekierowanie na stronę formularza uwierzytelnienia po HTTPS.
Wprowadzam admin/admin i znowu jestem na HTTP uwierzytelniony. Aplikacja działa zgodnie z oczekiwaniami!
UWAGA: Czasami zdarzało się, że przekierowanie nie było jednak wykonane z mvn jetty:run. Nie potrafię jednak wytłumaczyć dlaczego.
Kompletny projekt aplikacji dostępny jest do pobrania jako jsf-jaas-jetty.zip.




