späť na návody

Autentifikácia a autorizácia používateľov voči Active Directory pomocou Grails Spring Security 2.0 Plugin (Acegi 0.5.1)

9.6.2009 o 13:14, alon

Aktuálne pracujem na projekte, ktorý má ako implementačnú požiadavku autentifikáciu a autorizáciu používateľov voči Active Directory. Za účelom autentifikácie a autorizácie používateľov voči Active Directory som použil zásuvný modul Grails Spring Security, ktorý takúto funkčnosť poskytuje. O skúsenosti s jeho použitím a konfiguráciou by som sa chcel podeliť.

Inštalácia

Domovská stránka zásuvného modulu sa nachádza na URL http://grails.org/plugin/acegi (existujú nekonzistencie v pomenovaniach, ktoré sú dôsledkom premenovania Acegi Security na Spring Security). Pre inštaláciu zásuvného modulu je potrebné použiť štandardný príkaz grails install-plugin. Verzia 0.5.1 bude nainštalovaná po zadaní:

grails install-plugin acegi 0.5.1

Konfigurácia

Konfigurácia zásuvného modulu je uchovávaná v konfiguračnom súbore grails-app/conf/SecurityConfig.groovy. Implicitný konfiguračný súbor zásuvného modulu som upravil tak aby dochádzalo k nasledujúcemu:

  • overeniu prihlasovacích údajov používateľa
  • získaniu a overeniu oprávnení a rolí používateľa
  • definícií oprávení na úrovni akcií radičov (pomocou anotácií)
  • zakázaniu uchovávania akýchkoľvek informácií týkajúcich sa používateľa mimo Active Directory (implicitne sa dáta uchovávajú v databáze)
  • zakázaniu uchovávania mapovania URL na role v databáze

Konfiguračný súbor po aplikovaní zmien vyzerá nasledovne:

security {
active = true

// URL filtra, ktorý je zodpovedný za overenie prihlasovacích údajov
// používateľa
filterProcessesUrl = '/prihlasenie/filter'

// zakázanie získania mapovaní URL na role v databáze
useRequestMapDomainClass = false

// oprávnenia na základe ktorých bude povolené vykonanie akcie používateľovi
// budú definované v anotáciách pre akciách radičov
useControllerAnnotations = true
// povolenie autentifikácie a autorizácie používateľov voči Active Directory
useLdap = true

// zakázanie získania oprávenení a rolí z databázy
ldapRetrieveDatabaseRoles = false

// povolenie získania oprávenení a rolí z Active Directory
ldapRetrieveGroupRoles = true

// nasledujúce parametre sú použité v beane
// org.springframework.security.ldap.DefaultSpringSecurityContextSource, ktorá
// je zodpovedná za získanie pripojenia k Active Directory, musí mať definovaný
// server a port na ktorom je prevádzkovaný Active Directory a prihlasovacie
// meno a heslo používateľa pod ktorým dôjde k získaniu pripojenia
ldapServer = 'ldap://10.8.0.10:389'
ldapManagerDn = 'CN=pouzivatel,CN=users,DC=domain,DC=dev,DC=test,DC=lesipa,DC=sk'
ldapManagerPassword = 'heslo'

// nasledujúce parametre sú použité v beane
// org.springframework.security.ldap.search.FilterBasedLdapUserSearch, ktorá je
// zodpovedná za vyhľadávania používateľov na základe prihlasovacieho mena
ldapSearchBase = 'CN=users,DC=domain,DC=dev,DC=test,DC=lesipa,DC=sk'
ldapSearchFilter = '(sAMAccountName={0})'

// nasledujúce parametre sú použité v beane
// org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator,
// ktorá je zodpovedná za vyhľadávanie oprávnení a rolí na základe DN
// (distinguished name) používateľa
ldapGroupSearchBase = 'CN=users,DC=domain,DC=dev,DC=test,DC=lesipa,DC=sk'
ldapGroupSearchFilter = '(member={0})'
}

Autentifikácia

Autentifikácia používateľa sa uskutoční po prístupe na URL, ktoré je nadefinované v konfiguračnej vlastnosti filterProcessesUrl. Proces autentifikácie je vyvolaný v org.codehaus.groovy.grails.plugins.springsecurity.GrailsAuthenticationProcessingFilter. K samotnému rozhodnutiu či používateľ poskytol platné prihlasovacie údaje dochádza v org.springframework.security.ldap.authentication.BindAuthenticator pokusom o vytvorenie inštancie org.springframework.ldap.core.DirContextAdapter. V prípade, že sa inštanciu nepodarí vytvoriť je prístup používateľovi odmietnutý - autentifikácia je neúspešná.

