13 stycznia 2009

Spring Security z CAS w aplikacji webowej

Zabrałem się za acegi z Grails, ale coś mi nieszło, więc zszedłem na poziom samego Acegi, a właściwie to Spring Security 2.0.4 w "zwykłych" aplikacjach webowych z użyciem facelets. Instrukcja w Tutorial: Adding Security to Spring Petclinic zadziałała bezbłędnie. Przykładowy projekt, który zabezpieczałem Spring Security zarządzany jest przez Apache Maven, więc nie kopiowałem bibliotek, a jedynie zdefiniowałem konieczne zależności w pom.xml:
 <properties>
<spring-security.version>2.0.4</spring-security.version>
</properties>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core-tiger</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas-client</artifactId>
<version>${spring-security.version}</version>
</dependency>
i dodałem /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">

<http auto-config="true">
<intercept-url pattern="/**" access="ROLE_USER"/>
</http>
<!--
Usernames/Passwords are
rod/koala
dianne/emu
scott/wombat
peter/opal
-->
<authentication-provider>
<password-encoder hash="md5"/>
<user-service>
<user name="rod" password="a564de63c2d0da68cf47586ee05984d7"
authorities="ROLE_SUPERVISOR, ROLE_USER, ROLE_TELLER"/>
<user name="dianne" password="65d15fe9156f9c4bbffd98085992a44e" authorities="ROLE_USER,ROLE_TELLER"/>
<user name="scott" password="2b58af6dddbd072ed27ffc86725d7d3a" authorities="ROLE_USER"/>
<user name="peter" password="22b5c9accc6e1ba628cedc63a72d57f8" authorities="ROLE_USER"/>
</user-service>
</authentication-provider>

</beans:beans>
Prawie tak jak opisano w dokumencie, poza wymaganiem, że wszystkie strony <intercept-url pattern="/**" access="ROLE_USER"/> są chronione. Jest jednak drobny błąd w dokumentacji, która wymaga, aby zdefiniować /WEB-INF/applicationContext-security.xml w context-param w deskryptorze wdrożenia web.xml, podczas gdy przez cały dokument mówi się o applicationContext-security-ns.xml. Już zgłosiłem jako SEC-1079 applicationContext-security.xml in Tutorial: Adding Security to Spring Petclinic.

Pozostało zabrać się za CASowanie mojej przykładowej aplikacji. Zabrałem się za 3.4. CAS Sample, a tam...dwa (drobne?) błędy. Pierwszy to wskazanie na dokument z opisem jak pobrać źródła "...as described in the introduction.":

Not Found
The requested URL /spring-security/site/reference/html/get-source was not found on this server.


a kiedy już dobrałem się do właściwego dokumentu 1.4. Getting the Source okazało się, że http://acegisecurity.svn.sourceforge.net/svnroot/acegisecurity/spring-security/trunk/ jest już nieaktualny i faktycznie powinien być https://src.springframework.org/svn/spring-security/trunk
. Niezły bałagan! Zgłosiłem jako SEC-1080 3.4. CAS Sample refers to incorrect get-source document and incorrect svn repo URL. Warto odnotować, jak szybko nastąpiła reakcja ze strony członków zespołu Spring Security - niecała godzina i już znalazł się chętny do wdrożenia poprawek!

Wracając do CAS i Spring Security, w porównaniu z poprzednią konfiguaracją teraz to beans jest wiodącą przestrzenią nazw (poprzednio security). To jest akurat niewielka zmiana i wyłącznie dotyczy organizacji pliku xmlowego niż samej konfiguracji Spring Security.
 <?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
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">

<sec:http entry-point-ref="casProcessingFilterEntryPoint">
<sec:intercept-url pattern="/**" access="ROLE_USER" requires-channel="https"/>
</sec:http>
<sec:authentication-manager alias="authenticationManager"/>

<bean id="casProcessingFilter" class="org.springframework.security.ui.cas.CasProcessingFilter">
<sec:custom-filter after="CAS_PROCESSING_FILTER"/>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureHandler">
<bean class="org.springframework.security.ui.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/casfailed.jsp"/>
</bean>
</property>
<property name="authenticationSuccessHandler">
<bean class="org.springframework.security.ui.SimpleUrlAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/"/>
</bean>
</property>
<property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
<property name="proxyReceptorUrl" value="/secure/receptor" />
</bean>

<bean id="casProcessingFilterEntryPoint" class="org.springframework.security.ui.cas.CasProcessingFilterEntryPoint">
<property name="loginUrl" value="https://localhost:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>

<bean id="casAuthenticationProvider" class="org.springframework.security.providers.cas.CasAuthenticationProvider">
<sec:custom-authentication-provider />
<property name="userDetailsService" ref="userService"/>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg index="0" value="https://localhost:9443/cas" />
<property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
<property name="proxyCallbackUrl" value="https://localhost:8443/cas-sample/secure/receptor" />
</bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>

