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