Acegi Security System for Spring

Acegi Security System for Spring

Acegi is an open-source security framework based on Spring. The Acegi authentication mechanism will plug in to almost any approach required (LDAP, DAO, etc). Authorization can be url-controlled and/or handled through method invocation (using AOP). For this discussion only authentication and url-controlled authorization will be described. Please see section “1.5. Security Interception” of the Acegi reference manual for details on AOP method-controlled authorization.

All code fragments are shown in their proper context within files listed at the end of this discussion. Please note that certain steps (such as user caching) are skipped in order to show more relevent points without causing information overload. 🙂 Review the attached configuration files for full details.

Credit goes to Kevin for doing the legwork required to figure Acegi out and get it working for the QLM Sourcing project.

Links of interest

Acegi Configuration

The first step is to add an Acegi security filter to your web.xml:

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

In order to separate the Aecgi configuration from other Spring configuration tags, import an Acegi config file in your Spring config file (eg. applicationContext.xml). Also shown is the setup for our user service bean (which depends on a DAO bean, not shown), which will be the hook for Acegi authentication:

<!-- Acegi Authentication -->
<import resource="applicationContext-acegi-security.xml"/>

<!-- UserService bean -->
<bean id="UserService"
      class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
  <property name="transactionManager">
    <ref bean="myTransactionManager"/>
  </property>
  <property name="target">
    <bean class="ca.fmfs.portal.service.user.UserServiceImpl" >
      <property name="userDAO">
        <ref local="UserDAO" />
      </property>
    </bean>
  </property>
  <property name="transactionAttributes">
    <props>
      <prop key="*">PROPAGATION_REQUIRED,-Exception</prop>
    </props>
  </property>
</bean>

Next, create the Acegi config file (in this case, named “applicationContext-acegi-security.xml”) and add some standard configuration tags. The ones below setup the filtering mechanism and the password encryption bean:

<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
      <property name="filterInvocationDefinitionSource">
         <value>
		    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
		    PATTERN_TYPE_APACHE_ANT

/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,rememberMeProcessingFilter,anonymousProcessingFilter,securityEnforcementFilter,switchUserP

rocessingFilter
         </value>
      </property>
    </bean>

   <bean id="passwordEncoder" class="org.acegisecurity.providers.encoding.Md5PasswordEncoder"/>

Now we need to setup the authentication configuration (again, within “applicationContext-acegi-security.xml”). This sets up DAO authentication, and hooks Acegi to our user service and our chosen password encryption:

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
      <property name="providers">
         <list>
            <ref local="daoAuthenticationProvider"/>
         </list>
      </property>
   </bean>

   <!-- Acegi will use our UserService bean to do authentication -->
   <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
      <property name="userDetailsService"><ref bean="UserService"/></property>
      <property name="passwordEncoder"><ref local="passwordEncoder"/></property>
   </bean>

   <bean id="basicProcessingFilter" class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
      <property name="authenticationManager"><ref local="authenticationManager"/></property>
      <property name="authenticationEntryPoint"><ref local="basicProcessingFilterEntryPoint"/></property>
   </bean>

   <bean id="basicProcessingFilterEntryPoint" class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
      <property name="realmName"><value>User Realm</value></property>
   </bean>

Now, using your web-framework of choice, you need to redirect a login attempt to Acgei (Login html page and all dependent files attached at end). Send a POST/GET to “/j_acegi_security_check” with parameters “j_username” and “j_password”. A Tapestry approach is shown below:

public void login(IRequestCycle cycle) throws FMFSServiceException {
	    getUserContext().setAttemptedLoginUserId(getUsername());
	    String acegiUrl = cycle.getAbsoluteURL("/j_acegi_security_check?j_username="+getUsername()+"&j_password="+getPassword());
        throw new RedirectException(acegiUrl);
	}

Back to the Acegi xml file, add url-controlled security (“channel” and “decision maker” setup requirements have been skipped):

