Thursday, December 14, 2006

Acegi/Spring Security with X509 and LDAP Authentication

We have chosen Acegi security for our security interface for our web applications. We have decided to use X509 certificates to obtain user’s credential and roles. If the certificate is not available or expired, we then show the login page having the user enter the user name and password. To accomplish this we used a special XML configuration and had to create 3 new classes.

EhCacheX509LdapUserCache.java (source)
This class is used to search the cache for the user. We are using two different authenticator providers and because each provider implements the cache the differently, this class is used to provide a bridge between the two concepts by delegating methods to the underlying store.

SecurityContextHolderAwareRequestWrapperWrapper.java (source)
This class wraps SecurityContextHolderAwareRequestWrapper because the SecurityContextHolderAwareRequestFilter api requires both request and port resolver, however, Acegi’s SecurityContextHolderAwareRequestWrapper only supports request. Hopefully Acegi will fix this problem in the future, removing the need for this class.

X509LdapAuthoritiesPopulator.java (source)
We are using 2 authorities populators so our class implements the X509 interface and delegates to the LDAP authority populator. We expect to obtain the userid from the certificate or login page while we obtain the roles from LDAP. One note of interest is that we use Perl to simplify the use of regular expressions because of the need to search the CN string for the user name.

How they all tie together
We have enabled these all by creating a Spring configuration that utilizes them. In addition, we needed a login controller to redirect the user to their desired URI upon successful authentication (only necessary for form-based login). The last step is to make sure that your web.xml uses the correct servlet mappings, the following is how we organized ours:

<servlet-mapping>
<servlet-name>Phoenix</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Phoenix</servlet-name>
<url-pattern>/login_error</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Phoenix</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>


Source


EhCacheX509LdapUserCache.java (top)
package com.checkernet.security;

import java.security.cert.X509Certificate;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.Element;
import org.acegisecurity.providers.dao.UserCache;
import org.acegisecurity.providers.x509.X509UserCache;
import org.acegisecurity.userdetails.UserDetails;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.util.Assert;

