20. května 2009

Spring security namespaces

Koncept "namespaců" resp. možnost vytváření vlastních konfiguračních XML tagů je ve Springu již od verze 2.0 a již je celkem hodně zajímavých tagů - ať už přímo ve Spring frameworku nebo v jiných Spring knihovnách nebo i v knihovnách třetích stran, např. DWR. Cíl je jasný - umožnit jednodušší (= rychlejší, přehlednější, jasnější, ...) konfiguraci Spring beanů.

Spring security přišel s podporou namespaců ve verzi 2.0. Sice moc namespaci nevyužívám (zvyk je železná košile), ale nedalo mi to, abych možnosti namespaců ve Spring security nevyzkoušel na jedné menší testovací aplikaci.

Začal jsem následující magickou ukázkou v dokumentaci:

<http auto-config='true'>
<intercept-url pattern="/**" access="ROLE_USER" />
</http>

<authentication-provider>
<user-service>
<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>
</authentication-provider>

A pak jsem to začal upravovat podle mojí "standardní" konfigurace Spring security. Většinu jsem dokázal pořešit pouze konfigurací na úrovní namespaců, ale ne vždy to bylo dostačující:
  • u LogoutFiltru jsem zvyklý používat vlastní LogoutHandler pro zalogování potřebných informací při odhlášení. Abych mohl použít vlastní handler, tak jsem si musel filter nakonfigurovat standardním způsobem s použitím <custom-filter position="LOGOUT_FILTER"/>

  • podobně u použití vlastního AccessDeniedHandleru. V rámci konfigurace <http> je již implicitní ExceptionTranslationFilter nastaven. Stejně jako u LogoutFiltru jsem musel provést konfiguraci filtru standardní cestou a uvést, že se jedná o vlastní filtr. Pozice vlastního filtru nesmí kolidovat z pozicí implicitního filtru, takže buď je potřeba dát implicitní filtr úplně pryč (neuvádět ho v <http>) a nebo umístit vlastní filtr před/za nějaký již existující, např. u ExceptionTranslationFilteru.

Takto jsem se dostal hodně daleko a konfigurace aplikace již vypadala skoro dle mých představ - uměla vše co jsem požadoval, místy byl zápis opravdu čitelnější a kratší. A i s použitím namespaců jsem mohl být flexibilní, snad všechny implicitní konfigurace jsem mohl nahradit svými, např. použít vlastní AccessDecisionManager

Jen jedna věc mi pořád vadila - při použití <http> se všechny uvedené filtry aplikují na všechny URL. Nemám tedy možnost říci, že na nějaké URL použiji nějaké filtry a na jiné URL zase jiné filtry. Použité filtry je např. vhodné odlišit pro dynamický a statický obsah. Z tohoto důvodu jsem opustil konfiguraci pomocí <http> a použil jsem standardní FilterChainProxy s <filter-chain-map>.

Na začátku jsem měl pár tagů a na konci jsem skončil skoro u "normálního" nastavení bez namespaců. I tak jsem byl velice mile překvapen, jak pěkně to mají navržené a jakou míru flexibility to má. Já Spring Security již celkem znám a i při použití namespaců vím, co se děje pod pokličkou, proto nevím, zda je tato zjednodušená forma konfigurace vhodná i pro začátečníky. Začátek s namespaci bude určitě rychlý, ale v každé aplikaci je potřeba něco nastavit jinak než standardně a pak bude problém.

Pro možnou inspiraci posílám moji výslednou konfiguraci:

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

<description>
Konfigurace Spring security.
</description>

<!--
Base filter chain of used filters. Filter's order is important,
more info here http://static.springframework.org/spring-security/site/reference/html/ns-config.html#ns-custom-filters.
-->
<bean id="springSecurityFilterChain" class="org.springframework.security.util.FilterChainProxy">
<security:filter-chain-map path-type="ant">
<security:filter-chain pattern="/img/**" filters="none" />
<security:filter-chain pattern="/css/**" filters="none" />
<security:filter-chain pattern="/js/**" filters="none" />
<security:filter-chain pattern="/accessdenied.*" filters="anonymousProcessingFilter" />
<security:filter-chain pattern="/error*.*" filters="anonymousProcessingFilter" />
<security:filter-chain pattern="/**"
filters="sessionContextIntegrationFilter, logoutFilter, userProfileStubFilter, authProcessingFilter,
userProfileAwareFilter, anonymousProcessingFilter, exceptionTranslationFilter,
filterInvocationInterceptor"
/>
</security:filter-chain-map>
</bean>


<!-- Musi byt, i kdyz neni mozne vyuzivat session. -->
<bean id="sessionContextIntegrationFilter"
class="org.springframework.security.context.HttpSessionContextIntegrationFilter">
<property name="allowSessionCreation" value="true"/>
</bean>

<bean id="logoutFilter" class="org.springframework.security.ui.logout.LogoutFilter">
<!-- URL redirected to after logout -->
<constructor-arg value="/"/>
<constructor-arg>
<list>
<bean class="com.o2bs.globals.web.springsecurity.utils.LoggingLogoutHandlerImpl"/>
<bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler"/>
</list>
</constructor-arg>
<property name="filterProcessesUrl" value="/j_spring_security_logout"/>
</bean>

<!-- "natvrdo" nastaveni prihlaseneho uzivatele (pouze pro vyvoj) -->
<bean id="userProfileStubFilter" class="com.o2bs.globals.web.springsecurity.utils.UserProfileStubFilter">
<constructor-arg value="123"/>
<constructor-arg value="bob"/>
<constructor-arg value="heslo"/>
<constructor-arg>
<list>
<value>ROLE_WRITE</value>
<value>ROLE_READ</value>
</list>
</constructor-arg>
</bean>

<!-- Processes an authentication form. -->
<bean id="authProcessingFilter"
class="com.o2bs.globals.web.springsecurity.auth.blocking.AuthenticationBlockingProcessingFilter">
<security:custom-filter after="AUTHENTICATION_PROCESSING_FILTER" />
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureUrl" value="/login.do?login_error=1"/>
<property name="defaultTargetUrl" value="/"/>
<property name="authenticationBlockingManager" ref="authBlockingManager"/>
<property name="mandatoryRoles">
<list>
<value>ROLE_WRITE</value>
<value>ROLE_READ</value>
</list>
</property>
</bean>

<bean id="authBlockingManager"
class="com.o2bs.globals.web.springsecurity.auth.blocking.PeriodBlockingManagerMemoryImpl">
</bean>

<bean class="com.o2bs.globals.web.springsecurity.auth.blocking.AuthenticationFailureListener">
<property name="authenticationBlockingManager" ref="authBlockingManager"/>
</bean>

<bean id="userProfileAwareFilter"
class="com.o2bs.globals.web.springsecurity.utils.UserProfileHolderAwareRequestFilter">
</bean>

<!-- I need to use IS_AUTHENTICATED_ANONYMOUSLY - anonymous user has to be handled -->
<bean id="anonymousProcessingFilter"
class="org.springframework.security.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="anonymKey"/>
<property name="userAttribute" value="anonymousUser,ROLE_READ"/>
</bean>

<bean id="exceptionTranslationFilter" class="org.springframework.security.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.do"/>
</bean>
</property>
<property name="accessDeniedHandler">
<!-- errorPage neni definovana, protoze se vyhazuje 403 a ta je namapovana ve web.xml -->
<bean class="com.o2bs.globals.web.springsecurity.utils.LoggingAccessDeniedHandlerImpl"/>
</property>
</bean>

<!-- protect web URIs -->
<bean id="filterInvocationInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<security:filter-invocation-definition-source>
<security:intercept-url pattern="/create*" access="ROLE_WRITE" />
<security:intercept-url pattern="/**" access="ROLE_READ" />
</security:filter-invocation-definition-source>
</property>
</bean>





<!--
AffirmativeBased implementation will grant access if one or more ACCESS_GRANTED votes were
received (ie a deny vote will be ignored, provided there was at least one grant vote).
In other words principal must have corresponding ROLE and particular level of authentication.
-->
<bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false"/>
<property name="decisionVoters">
<list>
<!-- class will vote if any ConfigAttribute begins with PERM_. -->
<bean class="org.springframework.security.vote.RoleVoter">
<property name="rolePrefix" value="ROLE_"/>
</bean>
<!-- allow attributes IS_AUTHENTICATED_FULLY or IS_..._REMEMBERED or IS_..._ANONYMOUSLY -->
<bean class="org.springframework.security.vote.AuthenticatedVoter"/>
</list>
</property>
</bean>

<!-- There is implicit authenticationManager when namespaces are used. -->
<security:authentication-manager alias="authenticationManager" />

<security:authentication-provider>
<security:user-service>
<security:user name="jimi" password="heslo" authorities="ROLE_READ"/>
<security:user name="bob" password="heslo" authorities="ROLE_READ, ROLE_WRITE"/>
</security:user-service>
</security:authentication-provider>

<security:global-method-security access-decision-manager-ref="accessDecisionManager">
<security:protect-pointcut
expression="execution(* com.o2bs.globals.example.competence.service.*.save*(..))"
access="ROLE_WRITE"/>
</security:global-method-security>

<bean class="com.o2bs.globals.web.springsecurity.utils.LoggingAuthenticationListener"/>

</beans>

1 komentář:

xmarek řekl(a)...

Poté co jsem začal upravovat svoji konfiguraci v Acegi do Spring Security jsem narazil na Váš článek, který mi byl v mém úsilí velmi nápomocen. Proto Vám zasílám svůj Dík.