<bean id="proxyGrantingTicketStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl" />

<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="https://localhost:8443/cas-sample/j_spring_cas_security_check"/>
<property name="sendRenew" value="false"/>
</bean>

<sec:user-service id="userService">
<sec:user name="rod" password="rod" authorities="ROLE_SUPERVISOR,ROLE_USER" />
<sec:user name="dianne" password="dianne" authorities="ROLE_USER" />
<sec:user name="scott" password="scott" authorities="ROLE_USER" />
</sec:user-service>
</beans>
Nie podoba mi się odwołanie do https://localhost:8443/cas-sample w pliku konfiguracyjnym. Niby jest do zmiany podczas wdrażania aplikacji, ale wystarczy zmiana kontekstu webowego i już muszę pamiętać, aby zmienić coś w deskryptorze (!) Tutaj oczekiwałbym jakiejś zmiany.

Jeszcze tylko zmiana w web.xml i rozpoczynam testowanie.

Testy zakończyły się niepowodzeniem:
 Caused by: org.springframework.beans.factory.CannotLoadBeanClassException: 
Cannot find class [org.springframework.security.ui.SimpleUrlAuthenticationFailureHandler] for
bean with name 'org.springframework.security.ui.SimpleUrlAuthenticationFailureHandler#e3f429'
defined in ServletContext resource [/WEB-INF/applicationContext-security.xml]
bo klasa org.springframework.security.ui.SimpleUrlAuthenticationFailureHandler dostępna jest dopiero od wersji Spring Security 2.5.0-SNAPSHOT. Aby z niej skorzystać należy rozbudować konfigurację pom.xml o
 <build>
<extensions>
<extension>
<groupId>org.springframework.aws</groupId>
<artifactId>spring-aws-maven</artifactId>
<version>1.2.2</version>
</extension>
</extensions>
</build>
<repositories>
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>s3://maven.springframework.org/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
Po tym posypało się większymi uaktualnieniami, gdyż
 11:09:55,421 WARN  [BasicLifecycleMonitor] Exception occured while notifying listener
java.lang.NoClassDefFoundError: org/springframework/beans/factory/config/BeanExpressionResolver
Zdaje się, że jest to pierwszy raz, kiedy przyjdzie mi skorzystać ze Spring Framework 3.0.0.M1, bo ta klasa dopiero w tej wersji się pojawiła - org.springframework.beans.factory.config.BeanExpressionResolver. W takiej sytuacji mam dwa podejścia - brnąć dalej w uaktualnienia licząc, że ze zmianami nie przyjdzie mi zajmować się problemami, które wynikają z błędów w oprogramowaniu, a nie mojej nieznajomości Spring Security, albo po prostu doczytać, co należy zmienić, aby nie korzystać z nowości Spring Security 2.5.0-SNAPSHOT. Na razie wybieram podejście pierwsze - brnę dalej (i cichutko się modlę).

W komentarzach do komunikatu o Spring Framework 3.0.0.M1 można znaleźć odpowiedź dla poszukujących repozytorium mavenowego dla tego wydania.

Przede wszystkim zmieniamy artifactId na odpowiadający pakietowi (konwencja zapożyczona z nazewnictwa pakunków OSGi, którą SpringSource krzewi przez Spring-DM, a następnie SpringSource dm Server) i dodajemy odpowiednie repozytoria:
 <properties>
<spring.version>3.0.0.M1</spring.version>
</properties>
...
<repository>
<id>SpringSource Enterprise Bundle Repository - External Bundle Milestones</id>
<url>http://repository.springsource.com/maven/bundles/milestone</url>
</repository>
<repository>
<id>SpringSource Enterprise Bundle Repository - SpringSource Bundle Releases</id>
<url>http://repository.springsource.com/maven/bundles/release</url>
</repository>
<repository>
<id>SpringSource Enterprise Bundle Repository - External Bundle Releases</id>
<url>http://repository.springsource.com/maven/bundles/external</url>
</repository>
...
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.web</artifactId>
<version>${spring.version}</version>
</dependency>
Zbudować zbudowałem, ale przy uruchomieniu aplikacji pojawił się komunikat o niedostępności org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean, więc dodałem kolejną zależność do projektu:
 <dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.orm</artifactId>
<version>${spring.version}</version>
</dependency>
Ponownie budowanie i wdrożenie do Geronimo. Teraz jest cacy! Nawet działa! Coś jeszcze będę musiał poczytać o CASie (albo przegadać temat z Michałem Margielem, który oferował swoją pomoc w temacie - podobno się zna ;-)).

Oczywiście korzystając z atrybutu autowire możnaby znacząco uprościć plik konfiguracyjny Springa applicationContext-security.xml, gdzie wiązania typu <property name="authenticationManager" ref="authenticationManager"/> byłyby tworzone dynamicznie przez Spring Framework.