Autorizácia

Autorizácia používateľa sa uskutoční pri prístupe na akciu radiča. Akcia radiča musí mať nadefinovanú anotáciu org.codehaus.groovy.grails.plugins.springsecurity.Secured, napríklad:

@Secured(['ROLA_SPRAVCA'])

Role sú získavané na základe konfiguračných vlastností ldapGroupSearchBase, ldapGroupSearchFilter pomocou org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator. Tento prístup je vhodný na získanie rolí, ktoré sú definované na jedne úrovni. Role, ktoré pozostávajú z ďalších rolí pri použití tohto prístupu nebudú získané. Je potrebné vytvoriť vlastnú implementáciu, ktorá tieto role získa. Takúto implementáciu je najjednoduchšie vytvoriť dedením z triedy org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator a prepísaním jej metódy getAdditionalRoles, ktorá by mohla vyzerať nasledovne:

@Override
protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations pOperations, String pUsername) {
SearchControls sc = new SearchControls()
sc.setSearchScope(SearchControls.SUBTREE_SCOPE)

SpringSecurityLdapTemplate sslt = new SpringSecurityLdapTemplate(getContextSource())
sslt.setSearchControls(sc)

Set priamePrava = new HashSet()
Set nepriamePrava = new HashSet()
sslt.searchForSingleAttributeValues(getGroupSearchBase(), groupSearchFilter, [pOperations.getNameInNamespace(), pUsername] as String[], groupRoleAttribute).each {pravo ->
dajVsetkyNepriamePrava(sslt, "CN=${pravo},${getGroupSearchBase()}", priamePrava, nepriamePrava)
priamePrava << pravo
}

Set prava = new HashSet()
nepriamePrava.each {pravo ->
prava << new GrantedAuthorityImpl(convertToUpperCase ? pravo.toUpperCase() : pravo)
}

prava
}

private void dajVsetkyNepriamePrava(SpringSecurityLdapTemplate pSSLT, String pPravo, Set pPriamePrava, Set pNepriamePrava) {
DirContextOperations operacie = pSSLT.retrieveEntry(pPravo, ['objectClass', 'member'] as String[])
for (String objekt in operacie.getStringAttributes('objectClass')) {
if (objekt == 'group') {
for (String clen in operacie.getStringAttributes('member')) {
if (!pNepriamePrava.contains(clen)) {
dajVsetkyNepriamePrava(pSSLT, clen, pPriamePrava, pNepriamePrava)
if (clen.startsWith('CN=tis:') && !pPriamePrava.contains(clen) && !pNepriamePrava.contains(clen)) {
pNepriamePrava << pSSLT.retrieveEntry(clen, ['cn'] as String[]).getStringAttribute('cn')
}
}
}
break
}
}
}

Aby boli role získavané pomocou tejto implementácie musí dôjť ku zmene v konfiguračnom súbore grails-app/conf/spring/resources.groovy, musí byť pridaný nasledujúci kód:

def konfiguraciaZabezpecenia = org.codehaus.groovy.grails.plugins.springsecurity.AuthorizeTools.securityConfig.security

ldapAuthoritiesPopulator(sk.lesipa.test.MojPopulatorRoli, ref('contextSource'), konfiguraciaZabezpecenia.ldapGroupSearchBase) {
groupRoleAttribute = konfiguraciaZabezpecenia.ldapGroupRoleAttribute
groupSearchFilter = konfiguraciaZabezpecenia.ldapGroupSearchFilter
searchSubtree = konfiguraciaZabezpecenia.ldapSearchSubtree
}

Získanie hodnôt atribútov zo záznamu používateľa

Okrem rolí je možné z Active Directory získať akúkoľvek hodnotu atribútu zo záznamu používateľa. Napríklad identifikátor, skutočné meno používateľa, adresu elektronickej pošty atď.. Získanie hodnôt atribútov typu reťazec je priamočiare. Pre získanie hodnôt typu binárne dáta je potrebné uskutočniť určitú konfiguráciu. Získanie hodnoty atribútu typu reťazec a typu binárne dáta bude demonštrované na príklade.