<!-- some necessary Aecgi setup referencing some of the beans we have previously configured -->
   <bean id="securityEnforcementFilter" class="org.acegisecurity.intercept.web.SecurityEnforcementFilter">
      <property name="filterSecurityInterceptor"><ref local="filterInvocationInterceptor"/></property>
      <property name="authenticationEntryPoint"><ref local="authenticationProcessingFilterEntryPoint"/></property>
   </bean>

   <!-- more hooks into our specific project; where to go on login failure, where to go on login if no destination is specified, the url trigger for the filter -->
   <bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
      <property name="authenticationManager"><ref bean="authenticationManager"/></property>
      <property name="authenticationFailureUrl"><value>/Login.html?error=1</value></property>
      <property name="defaultTargetUrl"><value>/secure/FMFSHome.html</value></property>
      <property name="filterProcessesUrl"><value>/j_acegi_security_check</value></property>
   </bean>

   <!-- the security entry point (Login.html) -->
   <bean id="authenticationProcessingFilterEntryPoint" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
      <property name="loginFormUrl"><value>/Login.html</value></property>
      <property name="forceHttps"><value>false</value></property>
   </bean>

   <!-- various url filters based on (arbitrary) roles
   <!-- 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 bean="authenticationManager"/></property>
      <property name="objectDefinitionSource">
         <value>
			    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
			    PATTERN_TYPE_APACHE_ANT
			    /switchuser.jsp=ROLE_ADMINISTRATOR
			    /j_acegi_switch_user=ROLE_ADMINISTRATOR
			    /Home.*=ROLE_ADMINISTRATOR,ROLE_USER,ROLE_ANONYMOUS
			    /Login.*=ROLE_ADMINISTRATOR,ROLE_USER,ROLE_ANONYMOUS
			    /PageNotFound.*=ROLE_ADMINISTRATOR,ROLE_USER,ROLE_ANONYMOUS
			    /AccessDenied.*=ROLE_ADMINISTRATOR,ROLE_USER,ROLE_ANONYMOUS
				/secure/user/**=ROLE_ADMINISTRATOR,ROLE_USER
				/secure/admin/**=ROLE_ADMINISTRATOR
				/secure/**=ROLE_ADMINISTRATOR,ROLE_USER
         </value>
      </property>
   </bean>

Finally, we have to provide an implementation for Aecgi to use to do the actual authentication. First, we have a UserService interface that extends the Acegi UserDetails interface (not shown; file IUserService is attached). Now we provide an implementation of the “UserDetails loadUserByUsername(String userId)” method within our UserServiceImpl class. Given a userid, our code needs to create and return an Acegi UserDetails object:

public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException, DataAccessException {
        User user = null;
        GrantedAuthority[] grantedAuthorities = null;
        try {
            user = getUserDAO().lookupUser(userId);

            if(user==null) {
	            throw new UsernameNotFoundException("Invalid User");
	        }

	        Set roles = user.getRoles();
	        int i = 0;
	        grantedAuthorities = new GrantedAuthority[roles.size()];
	        for (Iterator iter = roles.iterator(); iter.hasNext(); i++) {
	            Role role = (Role) iter.next();

	            GrantedAuthority authority = new GrantedAuthorityImpl(role.getRole());
	            grantedAuthorities[i] = authority;
	        }
        } catch (DataStoreException e) {
            throw new DataRetrievalFailureException("Cannot loadUserByUsername userId:"+userId+ " Exception:" + e.getMessage(), e);
        }

        UserDetails userDetails = new org.acegisecurity.userdetails.User(
                user.getUserId(),
                user.getPassword(),
                user.isEnabled(), //enabled
                user.isEnabled(), //accountNonExpired
                user.isEnabled(), //credentialsNonExpired
                user.isEnabled(), //accountNonLocked
                grantedAuthorities
                );
        return userDetails;
    }

If we find a user with the given id, we setup all necessary details and pass the object back to Acegi to check for password comparison (authentication) or url-based role authorization.

When users are stored in our database we need to make sure we encrypt their password using the same mechanism setup within the Acegi configuration file, and that the user is assigned roles that match up to our url filters.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply