Ochrana přístupu k webovým zdrojům

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).

Filtry

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.

Rozhodnutí o autentizaci

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>

Rozhodnutí o autorizaci

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á.

Komentáře

Téma neobsahuje žádné komentáře.

Vložit komentář

Můžete používat značkovací jazyk Texy!


Jméno:
E-mail:
Url:
Komentář:
1 + 2 =
 
MoroSystems, s.r.o.