Implicitne sú údaje o používateľovi získavané pomocou triedy org.codehaus.groovy.grails.plugins.springsecurity.ldap.GrailsLdapUserDetailsMapper. Táto trieda neposkytuje mapovanie hodnôt atribútov, ktoré reprezentujú identifikátor a skutočné meno používateľa. Bude musieť vyť vytvorená nová implementácia, ktorá tieto dáta sprístupní. Najjednoduchším spôsobom je dedenie novej triedy z triedy org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator a prepísanie metódy mapUserFromContext, ktorá by mohla vyzerať nasledovne:

@Override
UserDetails mapUserFromContext(DirContextOperations pOperations, String pUsername, GrantedAuthority[] pAuthorities) {
def userDetails = super.mapUserFromContext(pOperations, pUsername, pAuthorities)

def id = dajHodnotuId(pOperations.getObjectAttribute('objectGUID'))
def skutocneMeno = pOperations.getStringAttribute('displayName')

new MojPouzivatel(userDetails.username, 'not_used', userDetails.enabled, userDetails.accountNonExpired, userDetails.credentialsNonExpired, userDetails.accountNonLocked, userDetails.authorities, userDetails.attributes, userDetails.dn, null, id, skutocneMeno)
}

Získanie skutočného mena používateľa je nekomplikované pretože sa jedná o hodnotu typu reťazec. Získanie identifikátora používateľa je komplikovanejšie pretože sa jedná o binárne dáta, ktoré sú do textovej formy prevedené nasledujúcou metódou:

private String akoRetazec(byte[] pObjectGuid) {
def cast1 = ''
def cast2 = ''
def cast3 = ''
def cast4 = ''
def cast5 = ''

for (int i = 0; i < pObjectGuid.length; i++) {
if (i > 9) {
cast5 += akoHexa(pObjectGuid[i])
} else if (i > 7 && i <= 9) {
cast4 += akoHexa(pObjectGuid[i])
} else if (i > 5 && i <= 7) {
cast3 = akoHexa(pObjectGuid[i]) + cast3
} else if (i > 3 && i <= 5) {
cast2 = akoHexa(pObjectGuid[i]) + cast2
} else {
cast1 = akoHexa(pObjectGuid[i]) + cast1
}
}

'{' + [cast1, cast2, cast3, cast4, cast5].join('-') + '}'
}

private String akoHexa(byte pByte) {
def hexa = Integer.toHexString(pByte & 0xFF)
hexa = hexa.length() == 1 ? '0' + hexa : hexa
hexa.toUpperCase()
}

ktorá vychádza z informácií uvedených na http://forums.sun.com/thread.jspa?threadID=646111&tstart=120. Navyše, aby bolo možné binárne dáta získať korektne musí dôjsť k atualizácii konfiguračného súboru grails-app/conf/spring/resources.groovy, musí byť pridaný nasledujúci kód:

contextSource(org.springframework.security.ldap.DefaultSpringSecurityContextSource, konfiguraciaZabezpecenia.ldapServer) {
userDn = konfiguraciaZabezpecenia.ldapManagerDn
password = konfiguraciaZabezpecenia.ldapManagerPassword
baseEnvironmentProperties = ['java.naming.ldap.attributes.binary' : 'objectGUID']
}

Metóda nakoniec vytvára inštanciu triedy MojPouzivatel:

final class MojPouzivatel extends GrailsLdapUser {
def id
def realname

MojPouzivatel(String pUsername, String pPassword, boolean pEnabled, boolean pAccountNonExpired, boolean pCredentialsNonExpired, boolean pAccountNonLocked, GrantedAuthority[] pAuthorities, Attributes pAttributes, String pDn, Object pDomainClass, String pId, String pRealname, String pEmail) {
super(pUsername, pPassword, pEnabled, pAccountNonExpired, pCredentialsNonExpired, pAccountNonLocked, pAuthorities, pAttributes, pDn, pDomainClass)
id = pId
realname = pRealname
}
}

ktorá môže byť získaná v radiči nasledujúcim spôsobom:

def authenticateService

@Secured(['ROLA_SPRAVCA'])
def infoPouzivatel = {
def pouzivatel = authenticateService.principal()
render "${pouzivatel.id} : ${pouzivatel.realname}"
}

Podobne ako bolo tomu pri vlastnej implementácii získania rolí aj v tomto prípade musí dôjsť k zmene konfiguračného súboru grails-app/conf/spring/resources.groovy, musí byť pridaný nasledujúci kód:

ldapUserDetailsMapper(sk.lesipa.test.MojMapperPouzivatela) {
}