/**
 * Notes:<br/>
 * Implements {@link org.acegisecurity.providers.x509.X509UserCache}
 * and {@link org.acegisecurity.providers.dao.UserCache} <br />
 * Used to provide a bridge between the two concepts by delegating methods to the underlying store
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
@SuppressWarnings({"OverloadedMethodsWithSameNumberOfParameters"})
public class EhCacheX509LdapUserCache implements UserCache, X509UserCache, InitializingBean {

    private final Log logger = LogFactory.getLog(EhCacheX509LdapUserCache.class);

    private Cache cache;

    private Pattern subjectDNPattern;

    private String subjectDNRegex = "CN=(.*?),";

    public void setCache(final Cache cache) {
        this.cache = cache;
    }

    /**
     * Obtains a {@link UserDetails} from the cache.
     *
     @param username the {@link org.acegisecurity.userdetails.User#getUsername()} used to place the user in
     *                 the cache
     @return the populated <code>UserDetails</code> or <code>null</code> if
     *         the user could not be found or if the cache entry has expired
     */
    public UserDetails getUserFromCache(final String username) {
        final Element element;

        try {
            element = cache.get(username);
        catch (CacheException cacheException) {
            //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
            throw new DataRetrievalFailureException("Cache failure: " + cacheException.getMessage());
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Cache hit for userName: " + username);
        }

        if (element == null) {
            //noinspection ReturnOfNull
            return null;
        else {
            return (UserDetailselement.getValue();
        }
    }

    /**
     * Obtains a {@link UserDetails} from the cache.
     *
     @param userCert the {@link X509Certificate} used to place the user in
     *                 the cache
     @return the populated <code>UserDetails</code> or <code>null</code> if
     *         the user could not be found or if the cache entry has expired
     */
    public UserDetails getUserFromCache(final X509Certificate userCert) {
        if (userCert != null && userCert.getSubjectDN() != null) {
            return getUserFromCache(extractUsernameFromCert(userCert));
        else if (logger.isDebugEnabled()) {
            logger.debug("Certificate or user is null");
        }

        //noinspection ReturnOfNull
        return null;
    }

    /**
     * Places a {@link UserDetails} in the cache. The <code>username</code> is
     * the key used to subsequently retrieve the <code>UserDetails</code>.
     *
     @param user the fully populated <code>UserDetails</code> to place in the
     *             cache
     */
    public void putUserInCache(final UserDetails user) {
        Element element = new Element(user.getUsername(), user);

        if (logger.isDebugEnabled()) {
            logger.debug("Cache put: " + user.getUsername());
        }

        cache.put(element);
    }

    /**
     * delegates to #putUserInCache(final UserDetails user)
     *
     @param userCert user's X509Certificate
     @param user     the fully populated <code>UserDetails</code> to place in the
     *                 cache
     */
    public void putUserInCache(final X509Certificate userCert, final UserDetails user) {
        putUserInCache(user);
    }

    /**
     * Removes the specified user from the cache. The <code>username</code> is
     * the key used to remove the user. If the user is not found, the method
     * should simply return (not thrown an exception).
     <p></p>
     <p>Some cache implementations may not support eviction from the cache,  in
     * which case they should provide appropriate behaviour to alter the user
     * in either its documentation, via an exception, or through a log
     * message.</p>
     *
     @param username to be evicted from the cache
     */
    public void removeUserFromCache(final String username) {
        if (logger.isDebugEnabled()) {
            logger.debug("Cache remove: " + username);
        }

        cache.remove(username);
    }

    /**
     * delegates to #removeUserFromCache(final String username)
     *
     @param userCert user's X509Certificate
     */
    public void removeUserFromCache(final X509Certificate userCert) {
        removeUserFromCache(extractUsernameFromCert(userCert));
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(cache, "cache is mandatory");

        final Perl5Compiler compiler = new Perl5Compiler();

        //noinspection UnusedCatchParameter
        try {
            subjectDNPattern = compiler.compile(subjectDNRegex,
                                                Perl5Compiler.READ_ONLY_MASK
                                                | Perl5Compiler.CASE_INSENSITIVE_MASK);
        catch (MalformedPatternException mpe) {
            //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
            throw new IllegalArgumentException("Malformed regular expression: "
                                               + subjectDNRegex);
        }
    }

    /**
     * extracts a username from a certificate's subjectDN
     *
     @param userCert X509Certificate
     @return String username
     */
    private String extractUsernameFromCert(final X509Certificate userCert) {
        final PatternMatcher matcher = new Perl5Matcher();

        if (userCert == null || userCert.getSubjectDN() == null
            || !matcher.contains(userCert.getSubjectDN().toString(), subjectDNPattern)) {
            //noinspection ReturnOfNull
            return null;
        }

        final MatchResult match = matcher.getMatch();

        if (match.groups() != 2) { // 2 = 1 + the entire match
            throw new IllegalArgumentException(
                    "Regular expression must contain a single group ");
        }

        return match.group(1);
    }
}


SecurityContextHolderAwareRequestWrapperWrapper.java (top)
package com.checkernet.security;

import javax.servlet.http.HttpServletRequest;
import org.acegisecurity.util.PortResolver;
import org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper;

/**
 * Notes:<br />
 * This class acts as a wrapper for org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper because it does
 * not conform to the SecurityContextHolderAwareRequestFilter api. This class may be removed when Acegi fixes this bug.
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
@SuppressWarnings({"ClassNamePrefixedWithPackageName""ClassNameSameAsAncestorName"})
public class SecurityContextHolderAwareRequestWrapperWrapper
        extends SecurityContextHolderAwareRequestWrapper {

    /**
     * conforming to SecurityContextHolderAwareRequestFilter api
     *
     @param request      HttpServletRequest
     @param portResolver PortResolver (not currently used)
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    public SecurityContextHolderAwareRequestWrapperWrapper(final HttpServletRequest request,
                                                           final PortResolver portResolver) {
        super(request);
    }
}


X509LdapAuthoritiesPopulator.java (top)
package com.checkernet.security;

import java.security.cert.X509Certificate;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.ldap.LdapUserSearch;
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
import org.acegisecurity.providers.x509.X509AuthoritiesPopulator;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

/**
 * Notes:<br/>
 * Implements the {@link X509AuthoritiesPopulator} while providing roles lookup from LDAP
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
@SuppressWarnings({"ClassNamingConvention"})
public class X509LdapAuthoritiesPopulator implements X509AuthoritiesPopulator, InitializingBean {

    private LdapUserSearch userSearch;

    private LdapAuthoritiesPopulator authoritiesPopulator;

    private Pattern subjectDNPattern;

    private String subjectDNRegex = "CN=(.*?),";

    public void setAuthoritiesPopulator(final LdapAuthoritiesPopulator authoritiesPopulator) {
        this.authoritiesPopulator = authoritiesPopulator;
    }

    public void setUserSearch(final LdapUserSearch userSearch) {
        this.userSearch = userSearch;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(userSearch, "A userSearch must be set");
        Assert.notNull(authoritiesPopulator, "An authoritiesPopulator must be set");

        final Perl5Compiler compiler = new Perl5Compiler();

        //noinspection UnusedCatchParameter
        try {
            subjectDNPattern = compiler.compile(subjectDNRegex,
                                                Perl5Compiler.READ_ONLY_MASK
                                                | Perl5Compiler.CASE_INSENSITIVE_MASK);
        catch (MalformedPatternException mpe) {
            //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
            throw new IllegalArgumentException("Malformed regular expression: "
                                               + subjectDNRegex);
        }
    }

    /**
     * throws AuthenticationException
     * {@inheritDoc}
     */
    public UserDetails getUserDetails(final X509Certificate userCertificate) {
        final String subjectDN = userCertificate.getSubjectDN().getName();
        final PatternMatcher matcher = new Perl5Matcher();

        if (!matcher.contains(subjectDN, subjectDNPattern)) {
            throw new BadCredentialsException("No matching pattern was found in subjectDN: {0}");
            /*throw new BadCredentialsException(messages.getMessage(
                    "DaoX509AuthoritiesPopulator.noMatching",
                    new Object[] {subjectDN},
                    "No matching pattern was found in subjectDN: {0}"));*/
        }

        final MatchResult match = matcher.getMatch();

        if (match.groups() != 2) { // 2 = 1 + the entire match
            throw new IllegalArgumentException(
                    "Regular expression must contain a single group ");
        }

        final String userName = match.group(1);
        LdapUserDetails userDetails = userSearch.searchForUser(userName);
        return new User(userName, "[PROTECTED]", true, true, true, true,
                        authoritiesPopulator.getGrantedAuthorities(userDetails));
    }

}


LoginController.java (top)
package com.checkernet.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.acegisecurity.ui.AbstractProcessingFilter;
import org.acegisecurity.ui.savedrequest.SavedRequest;
import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

/**
 * Notes:<br />
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
public class LoginController extends BaseController implements InitializingBean {

    /**
     * user-defined loginPage view name
     */
    protected String loginPage;

    public void setLoginPage(final String loginPage) {
        this.loginPage = loginPage;
    }

    /**
     * Login Page, redirects to saved request on authentication, otherwise goes to loginPage
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
            throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("login attempt: "
                         + request.getParameter(AuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY));
        }

        final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
            logger.debug("redirecting to first request");
            final Object savedRequest = request.getSession()
                    .getAttribute(AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY);
            if (savedRequest instanceof SavedRequest) {
                return new ModelAndView(new RedirectView(((SavedRequestsavedRequest).getFullRequestUrl()));
            else {
                logger.warn(AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY
                            " found in session not of correct type returning to login page");
            }
        }
        return new ModelAndView(loginPage);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(loginPage, "loginPage is required");
    }
}


x509Ldap-servlet.xml (top)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- This file describes standard concrete beans for security use,
as well as provides abstract beans for bean defaults -->

<!-- Automatically receives AuthenticationEvent messages -->
<bean id="loggerListener" class="org.acegisecurity.event.authentication.LoggerListener" />

<!--ldap configuration-->
<bean id="initialDirContextFactory" class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg value="ldap://checkernet.com:389" />
<property name="managerDn">
<value>cn=ReadOnlyUser,ou=webappgroup,dc=checkernet,dc=com</value>
</property>
<property name="managerPassword">
<value>bogusPassword</value>
</property>
<property name="extraEnvVars">
<map>
<entry key="java.naming.referral" value="follow" />
</map>
</property>
</bean>

<bean id="checkerboardLdapProvider" class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">
<constructor-arg>
<bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="userSearch">
<bean class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg>
<value>DC=checkernet,DC=com</value>
</constructor-arg>
<constructor-arg>
<value>(sAMAccountName={0})</value>
</constructor-arg>
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="searchSubtree" value="true" />
</bean>
</property>
</bean>
</constructor-arg>
<constructor-arg ref="ldapAuthoritiesPopulator" />
<!--you supply this part:-->
<!--<property name="userCache" ref="userCache" />-->
</bean>

<bean id="ldapAuthoritiesPopulator"
class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<constructor-arg>
<value>dc=checkernet,dc=com</value>
</constructor-arg>
<property name="groupSearchFilter">
<value>(member={0})</value>
</property>
<property name="groupRoleAttribute">
<value>CN</value>
</property>
<property name="searchSubtree" value="true" />
</bean>

<!--this bean specifies the special case of binding an x509 cert to an Ldap entry
it requires a userCache to exist-->
<bean id="x509LdapAuthoritiesPopulator" class="com.checkernet.security.X509LdapAuthoritiesPopulator">
<property name="userSearch">
<bean class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg>
<value>DC=checkernet,DC=com</value>
</constructor-arg>
<constructor-arg>
<value>(cn={0})</value>
</constructor-arg>
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="searchSubtree" value="true" />
</bean>
</property>
<property name="authoritiesPopulator">
<ref local="ldapAuthoritiesPopulator" />
</property>
</bean>

<!-- An access decision voter that reads ROLE_* configuration settings -->
<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter" />

<!-- An access decision manager used by the business objects -->
<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<ref local="roleVoter" />
</list>
</property>
</bean>

<!-- ================= METHOD INVOCATION AUTHORIZATION ==================== -->

<!--
NOTE:
users need to already be authenticated before invoking this security check
-->
<bean id="securityAttributes" class="org.acegisecurity.annotation.SecurityAnnotationAttributes" />

<!--AOP security config-->
<bean id="securityInterceptor" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="validateConfigAttributes" value="true" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<bean class="org.acegisecurity.intercept.method.MethodDefinitionAttributes">
<property name="attributes" ref="securityAttributes" />
</bean>
</property>
</bean>

<!--AspectJ security config-->
<bean id="securityInterceptorAspectJ" class="org.acegisecurity.intercept.method.aspectj.AspectJSecurityInterceptor">
<property name="validateConfigAttributes" value="true" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<bean class="org.acegisecurity.intercept.method.MethodDefinitionAttributes">
<property name="attributes" ref="securityAttributes" />
</bean>
</property>
</bean>

<!-- ======================== FILTER CHAIN ======================= -->

<!-- If you wish to use channel security, add "channelProcessingFilter," in front
of "httpSessionContextIntegrationFilter" in the list below -->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,securityContextHolderAwareRequestFilter,x509ProcessingFilter,authenticationProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>

<!-- ======================== AUTHENTICATION ======================= -->

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="x509AuthenticationProvider" />
<ref bean="ldapAuthenticationProvider" />
<ref bean="anonymousAuthenticationProvider" />
</list>
</property>
</bean>

<bean id="anonymousAuthenticationProvider"
class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key" value="fakeAnonymousKey" />
<!--must match anonymousProcessingFilter key-->
</bean>

<bean id="x509AuthenticationProvider" class="org.acegisecurity.providers.x509.X509AuthenticationProvider">
<property name="x509AuthoritiesPopulator" ref="x509LdapAuthoritiesPopulator" />
<property name="x509UserCache" ref="userCache" />
</bean>

<bean id="ldapAuthenticationProvider" class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider"
parent="checkerboardLdapProvider">
<property name="userCache" ref="userCache" />
</bean>

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" />

<bean id="userCache" class="com.checkernet.security.EhCacheX509LdapUserCache">
<property name="cache">
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager" ref="cacheManager" />
<property name="cacheName" value="x509Cache" />
</bean>
</property>
</bean>

<bean id="httpSessionContextIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
</bean>
<bean id="securityContextHolderAwareRequestFilter"
class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter">
<property name="wrapperClass" value="com.checkernet.security.SecurityContextHolderAwareRequestWrapperWrapper" />
</bean>

<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="/" />
<constructor-arg>
<list>
<ref bean="securityContextLogoutHandler" />
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
</list>
</constructor-arg>
<property name="filterProcessesUrl" value="/app/logout" />
</bean>
<bean id="securityContextLogoutHandler"
class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler">
</bean>


<!-- ===================== HTTP REQUEST SECURITY ==================== -->

<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="fakeAnonymousKey" />
<!--must match anonymousAuthenticationProvider key-->
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS" />
</bean>

<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl">
<value>/login</value>
</property>
<property name="forceHttps" value="true" />
</bean>
</property>
</bean>

<bean id="x509ProcessingFilter" class="org.acegisecurity.ui.x509.X509ProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationFailureUrl">
<value>/login_error</value>
</property>
<property name="defaultTargetUrl">
<value>/</value>
</property>
<property name="filterProcessesUrl">
<value>/login/check</value>
</property>
</bean>

<bean id="httpRequestAccessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<ref local="roleVoter" />
</list>
</property>
</bean>

<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager" />
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
\A/login.*\Z=ROLE_ANONYMOUS,ROLE_PHOENIX
\A/app/SecuredPage.*\Z=ROLE_SUPERUSER
\A/app/AnotherSecuredPage.*\Z=ROLE_SUPERUSER
\A/.*\Z=ROLE_PHOENIX
</value>
</property>
</bean>
</beans>