23. září 2007

Ukázka konfigurace Acegi security

Pro tento článek jsem vybral konfigurační soubor Acegi security pro náš jeden projekt. Rád bych pár slovy popsal jednotlivé body konfigurace a částečně tím prezentoval možnosti této knihovny. Já osobně považuji Acegi security za nejlepší knihovnu pro řešení bezpečnostních problémů spojených s vývojem aplikací, tedy hlavně s autentizací a autorizací uživatelů.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<!--
- Acegi Security configuration.
-->
<beans>

Definice filtrů pro různé adresy. Zmínil bych zde jen dvě důležité věci - pořadí filtrů je velice důležité a je možné definovat různé filtry pro různé URL adresy. Zápis je pomocí ANT stylu.

<!-- Zakladni bean s definici pouzitych filtru.
Poradi pouzitych filtru je dulezite! -->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/remoting/**=basicProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
/**=concurrentSessionFilter,httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>

ConcurrentSessionFilter aktualizuje informace o session a tím pádem je možné kontrolovat vypršení session pro daného uživatele.

<!-- Filter required by concurrent session handling package -->
<bean id="concurrentSessionFilter" class="org.acegisecurity.concurrent.ConcurrentSessionFilter">
<property name="expiredUrl" value="${acegi.expiredUrl}"/>
<property name="sessionRegistry" ref="sessionRegistry"/>
</bean>

HttpSessionContextIntegrationFilter se stará o přenos SecurityContext mezi jednotlivými HTTP voláními.

<!-- HttpSessionContextIntegrationFilter is responsible for storing a SecurityContext between HTTP requests -->
<bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>

LogoutFilter slouží k odhlášení přihlášeného uživatele (vymazání kontextu z registru). Definice obsahuje URL adresu, kam se má uživatel přesměrovat po odhlášení a seznam handlerů (implementace LogoutHandler), které se mají provést. Nezbytným handlerem je SecurityContextLogoutHandler, který vymazává informace o přihlášeném uživateli z registru. Kromě toho jsem si vytvořil handler pro účely logování.

<!-- Logs a principal out.
Use <a href="j_acegi_logout">Logout</a> on the page. -->
<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="${acegi.urlAfterLogout}"/> <!-- URL redirected to after logout -->
<constructor-arg>
<list>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
<bean class="cz.anect.securitymodule.ldap.LogoutHandlerImpl"/>
</list>
</constructor-arg>
</bean>

Filter pro zpracování požadavku na přihlášení. Standardně je možné použít AuthenticationProcessingFilter. Já jsem si standardní filtr upravil, protože jsem požadoval zablokování účtu po několika špatných pokusech o přihlášení. Tuto implementaci jsem popisoval v tomto článku.

<!-- Processes an authentication form. -->
<bean id="authenticationProcessingFilter" class="cz.anect.securitymodule.DisableAuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureUrl" value="${acegi.authenticationFailureUrl}"/>
<property name="defaultTargetUrl" value="/"/>
<property name="filterProcessesUrl" value="/j_acegi_security_check"/>
<property name="disabledAccountManager" ref="disabledAccountManager"/>
</bean>

BasicProcessingFilter zpracovává BASIC hlavičky z HTTP požadavků. Tento filtr používám pouze pro relativní adresy začínající "remoting". Na této adrese jsou totiž publikované vzdálené služby (remote services) a já pomocí Acegi zajišťuji autorizovaný přístup k těmto službám.
  
<bean id="basicProcessingFilter" class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="authenticationEntryPoint"><ref bean="authenticationEntryPoint"/></property>
</bean>

<bean id="authenticationEntryPoint" class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
<property name="realmName" value="MIS_REALM"/>
</bean>

<!-- A Filter which populates the ServletRequest with a new request wrapper. -->
<bean id="securityContextHolderAwareRequestFilter" class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"/>

AnonymousProcessingFilter slouží k vytváření anonymních uživatelů (= uživatelů, kteří nejsou přihlášeni). To má tu velkou výhodu, že já si zde mohu nadefinovat parametry anonymního uživatele a pak v aplikaci s tím pracovat jako s jakýmkoliv dalším uživatelem. Nepřihlášení uživatel tedy není null objekt, ale normální uživatel se specifickými vlastnostmi.

<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="anonymKey"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

ExceptionTranslationFilter zpracovává Java výjimky a vytváří odpovídající HTTP odpovědi.

<!-- Handles any AccessDeniedException and AuthenticationException thrown within the filter chain. -->
<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="${acegi.loginFormUrl}"/>
<property name="forceHttps" value="true"/>
</bean>
</property>
<property name="accessDeniedHandler">
<bean class="cz.anect.securitymodule.ldap.CustomAccessDeniedHandlerImpl">
<property name="errorPage" value="${acegi.accessDeniedUrl}"/>
</bean>
</property>
</bean>

FilterSecurityInterceptor řídí přístup k jednotlivým URL adresám. Nastavení URL adres mám v odděleném properties souboru, který uvedu na konci.

<!-- protect web URIs -->
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<value>${acegi.urlMapping}</value>
</property>
</bean>

ProviderManager prochází zaregistrované providery a snaží se získat odpověď na autentikační požadavek (např. přihlášení uživatele k aplikaci). Jednotlivé providery se procházejí v uvedeném pořadí a to do té doby, než nejaký provider vrátí odpověď.
    
<!-- authority providers -->
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
<ref bean="ldapAuthProvider"/>
<bean class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key" value="anonymKey"/>
</bean>
</list>
</property>
</bean>

DaoAuthenticationProvider je provider pro přístup k datům, které jsou uloženy v databázi nebo v paměti. Tento provider toho sám o sobě moc neumí, proto je potřeba zaregistrovat userDetailsService. Já tento provider používám pouze pro účely autorizace přístupu k publikovaným službám (=remote services).

<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService"/>
</bean>

InMemoryDaoImpl je nejjednodušší implementace UserDetailsService a umožňuje mi nadefinovat si uživatele např. pomocí properties souborů. Jak jsem již zmiňoval v minulém bodě, tak tento provider resp. UserDetailService používám pouze pro oveření přístupu ke vzdáleným službám. V properties souboru si nadefinuji fiktivní uživatele pro jednotlivé klienty vzdálených služeb a mám jednoduchý způsob, jak mohu ověřovat přístup.

<!-- In-memory implementation; all information is loaded from properties file -->
<bean id="userDetailsService" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userProperties">
<bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="classpath:/config/common/users.properties"/>
</bean>
</property>
</bean>

DefaultInitialDirContextFactory slouží pro připojení k LDAP serveru. Jednotlivé hodnoty jsou uloženy v properties souboru, který uvedu na konci článku.

<!-- =========================== LDAP ========================== -->
<!-- Pripojeni k LDAP serveru -->
<bean id="initialDirContextFactory" class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg value="${ldap.providerUrl}"/>
<property name="managerDn" value="${ldap.managerDn}"/>
<property name="managerPassword" value="${ldap.password}"/>
</bean>

LdapAuthenticationProvider je základní provider pro integraci s LDAP serverem. Nastavení LDAP provideru spočívá ve dvou hlavních věcech - jak bude probíhat autentikace uživatele a jak se budou načítat role k uživatelům. Tyto věci jsou závislé na struktuře LDAP serveru a proto je nutné toto nastavení upravit dle aktuální podoby. S ohledem na specifické požadavky autentikace na základě různých atributů v LDAP serveru jsem si vytvořil vlastní implementaci LDAP provideru.

<bean id="ldapAuthProvider" class="cz.anect.securitymodule.ldap.CustomLdapAuthenticationProvider">
<constructor-arg>
<!-- autentifikace uzivatele -->
<bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
<property name="userDnPatterns">
<list>
<value>${ldap.userDnPatterns1}</value>
<value>${ldap.userDnPatterns2}</value>
<value>${ldap.userDnPatterns3}</value>
<value>${ldap.userDnPatterns4}</value>
<value>${ldap.userDnPatterns5}</value>
<value>${ldap.userDnPatterns6}</value>
<value>${ldap.userDnPatterns7}</value>
<value>${ldap.userDnPatterns8}</value>
<value>${ldap.userDnPatterns9}</value>
<value>${ldap.userDnPatterns10}</value>
<value>${ldap.userDnPatterns11}</value>
<value>${ldap.userDnPatterns12}</value>
<value>${ldap.userDnPatterns13}</value>
<value>${ldap.userDnPatterns14}</value>
<value>${ldap.userDnPatterns15}</value>
<value>${ldap.userDnPatterns16}</value>
<value>${ldap.userDnPatterns17}</value>
<value>${ldap.userDnPatterns18}</value>
<value>${ldap.userDnPatterns19}</value>
<value>${ldap.userDnPatterns20}</value>
</list>
</property>
</bean>
</constructor-arg>
<constructor-arg>
<!-- nacteni roli k uzivateli -->
<bean class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
<constructor-arg value="${ldap.groupSearchBase}"/>
<property name="groupSearchFilter" value="${ldap.groupSearchFilter}"/>
<property name="groupRoleAttribute" value="${ldap.groupRoleAttribute}"/>
<property name="rolePrefix" value="${ldap.rolePrefix}"/>
<property name="convertToUpperCase" value="${ldap.convertToUpperCase}"/>
<property name="searchSubtree" value="true"/>
</bean>
</constructor-arg>
</bean>
<!-- =========================== LDAP ========================== -->

AffirmativeBased je jednoduchou implementací AccessDecisionManager. AccessDecisionManager rozhoduje o tom, zda přihlášený uživatel má dostatečná práva pro přístup k cílovému zdroji (to může být např. webová stránky na určité adrese, to může být metoda v Java rozhraní apod.). Pro můj konkrétní případ se kontroluje role daného uživatele (RoleVoter) a (pokud není první kontrola úspěšná) speciální atributy IS_AUTHENTICATED_FULLY nebo IS_AUTHENTICATED_REMEMBERED nebo IS_AUTHENTICATED_ANONYMOUSLY (AuthenticatedVoter) , které mohu použít v definici přístupu ke stránkám.

<!--
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.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false"/>
<property name="decisionVoters">
<list>
<!-- class will vote if any ConfigAttribute begins with ROLE_. -->
<bean class="org.acegisecurity.vote.RoleVoter"/>
<bean class="org.acegisecurity.vote.AuthenticatedVoter"/>
</list>
</property>
</bean>

MethodSecurityInterceptor slouží k ověřování přístupu na úrovni Java kódu.

<!-- method authorization in FACADE LAYER -->
<bean id="facadeSecurity" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="validateConfigAttributes"><value>true</value></property>
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<value>
cz.anect.mis.web.facade.DocumentFacade.addDocuments=ROLE_UZIVATELE, ROLE_SPRAVCIGLOBALNI, ROLE_SPRAVCILOKALNI, ROLE_ADDDOCUMENT_WITHOUTEDIT, ROLE_ADDDOCUMENT_WITHEDIT
cz.anect.mis.web.facade.DocumentFacade.addDocumentData=ROLE_ADDDOCUMENTDATA
</value>
</property>
</bean>

<!-- auto proxy for beans which we want to intercepts by security interceptor -->
<bean id="autoProxySecurityCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<idref bean="facadeSecurity"/>
</list>
</property>
<property name="beanNames">
<list>
<idref bean="DocumentFacade"/>
</list>
</property>
</bean>

Registrace listenerů pro účely logování a zachycení neúspěšných pokusů o přihlášení.
    
<!-- This bean is optional; it isn't used by any other bean as it only listens and logs -->
<bean id="loggerListener" class="cz.anect.securitymodule.CustomLoggerListener"/>

<bean id="failureListener" class="cz.anect.securitymodule.AuthentificationFailureListener">
<property name="disabledAccountManager" ref="disabledAccountManager"/>
</bean>

<bean id="disabledAccountManager" class="cz.anect.securitymodule.BasicDisabledAccountManager">
</bean>

<bean id="sessionRegistry" class="org.acegisecurity.concurrent.SessionRegistryImpl"/>

</beans>

Nyní ještě uvedu použité properties soubory.
###############################################
# property file: acegi_base.properties #
# format : key = value #
# Zakladni nastaveni pro Acegi knihovnu #
###############################################

acegi.expiredUrl =/expired.jsp
acegi.urlAfterLogout =/index.jsp
acegi.authenticationFailureUrl =/login.jsp?login_error=1
acegi.loginFormUrl =/login.jsp
acegi.accessDeniedUrl =/accessDenied.jsp



<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>
Mapovani mezi URLs a rolemi.
Poradi je dulezite, konstanty jsou definovany v AuthenticatedVoter:
IS_AUTHENTICATED_FULLY or IS_AUTHENTICATED_REMEMBERED or IS_AUTHENTICATED_ANONYMOUSLY.
!!! prvni dva radky nechat beze zmeny !!!
Dalsi radky jsou ve formatu: url = jmeno role (popr. vyse uvedene promenne)
</comment>
<entry key="acegi.urlMapping">
<![CDATA[
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/adddocument.*=ROLE_UZIVATELE, ROLE_SPRAVCIGLOBALNI, ROLE_SPRAVCILOKALNI
/adddocumentas.*=ROLE_UZIVATELE, ROLE_SPRAVCIGLOBALNI, ROLE_SPRAVCILOKALNI
/adddocumentdataonly.*=ROLE_ADDDOCUMENTDATA
/adddocumenttodata.*=IS_AUTHENTICATED_FULLY
/analogdocstoreadmin.*=IS_AUTHENTICATED_FULLY
/newdigiuser.*=IS_AUTHENTICATED_FULLY

/remoting/*=ROLE_REMOTE_SERVICES, ROLE_BINDED_APPLICATION
]]>
</entry>
</properties>


###########################################################
# property file: users.properties #
# format : key (username) = value (password, roles #
# List of technical users for accessing remote services #
###########################################################

system1=673yuededjei3yuy3bhyu3,ROLE_REMOTE_SERVICES
system2=eddjeihyu63jeui3j7337,ROLE_REMOTE_SERVICES


###############################################
# property file: ldap.properties #
# format : key = value #
# Udaje pro pripojeni k LDAP serveru #
###############################################

#-------------------- pripojeni k LDAPu
#This should be in the form ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org
ldap.providerUrl =ldap://
#directory user to authenticate (including base DN)
ldap.managerDn =
ldap.password =

#-------------------- autentizace uzivatele
#DN sablona pro vyhledavani uzivatele (max. 20 polozek)
ldap.userDnPatterns1 =uid={0},ou=uzivatele,ou=302
ldap.userDnPatterns2 =uid={0},ou=uzivatele,ou=311
ldap.userDnPatterns3 =uid={0},ou=uzivatele,ou=321
ldap.userDnPatterns4 =uid={0},ou=uzivatele,ou=331
ldap.userDnPatterns5 =uid={0},ou=uzivatele,ou=341
ldap.userDnPatterns6 =uid={0},ou=uzivatele,ou=342
ldap.userDnPatterns7 =uid={0},ou=uzivatele,ou=351
ldap.userDnPatterns8 =uid={0},ou=uzivatele,ou=353
ldap.userDnPatterns9 =uid={0},ou=uzivatele,ou=361
ldap.userDnPatterns10 =uid={0},ou=uzivatele,ou=362
ldap.userDnPatterns11 =uid={0},ou=uzivatele,ou=371
ldap.userDnPatterns12 =uid={0},ou=uzivatele,ou=373
ldap.userDnPatterns13 =uid={0},ou=uzivatele,ou=381
ldap.userDnPatterns14 =uid={0},ou=uzivatele,ou=391
ldap.userDnPatterns15 =uid={0},ou=uzivatele,ou=partneri
ldap.userDnPatterns16 =uid={0},ou=uzivatele,ou=verejnost
ldap.userDnPatterns17 =
ldap.userDnPatterns18 =
ldap.userDnPatterns19 =
ldap.userDnPatterns20 =

#-------------------- nacteni roli k uzivateli
#kontejner pro vyhledavani roli k teto aplikaci (DN bez zakladniho DN)
ldap.groupSearchBase =
#jmeno atributu u role, ktery obsahuje odkazy na uzivatele v dane skupine
ldap.groupSearchFilter =(roleOccupant={0})
#jmeno atributu, ktery se pouzije pro ziskani nazvu role
ldap.groupRoleAttribute =cn
#prefix ke jmenu role v LDAPu
ldap.rolePrefix =ROLE_
#maji se jmena roli prevadet na velka pismena? true|false
ldap.convertToUpperCase =true

2 komentáře:

Lukáš Vlček řekl(a)...

Moc pěkný článek. Mám tři dotazy:

1) K čemu přesně používáš securityContextHolderAwareRequestFilter ?

2) Je možné refreshovat Authorization object, který je uložen v contextu? Uvažujme například situaci, kdy uživatelé mohou přes web ui modifikovat svá práva, jak donutit acegi, aby se updatoval seznam rolí, které přihlášený uživatel má?

3) Pokud chci použít anonymousProcessingFilter, je nůtné definovat i AnonymousAuthenticationProvider a zařadit jej do seznamu ostatních providerů?

Petr Jůza řekl(a)...

Děkuji za pochvalu. K tvým dotazům:

1) Přesně k ničemu :). Mám to tam úplně od začátku a až teď po napsaní tohoto článku jsem si uvědomil, že to vůbec nepotřebuji. Sám přesně nevím, kdy bych tento filtr použil.

2) Nedávno jsem něco podobného řešil a myslím, že by to nějak šlo. Ale nakonec jsme se domluvili, že bude vhodnější, aby se uživatel odhlásil a pak znovu přihlásil. Myslím, že takto to řeší většina systémů. Kdyby jsi to nutně potřeboval měnit za běhu, tak napiš, zkusím něco najít.

3) Nutné to podle mého názoru není, ale vhodné jo - záleží co přesně potřebuješ. AnonymousProcessingFilter dělá pouze to, že vytvoří Authentication objekt pro anonymního uživatele a uloží do SecureContext.
AnonymousAuthenticationProvider je definován v authenticationManager, který rozhoduje o tom, zda daný uživatel může být "ověřen". Pokud zde nebude AnonymousAuthenticationProvider, tak pro anonymního uživatele to nenajde žádný záznam a tím pádem ho to nepustí na požadovaný cíl (stránka, metoda).