Pre-Authentication Spring Security-vel

Felhasznált technológiák: Spring Security 3.2.5, Jetty 9.1.0.v20131115

Vannak olyan esetek, mikor ugyan Spring Security-t szeretnénk használni, azonban autentikációra meghagynánk a webkonténer, vagy az alkalmazásszerver mechanizmusát. Ilyen eset lehetséges, mikor alkalmazásszerverben van beállítva az X.509 tanúsítványok kezelése, vagy esetleg az SSO-val való integráció.

Ebben az esetben az alkalmazásszerver végzi a bejelentkeztetést, esetleg még a jogosultságok kiosztását is, de a többit a Spring Security végzi. Az alkalmazásszerver a bejelentkeztetett felhasználót és jogosultságait a szabványos módon adja át, lekérdezni a Servlet API HttpServletRequest példányának getUserPrincipal() és isUserInRole(java.lang.String role) metódusaival lehet. A Spring Security-t tehát arra kell rábeszélni, hogy a megfelelő esetekben ide delegálja a hívásait.

A Spring Security használata azért lehet hasznos ebben az esetben is, mert a Servlet API-hoz képest rengeteg plusz funkciót ad, mint pl. URL-ek védelme, amihez a jogosultságokat bonyolult kifejezésekkel adhatjuk meg. Vagy pl. az annotáció alapú deklaratív jogosultságkezelés, stb. A Spring Security-ről bővebben egy előző posztomban olvashatsz, melyet ismét frissítettem, hogy a legfrissebb verziókat tartalmazza.

Jelenlegi poszthoz egy példaprogram is letölthető a GitHub-ról. Az mvn jetty:run paranccsal indítható. Bejelentkezni az admin1/admin1 és a user1/user1 felhasználónév jelszó párosokkal lehetséges. és Egy Jetty beépített webkonténert tartalmaz, melybe fel vannak véve a felhasználók, és a hozzá tartozó szerepkörök. Első körben a Jetty Maven pluginnak kell megmondani, hogy hol található a Jetty-hez tartozó konfigurációs állomány.

<plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>9.1.0.v20131115</version>
    <configuration>
            <webAppXml>src/main/webapp/WEB-INF/jetty.xml</webAppXml>
    </configuration>
</plugin>

Amennyiben ez megvan, az src/main/webapp/WEB-INF/jetty.xml konfigurációs állományban kell megadni, hogy mely állomány tartalmazza a felhasználókat és szerepköröket.

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Get name="securityHandler">
        <Set name="loginService">
            <New class="org.eclipse.jetty.security.HashLoginService">
                <Set name="name">UserRealm</Set>
                <Set name="config">
                    src/main/webapp/WEB-INF/jetty-realm.properties</Set>
                <Call name="start"/>
            </New>
        </Set>
        <Set name="checkWelcomeFiles">true</Set>
    </Get>
</Configure>

A src/main/webapp/WEB-INF/jetty-realm.properties egy elég egyszerű állomány, elöl a felhasználónév, majd a jelszó (plain-text-ben), majd a szerepkör. Teszteléshez tökéletes.

admin1: admin1,admin
user1: user1,user

Majd megadjuk a web.xml-ben, hogy Basic autentikációt használjon. Ilyenkor a böngésző feldob egy ablakot, és az autentikációs információk a HTTP kérés fejlécében utaznak, plain textben. Ez már szabványos Servlet API megoldás.

<security-constraint>
    <web-resource-collection>
        <web-resource-name>allwebresource</web-resource-name>
        <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>user</role-name>
        <role-name>admin</role-name>
    </auth-constraint>
</security-constraint>

<login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>UserRealm</realm-name>
</login-config>

<security-role>
    <role-name>user</role-name>
</security-role>

<security-role>
    <role-name>admin</role-name>
</security-role>

Ezután már csak a Spring Security-t konfiguráltam be az applicationContext-security.xml állományban, hogy a web konténertől vegye át a felhasználót és a hozzá tartozó szerepköröket. Nézzük az ehhez tartozó konfigurációt.

<http entry-point-ref="entryPoint" auto-config="false">
        <intercept-url pattern="/index.html" 
            access="IS_AUTHENTICATED_ANONYMOUSLY" />
        <intercept-url pattern="/user.html" 
            access="ROLE_USER,ROLE_ADMIN" />
        <intercept-url pattern="/admin.html" 
            access="ROLE_ADMIN" />
        <custom-filter position="PRE_AUTH_FILTER" 
            ref="preAuthFilter" />
</http>

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="authenticationProvider" />
</authentication-manager>

<beans:bean id="entryPoint"
    class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/>

<beans:bean id="authenticationProvider"
        class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <beans:property name="preAuthenticatedUserDetailsService" 
        ref="userDetailsService"/>
</beans:bean>

<beans:bean id="userDetailsService" 
    class="jtechlog.springsecurity.service.JpaAuthenticationUserDetailsService"/>

<beans:bean id="preAuthFilter" 
        class="org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter">
    <beans:property name="authenticationManager" 
        ref="authenticationManager"/>
    <beans:property name="authenticationDetailsSource">
        <beans:bean 
                class="org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource">
            <beans:property name="mappableRolesRetriever">
                <beans:bean 
                    class="org.springframework.security.web.authentication.preauth.j2ee.WebXmlMappableAttributesRetriever" />
            </beans:property>
            <beans:property name="userRoles2GrantedAuthoritiesMapper">
                <beans:bean 
                        class="org.springframework.security.core.authority.mapping.SimpleAttributes2GrantedAuthoritiesMapper">
                    <beans:property name="convertAttributeToUpperCase" 
                        value="true"/>
                </beans:bean>
            </beans:property>
        </beans:bean>
    </beans:property>
</beans:bean>

Ez talán már igényelhet némi magyarázatot. Az entryPoint bean mondja meg, hogy a konténerre van bízva az autentikáció. Az authenticationProvider mondja meg, hogy honnan kell a felhasználót feltölteni. Itt egy PreAuthenticatedAuthenticationProvider példányt használunk, ami azt mondja, hogy az autentikációt már elvégezte a konténer, és ennek eredménye alapján tölthetünk be saját User példányt. Ezt a saját JpaAuthenticationUserDetailsService példányunk teszi, adatbázisból a felhasználónév alapján JPA technológiával. A preAuthFilter bean a web.xml-ben talált szerepköröket mappeli át Spring Security-sra, nagybetűsít, és alapértelmezetten hozzáfűzi a ROLE_ prefixet. tehát az admin és a user szerepkörből csinál ROLE_ADMIN és ROLE_USER szerepköröket, vagy ahogy a Spring Security hívja, granted authority-ket.

A saját JpaAuthenticationUserDetailsService lényeg része a következőképp néz ki.

@Override
public UserDetails 
        loadUserDetails(PreAuthenticatedAuthenticationToken token) 
        throws UsernameNotFoundException {
    try {        
        User user = entityManager
            .createQuery("select u from User u where u.username = :username", User.class)
            .setParameter("username", token.getName())
            .getSingleResult();

        PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails details =
            (PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails) token.getDetails();

        user.setAuthorities(details.getGrantedAuthorities());
        return user;
    } catch (NoResultException nre) {
        throw new UsernameNotFoundException("A felhasználó a megadott felhasználónévvel nem található: " + token.getName(), nre);
    }
}

A paraméterként kapott token már tartalmazza az alkalmazásszerver által meghatározott felhasználónév és szerepkör információkat, ez alapján betöltjük adatbázisból a felhasználót, és beállítjuk a szintén megkapott szerepköröket.