S právě nabytými znalostmi o architektuře rámce Acegi
Security™ si teď ukážeme popsané třídy v akci, a to opět s
využitím mírně upravené přiložené vzorové webové aplikace, kde je
Acegi Security použito™ pro řízení přístupu
uživatelů k jednotlivým webovým zdrojům identifikovaným pomocí
URL (viz soubor
WEB-INF\spring\applicationContext-acegi.xml).
Integrace služeb Acegi Security™ do
webové aplikace je realizována pomocí klasických
filtrů specifikace servletů. V souboru
web.xml webové aplikace tedy musíme uvést
následující deklaraci:
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Třída
FilterToBeanProxy ve skutečnosti pouze deleguje
zpracování na objekt (filtr), který je definován v aplikačním kontextu
a jehož třída je definována pomocí atributu
targetClass. Tak lze pro klasické filtry naší
aplikace využít všech služeb, které aplikační kontext rámce Spring
nabízí. Třída se stejnými delegujícími vlastnostmi existuje i v
samotném rámci Spring
(org.springframework.web.filter.DelegatingFilterProxy),
Acegi Security však z historických důvodů poskytuje vlastní
implementaci.
Situace je však ještě zdánlivě komplikována (ve skutečnosti však
zjednodušena) tím, že objekt třídy
org.acegisecurity.util.FilterChainProxy, na
který zpracování delegujeme, je pouhým zapouzdřením řetězce vlastních
funkcionalitu poskytujících filtrů, rovněž definovaných v aplikačním
kontextu. Definice filtrů v konfiguračním XML
souboru
(WEB-INF\spring\applicationContext-acegi.xml),
který je načítán stejným způsobem jako ostatní konfigurační soubory
kořenového aplikačního kontextu Spring™
webové aplikace (viz „Startujeme aplikaci“), jsou následující:
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,
exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>
<bean id="httpSessionContextIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
<property name="context"
value="org.acegisecurity.context.SecurityContextImpl" />
</bean>
<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureUrl"
value="/login.html?login_error=1" />
<property name="defaultTargetUrl" value="/index.html" />
<property name="filterProcessesUrl" value="/j_acegi_security_check" />
</bean>
<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</bean>
<bean id="authenticationEntryPoint"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.html" />
<property name="forceHttps" value="false" />
</bean>
<bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/secure/client/**/*=ROLE_CLIENT,ROLE_ADMIN
/secure/**/*=ROLE_ADMIN
</value>
</property>
</bean>
První deklarace představuje zmiňovaný řetězec filtrů, při čemž lze pomocí klasické Ant-like syntaxe specifikovat, který řetěz filtrů bude aplikován na které požadované URL. V našem případě bude uvedený řetěz aplikován na všechna požadovaná URL. Pořadí filtrů v řetězu je velmi důležité a nedodržení uvedeného pořadí je častým zdrojem chyb.
Prvním filtrem v pořadí je filtr třídy
HttpSessionContextIntegrationFilter. Ten má za
úkol vyjmutí objektu SecurityContext (a v něm
obsaženého objektu Authentication příslušného
aktuálnímu uživateli) z uživatelského sezení (z objektu
javax.servlet.http.HttpSession) a jeho vložení
do aktuálního vlákna pomocí objektu
SecurityContextHolder. Neexistuje-li dosud
uživatelské sezení či neobsahuje-li toto sezení objekt
SecurityContext a tedy ani
Authentication, pak je vytvořen nový prázdný
objekt SecurityContext (konkrétně defaultní
implementace
org.acegisecurity.context.SecurityContextImpl)
a tento je pak vložen do vlákna požadavku. Po ukončení zpracování
požadavku je filtrem opět z objektu
SecurityContextHolder vyzvednut potenciálně
změněný objekt SecurityContext a je uložen zpět
do uživatelského sezení, kde je uchován do příštího požadavku daného
uživatele.
Filtr třídy
AuthenticationProcessingFilter má na starosti
integraci prezentační vrstvy naší aplikace (přihlašovacího formuláře)
s rámcem Acegi Security za účelem poskytnutí uživatelovy identity
(uživatelského jména) a jejího důkazu (hesla). V případě požadavku na
chráněný webový zdroj a současné absence objektu
Authentication v objektu
SecurityContextHolder je uživatel přesměrován
na přihlašovací formulář, jehož URL je definováno
prostřednictvím následujícího deklarovaného filtru třídy
ExceptionTranslationFilter a asociovaného
objektu třídy
AuthenticationProcessingFilterEntryPoint.
Samotný přihlašovací formulář může vypadat například takto (v
přiložené aplikaci je obsáhlejší kód v JSP šabloně
WEB-INF/jsp/login.jsp potažmo ve vložené šabloně
WEB-INF/jsp/include/login.jsp):
<form action="j_acegi_security_check" method="post"> <input type="text" name="j_username"/> <input type="password" name="j_password" /> <input class="button" type="submit" value="Odeslat" /> </form>
Klíčová je hodnota atributu
action, která se musí shodovat s hodnotou
vlastnosti
filterProcessesUrl filtru
authenticationProcessingFilter. Názvy parametrů
obsahujících uživatelské jméno a heslo musí dále být
j_username a j_password.
Filtr tyto údaje vloží do objektu
Authentication a nechá je prověřit asociovaným
objektem AuthenticationManager. V případě
úspěšného ověření je uživatel přesměrován na původně požadovanou
URL. Přistoupil-li k přihlašovacímu formuláři
přímo, pak je po úspěšné autentizaci přesměrován na adresu definovanou
atributem defaultTargetUrl filtru
authenticationProcessingFilter. V případě neúspěšné
autentizace je uživatel přesměrován na adresu definovanou atributem
authenticationFailureUrl.
No a konečně filtr
filterInvocationInterceptor definuje pomocí
Ant-like vzorů přístupové atributy pro jednotlivé webové zdroje, tedy
pro jednotlivá URL. V našem případě jde o
přístupové atributy - řetězce - s prefixem ROLE_,
čímž imitujeme klasické řízení přístupu na základě uživatelských rolí.
Jednotlivé vzory jsou vyhodnocovány jeden po druhém, což je nutno při
jejich návrhu brát v úvahu. Pro rozhodnutí o přístupu k chráněné
URL je použit asociovaný objekt
accessDecisionManager.
Pro ověření předložených přihlašovacích údajů potřebuje objekt
authenticationManager samozřejmě definovat datový
zdroj, který mu pro toto rozhodnutí poskytne podklady. Existuje
několik typů těchto zdrojů, jejichž podpora je v rámci
Acegi Security™ standardně k dispozici. Lze
použít specifikaci všech uživatelských jmen a hesel systému v
konfiguračním XML souboru (vhodné pouze pro
testovací účely), předdefinovaný přístup do databáze pomocí
JDBC spojení, uložení přihlašovacích údajů v
LDAP serveru, ale je samozřejmě možné vytvořit si i
vlastní strategii přístupu k datovému skladu s přihlašovacími údaji
systému. V našem případě půjde o vlastní implementaci přístupu k těmto
údajům prostřednictvím naší vlastní servisní vrstvy.
Následují související definice v konfiguračním souboru:
<bean id="authenticationManager"
class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
</list>
</property>
</bean>
Objekt authenticationManager,
konkrétně použitá implementace ProviderManager,
definuje seznam tzv. poskytovatelů
autentizace (authentication providers), kterým je jednomu po druhém
předáván objekt třídy Authentication, dokud jej
některý z nich úspěšně neautentizuje či nezamítne.
<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="hibernateAuthenticationDaoImpl"/>
<property name="userCache" ref="userCache"/>
<property name="saltSource" ref="saltSource"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
<bean id="hibernateAuthenticationDaoImpl"
class="cz.morosystems.sportportal.spring.HibernateAuthenticationDaoImpl">
<property name="userManager" ref="userManager"/>
</bean>
Námi použitý poskytovatel autentizace pak pro
získání reprezentace uživatele z databáze na základě předloženého
uživatelského jména uloženého v objektu
authentication použije naši implementaci strategie
pro přístup k databázi, tedy třídu
cz.morosystems.sportportal.spring.HibernateAuthenticationDaoImpl.
Název třídy je odvozen od faktu, že pro přístup k databázi používá
přiložená vzorová aplikace technologii
Hibernate™. Kód třídy je jednoduchý:
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.springframework.dao.DataAccessException;
public class HibernateAuthenticationDaoImpl implements UserDetailsService{
private UserManager userManager;
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
User userFromDB = userManager.getUserByUsername(username);
if (userFromDB==null) {
throw new UsernameNotFoundException("User not found");
}
if (userFromDB.getAuthorities().length==0) {
throw new UsernameNotFoundException("User has no GrantedAuthority");
}
return userFromDB;
}
... userManager set/get metody ...
}
Z databáze je takto získán doménový objekt třídy
cz.morosystems.sportportal.pojo.User,
reprezentující uživatele systému, implementující rozhraní
UserDetails a obsahující uživatelské jméno i
heslo. Heslo ze získaného objektu je porovnáno s heslem v objektu
Authentication a v případě kladné odpovědi je
uživatel autentizován.
V reálné aplikaci je většinou nepřípustné ukládat hesla v databázi v čitelném tvaru a ve většině případů je pro zašifrování hesla v databázi použit nějaký hashovací algoritmus:
<bean id="passwordEncoder"
class="org.acegisecurity.providers.encoding.ShaPasswordEncoder"/>
<bean id="saltSource"
class="org.acegisecurity.providers.dao.salt.SystemWideSaltSource">
<property name="systemWideSalt" value="abcba" />
</bean>
V našem případě tedy v poskytovateli autentizace
registrujeme speciální objekt rozhraní
org.acegisecurity.providers.encoding.PasswordEncoder,
konkrétně implementaci ShaPasswordEncoder.
Předpokládáme pak, že v databázi není uloženo heslo, ale pouze jeho
SHA1 hash, navíc ještě přisolený pomocí námi definovaného
řetězce. Při srovnání předloženého hesla s heslem z databáze tedy náš
poskytovatel autentizace nejprve vytvoří přisolený SHA1 hash
předloženého hesla a teprve ten srovná s hodnotou získanou z
databáze.
Je samozřejmě nutné, aby bylo heslo každého nového uživatele
systému před uložením do databáze zakódováno pomocí téhož objektu
passwordEncoder a téhož objektu
saltSource.
Vzhledem k tomu, že není žádoucí, abychom při každém požadavku autentizovaného uživatele přistupovali do databáze, registrujeme v našem autentizačním poskytovateli i jednu z rámcem Spring™ poskytovaných implementací kešovací strategie a sice strategii postavenou na populárním projektu EHCache™™.
<bean id="userCacheBackend"
class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager" ref="cacheManager"/>
<property name="cacheName" value="userCache" />
</bean>
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
<bean id="userCache"
class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
<property name="cache" ref="userCacheBackend"/>
</bean>
Poslední částí příkladu jsou definice související s třídou
AccessDecisionManager:
<bean id="accessDecisionManager"
class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<ref local="adminAccessToClientSectionVoter" />
<ref local="roleVoter"/>
</list>
</property>
</bean>
<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter"/>
<bean id="adminAccessToClientSectionVoter"
class="cz.morosystems.sportportal.spring.AdminAccessToClientSectionVoter"/>
Objekt
rozhraní AccessDecisionManager obsahuje seznam
objektů rozhraní
org.acegisecurity.vote.AccessDecisionVoter a
jednomu po druhém (konkrétně jejich metodě
vote) předává objektovou reprezentaci
chráněného cíle, jeho přístupové atributy a objekt
Authentication. Objekt
AccessDecisionVoter má tři alternativy
návratové hodnoty. Může zamítnout přístup objektu
Authentication k chráněnému cíli, může jej
povolit, ale může se také zdržet rozhodnutí. Na základě návratových
hodnot metody vote všech těchto objektů
rozhodne i accessDecisionManager. Implementace
AffirmativeBased, kterou jsme použili my,
rozhodne kladně v případě, že alespoň jeden z objektů
AccessDecisionVoter rozhodne kladně.
Rámec Acegi Security poskytuje třídu
RoleVoter, velmi často to používanou
implementaci rozhraní AccessDecisionVoter,
která pracuje s výše uvedenými přístupovými atributy s prefixem
ROLE_ a interpretuje je jako
uživatelské role, které mají k danému chráněnému cíli přístup. Přístup k
těmto cílům pak udělí těm uživatelům, kteří mají mezi svými
přístupovými atributy právě požadovanou roli.
Druhá deklarovaná implementace rozhraní
AccessDecisionVoter je součástí přiložené
vzorové aplikace a demonstruje další možnost, jak přizpůsobit chování
rámce Acegi Security specifickým potřebám aplikace, která jej
používá.