Acegi Security for Dummies

Probably this post is one of many Acegi Security Getting Started’s
to be found on the web. However we at Amis just recently went thru it. Here
is our step-by-step guide how to set up basic authentication and web request authorization.
We used a bit older version of the frameworks. However after going thru
the steps described below, you’ll end up with a basic understanding of this security system.

After
downloading the lab you’ll find an application that contains all layers
of the Spring framework: a basic Spring MVC tier and service tier that
uses Spring JDBC.
Acegi Security logo

1. Getting Up and Running

We are going to add security measures to an existing fully insecure
application created with the Spring framework. This application can be downloaded here: https://technology.amis.nl/wp-content/uploads/images/lab_acegi_security.zip. Before you can go ahead with implementing security on this application, you should perform the following steps:

  • Install Maven on the project
  • Install database schema
  • Prepare Tomcat
  • Import project into Eclipse

Install Maven on the project


Install Maven 2 (http://maven.apache.org/download.html). Start up DOS prompt. Navigate to directory where project is located (e.g.
../projects/acegi-lab). Issue command:

>mvn install

You
should see the Maven log message that says that pallas-0.1.war has
installed into local .m2/repository; and you should see the message:
build
successful. Next, create Eclipse project by issuing command

>mvn eclipse:eclipse

Now
you should see the message that says that Eclipse project has been
written
to local directory ../projects/acegi-lab. When you look in your local
project directory you’ll notice that the files .classpath, .project,
.wtpmodules and  a
directory called target has been added.

Install database schema

Install Oracle XE database (http://www.oracle.com/technology/software/products/database/xe/index.html).  Create user PALLAS/PALLAS. Grant
DBA privilege. Create objects and test data by running script
acegi-lab/src/main/sql/create_schema.sql . Modify data source URL
jdbc:oracle:thin:@localhost:1521:XE that is defined in file
acegi-lab/src/main/webapp/META-INF/context.xml. Change the SID XE into
you own SID.

Prepare Tomcat

Install Tomcat 5.5 (http://tomcat.apache.org/). and add the following jar files to CATALINA_HOME/common/lib:

  • commons-pool-1.2.jar
  • commons-dbcp-1.2.1.jar
  • commons-collections-3.1.jar
  • ojdbc14.jar

Import project into Eclipse

Install Eclipse 3.1. and start it. Go to
Window>Preferences>Server>Install Runtimes and add Tomcat. Go
to file>import and select Existing Projects into
Workspace. Select the project home (directory acegi-lab), and click
Finish. It can
be that Eclipse complains about missing directory .deployables and
shows a building
error. This can be ignored. Restart Eclipse are you are fine. Select
src>main>webapp>index.html. Choose from Right-click menu:
Run As>Run on Server. The application welcome page should appear.
Congratulations!

2. Implementing Authentication

The first step in building up the security for this application is
providing authentication. In Acegi the authentication is performed by
the AuthenticationManager. Therefore we need to create this class. As
for most objects in Spring this is done by wiring it in the application
context. So we have to go to the context configuration file of our
project. For convenience reasons this file is split into multiple files
that contain bean definitions grouped according to their role within
the application (could be any division). If we go to
src>main>webapp>WEB-INF, we see:

  • pallas-servlet.xml
  • dataAccessContext.xml
  • businessContext.xml
  • webContext.xml

Now we are going to add yet another one to this list, named:
securityContext.xml. Please add file securityContext.xml to directory
src>main>webapp>WEB-INF. This file should have the following
content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  -
</beans>

As you can see this file expects a couple of bean definitions. After
completion of this lab we’ll have added the beans that are represented
in the figure below. An arrow denotes a dependency that a bean has on
another bean:

Security beans

First we start with adding three filters:

<bean id="filterChainProxy" class="net.sf.acegisecurity.util.FilterChainProxy">
  <property name="filterInvocationDefinitionSource">
    <value>
      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
      PATTERN_TYPE_APACHE_ANT
      /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,securityEnforcementFilter
    </value>
  </property>
</bean>

The order in which the filters are listed above, defines the order
in which they are run. Next we add the bean definitions for the filters:

<bean id="httpSessionContextIntegrationFilter" class="net.sf.acegisecurity.context.HttpSessionContextIntegration Filter">
  <property name="context" value="net.sf.acegisecurity.context.security.SecureContextImpl" />
</bean>

<bean id="authenticationProcessingFilter" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
  <property name="authenticationManager">
    <ref bean="authenticationManager" />
  </property>
  <property name="authenticationFailureUrl" value="/login.html?error=1" />
  <property name="defaultTargetUrl" value="/" />
  <property name="filterProcessesUrl" value="/j_acegi_security_check" />
</bean>
	
<bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter">
  <property name="filterSecurityInterceptor" ref="filterSecurityInterceptor" />
  <property name="authenticationEntryPoint" ref="authenticationEntryPoint" />
</bean>

Into these filters other beans are injected. We start with the AuthenticationManager, the bean that does the authentication:

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

In its turn the AuthenticationManager depends on one (or more)
providers. We inject the DaoAuthenticationProvider, that is defined by:

<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
  <property name="authenticationDao">
    <ref local="memoryAuthenticationDao" />
  </property>
</bean>

Next we choose to inject a MemoryAuthenticationDao into the DaoAuthentiactionProvider, that on its turn is defined by:

<bean id="memoryAuthenticationDao" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl">
  <property name="userMap">
    <value>
      piet=hup,ROLE_EMPLOYEE,ROLE_MANAGER
      jan=geheim,ROLE_EMPLOYEE 
    </value>
  </property>
</bean>

A list of principals and their credentials are stored in memory.
Next we need to wire a couple of beans to finish this context file:

<bean id="filterSecurityInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor">
  <property name="authenticationManager"
    ref="authenticationManager" />
  <property name="accessDecisionManager"
    ref="httpRequestAccessDecisionManager" />
  <property name="objectDefinitionSource">
    <value>
      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
	PATTERN_TYPE_APACHE_ANT 
    </value>
  </property>
</bean>

The FilterSecurityInterceptor also uses the AuthenticationManager.
Furthermore an AccessDecisionManager is wired into it, that makes use
of a RoleVoter:

<bean id="httpRequestAccessDecisionManager" class="net.sf.acegisecurity.vote.AffirmativeBased">
  <property name="allowIfAllAbstainDecisions" value="false" />
  <property name="decisionVoters">
    <bean class="net.sf.acegisecurity.vote.RoleVoter" />
  </property>
</bean>

Finally we need to wire a AuthenticationEntryPoint into the SecurityEnforcementFilter:

<bean id="authenticationEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
  <property name="loginFormUrl" value="/login.html" />
  <property name="forceHttps" value="false" />
</bean>

Please save file securityContext.xml. In order to use this
securityContext, we need to add it to the list of context configuration
locations in web.xml. Please go ahead. For help see solution #1 below.

Next we need to register the FilterChainProxy bean in web.xml. In order to do so, add the following two XML elements to web.xml:

<filter>
  <filter-name>Acegi Filter Chain Proxy</filter-name>
    <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
  <init-param>
    <param-name>targetClass</param-name>
      <param-value>net.sf.acegisecurity.util.FilterChainProxy</param-value>
  </init-param>
</filter>

<filter-mapping>
  <filter-name>Acegi Filter Chain Proxy</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

This should be placed before the servlet element. Now it’s time to
create a logon HTML page. Remember that in the AuthenticationEntryPoint
bean above, the loginFormUrl is defined as login.html. Now we add a
login.jsp to src>main>webapp>WEB-INF>jsp, defined something
like:

<%@ page session="false"%>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ include file="taglibs.jsp"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title>Login</title>
  </head>
  <body>
    <%@ include file="banner.jsp"%>
    <h3>Login</h3>
    <c:if test="${model.errorTxtKey != null}">
      Invalid Username / Password
      <p>
    </c:if>     
    <form action="<c:url value="j_acegi_security_check"/>" method="POST">
      <table>
        <tr>
	  <td>Username :</td>
	  <td><input name="j_username" /></td>
	</tr>
	<tr>
          <td>Password :</td>
	  <td><input type="password" name="j_password" /></td>
	</tr>
	<tr>
	  <td><input type="submit" value="Login" /></td>
	  <td>&nbsp</td>
	</tr>
      </table>
    </form>   
  </body>
</html>

Still, at this point of our building process, the authentication
entry point, called login.html is not yet mapped to this login.jsp. In
order to get this mapping right, we need to add a new URL mapping in
context configuration XML file, called pallas-servlet.xml. Please add
the mapping of login.html to loginController. For help see solution #2.

As you can see the path /login.html that enters the DispatcherServlet
is routed to a new controller, called LoginController. This controller
does not yet exist in our project, so we need to create it. Now we
switch to the Java classes. Please open package nl.amis.pallas.web.
This package contains controllers that are part of Spring MVC. Create
Java file LoginController.java, and add the following code to it:

package nl.amis.pallas.web;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.*;

public class LoginController extends AbstractController {

	protected final Log logger = LogFactory.getLog(getClass());

	public ModelAndView handleRequestInternal(HttpServletRequest request,
			HttpServletResponse response) throws ServletRequestBindingException {
		String errorTxtKey = null;
		if (RequestUtils.getIntParameter(request, "error") != null) {
			errorTxtKey = "err.incorrect_usr_pwd";
		}

		// use pk to find model data
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("errorTxtKey", errorTxtKey);

		return new ModelAndView("login", "model", model);
	}
}

In Spring, creating a new Java class is not sufficient. What do we need to do next?

Yep, we need to wire it in a context configuration file. In this
case, we need to add the LoginController bean to the XML file
webContext.xml. Please try to add this bean to webContext.xml. For help
see solution #3 below.

When we run the application, we notice that authentication is not taken place. What would be the reason for this?

Indeed, we still miss a mapping of a URI pattern to a role, in order to
trigger the authentication. In order to facilitate this pattern
mapping, we will put URIs that we want to secure in a virtual path,
called secure/. Please modify the following files:

File Old New
welcome.jsp show_employee_list.html secure/show_employee_list.html
webContext.xml /show_employee_list.html /secure/show_employee_list.html
pallas-servlet.xml /*employee_list.html /secure/*employee_list.html
/add_employee.html /secure/add_employee.html
/*resume.html /secure/*resume.html

Finally, we need to add a URI filter pattern to the
FilterSecurityInterceptor bean that is defined in XML file
securityContext.xml. Please go ahead and adapt the value of the
ObjectDefinitionSource attribute of this Interceptor, below
PATTERN_TYPE_APACHE_ANT in the XML element below:

<bean id="filterSecurityInterceptor"
class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor">
  <property name="authenticationManager"
    ref="authenticationManager" />
  <property name="accessDecisionManager"
    ref="httpRequestAccessDecisionManager" />
  <property name="objectDefinitionSource">
    <value>
      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
	PATTERN_TYPE_APACHE_ANT 
    </value>
  </property>
</bean>

For help see solution #4.

3. Authorization

So far we have succeeded to get the authentication to work. During
this process we implemented authorization too, namely principals with
the granted authorities employee and manager are allowed to enter the
application and all of its (secure) pages. Now we will modify the
authorization by implementing the requirement that only managers are
allowed to add new employees. Please implement this requirement by
modifying the ObjectDefinitionSource attribute of the
FilterSecurityInterception. One way to do this is adding the following
line above the existing pattern:

/secure/manager/*=ROLE_MANAGER

This should be above the existing pattern because the patterns are evaluated from specific to more generic. So this results in:

/secure/manager/*=ROLE_MANAGER
/secure/*=ROLE_EMPLOYEE,ROLE_MANAGER

This must of course be complemented by modifying the paths located in the following files:

  • employee_list.jsp : manager/add_employee.html
  • pallas-servlet.xml : /secure/manager/add_employee.html

Please implement this authorization requirement and test the changes by
logging as manager with username = piet, and password = hup. The Acegi
tag library offers the possibility to test the granted authorities of
principals. So next we want to display the link to Add Employee page
only when the user is a manager. This can be accomplished by embedding
the link in employee_list.jsp into the tag:

<authz:authorize ifAnyGranted="ROLE_MANAGER">

Please go ahead and try it out.

4. Authentication thru JDBC

Often the authentication repository is located in a database that
contains a list of principals and their granted authorities. Therefore
we will build this in this lab. In our datasource PALLASDS there exists
a minimal authentication repository in the form of some extra columns
in table EMPLOYEE, namely: USERNAME, PASSWORD, IS_ACCOUNT_ENABLED and
ROLE.

Username Password Enabled Role
cas_s cas_s Y EMPLOYEE
lucas_j lucas_j Y EMPLOYEE
wim_h wim_h Y MANAGER

Switching
the type of the DaoAuthenticationProvider demonstrates the power of
Spring. Because all we need to do is modify beans that are wired in the
securityContext.xml, and to provide the Java class that corresponds
with the new bean that will be wired into the DaoAuthenticationProvider
bean.

Steps:

  1. Open
    securityContext.xml and remove the MemoryAuthenticationDao element and
    the existing DaoAuthenticationProvider bean definition.
  2. Inject the new DaoAuthenticationProvider bean by adding to securityContext.xml:
<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
  <property name="authenticationDao" ref="authenticationDao"/>
  <property name="userCache">
    <bean id="userCache" class="net.sf.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
      <property name="cache">
        <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
          <property name="cacheManager" ref="cacheManager"/>
	    <property name="cacheName" value="userCache"/>
        </bean>
      </property>
    </bean>
  </property>
</bean>

<bean id="authenticationDao" class="nl.amis.pallas.security.AuthenticationJdbcDaoImpl">
  <property name="dataSource" ref="dataSource"/>
</bean>

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

As
you can see, we’ll use a custom AuthenticationDao, that is implemented
as a new class located in package nl.amis.palllas.security. Therefore
we need to add the class to our project. Create a new package
nl.amis.pallas.security and add class AuthenticationJdbcDaoImpl.java,
for example as follows:


package nl.amis.pallas.security;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

import javax.sql.DataSource;

import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.GrantedAuthorityImpl;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.User;
import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
import net.sf.acegisecurity.providers.dao.jdbc.JdbcDaoImpl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.MappingSqlQuery;

public class AuthenticationJdbcDaoImpl extends JdbcDaoImpl {

	protected final Log logger = LogFactory.getLog(getClass());

	private Long depId;

	private String empName;

	private Long empId;

	protected void initMappingSqlQueries() {
		super.setUsersByUsernameQuery("select username, password, is_account_enabled, department_id, name, id from employee where username = lower(?)");
		super.setAuthoritiesByUsernameQuery("select role from employee where username = lower(?)");
		super.initMappingSqlQueries();
		this.setUsersByUsernameMapping(new CustomUsersByUsernameMapping(this.getDataSource()));
		this.setAuthoritiesByUsernameMapping(new CustomAuthoritiesByUsernameMapping(
						this.getDataSource()));
		logger.info("query users = " + getUsersByUsernameQuery());
		logger.info("query authorities = " + getAuthoritiesByUsernameQuery());
	}

	public UserDetails loadUserByUsername(String username) {
		try {
			// is instance of User and is not a CustomUser, as I would expect...
			UserDetails u = super.loadUserByUsername(username);
			CustomUser cu = new CustomUser(u, depId, empName, empId);

			String str = "load user " + cu.getUsername() + ", "
					+ cu.isEnabled() + ", " + cu.getDepartmentId() + ", "
					+ cu.getEmployeeName() + ", " + cu.getEmployeeId();
			GrantedAuthority[] roles = cu.getAuthorities();
			for (int i = 0; i < roles.length; i++) {
				str += "\n - " + roles[i].getAuthority();
			}
			logger.info(str);
			return cu;
		} catch (UsernameNotFoundException e1) {
			logger.info(e1.getMessage());
			throw e1;
		} catch (DataAccessException e2) {
			logger.info(e2.getMessage());
			throw e2;
		}
	}

	protected class CustomUsersByUsernameMapping extends MappingSqlQuery {

		protected CustomUsersByUsernameMapping(DataSource ds) {
			super(ds, getUsersByUsernameQuery());
			declareParameter(new SqlParameter(Types.VARCHAR));
			compile();
		}

		protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
			String username = rs.getString(1);
			String password = rs.getString(2);
			String enabled_str = rs.getString(3);
			boolean enabled = false;
			if ("Y".equalsIgnoreCase(enabled_str)) {
				enabled = true;
			}
			// get custom user attributes
			depId = rs.getLong(4);
			empName = rs.getString(5);
			empId = rs.getLong(6);

			User user = new User(username, password, enabled, true, true, true,
					new GrantedAuthority[0]);
			logger.info("mapRow user = " + user.getUsername());
			return user;
		}
	}

	protected class CustomAuthoritiesByUsernameMapping extends MappingSqlQuery {
		protected CustomAuthoritiesByUsernameMapping(DataSource ds) {
			super(ds, getAuthoritiesByUsernameQuery());
			declareParameter(new SqlParameter(12));
			compile();
		}

		protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
			String roleName = "ROLE_" + rs.getString(1);
			GrantedAuthorityImpl authority = new GrantedAuthorityImpl(roleName);
			logger.info("mapRow authority = " + roleName);
			return authority;
		}

	}
}

This is indeed quite some code, and in fact we could have just a few lines (or just XML declaration) as we would be satisfied with the standard implementation, that is we would create a User object that just contains the attributes username, password and granted authoritieslist. However in the example above we create a custom user, that also holds attributes like: DepartmentId, EmployeeName, EmployeeId. The benefit of this is that at any place in the code of our application we can access the attributes and use this for our puposes. Please add class CustomUser.java to package nl.amis.pallas.security as follows:


package nl.amis.pallas.security



import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.User;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class CustomUser extends User {

    private static final long serialVersionUID = 1L;

    protected final Log logger = LogFactory.getLog(getClass());

    private Long departmentId;

    private String employeeName;

    private Long employeeId;

    public Long getDepartmentId() {
        return departmentId;
    }

    public void setDepartmentId(Long departmentId) {
        this.departmentId = departmentId;
    }

    public Long getEmployeeId() {
        return employeeId;
    }

    public void setEmployeeId(Long employeeId) {
        this.employeeId = employeeId;
    }

    public String getEmployeeName() {
        return employeeName;
    }

    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }

    public CustomUser(String username, String password, boolean isEnabled,
            GrantedAuthority[] authorities, Long depId, String empName,
            Long empId) {
        super(username, password, isEnabled, true, true, true, authorities);
        this.setDepartmentId(depId);
        this.setEmployeeName(empName);
        this.setEmployeeId(empId);
    }

    public CustomUser(UserDetails u, Long depId, String empName, Long empId) {
        this(u.getUsername(), u.getPassword(), u.isEnabled(), u
                .getAuthorities(), depId, empName, empId);
    }
}

5. Solutions

Solution 1

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
    /WEB-INF/dataAccessContext.xml
    /WEB-INF/securityContext.xml
    /WEB-INF/businessContext.xml
    /WEB-INF/webContext.xml

  </param-value>
</context-param>

Solution 2

<prop key="/login.html">loginController</prop

Solution 3

Add the following element to webContext.xml:

<bean id="loginController" class="nl.amis.pallas.web.LoginController" />

Solution 4

/secure/*=ROLE_EMPLOYEE,ROLE_MANAGER


21 Comments

  1. Sanjoy De September 17, 2008
  2. Sachin Mali March 12, 2008
  3. Thomas December 4, 2007
  4. Anand Mohan Sravanam July 2, 2007
  5. filip June 16, 2006
  6. Bunard May 25, 2006
  7. Erik Kerkhoven May 23, 2006
  8. Bunard May 19, 2006
  9. Erik Kerkhoven May 18, 2006
  10. Bunard May 18, 2006
  11. Bunard May 16, 2006
  12. Bunard May 13, 2006
  13. Kidd April 21, 2006
  14. Erik Kerkhoven April 20, 2006
  15. Angelo April 20, 2006
  16. Erik Kerkhoven April 19, 2006
  17. Erik Kerkhoven April 19, 2006
  18. Erik Kerkhoven April 19, 2006
  19. Erik Kerkhoven April 19, 2006
  20. Angelo April 19, 2006
  21. Kidd April 18, 2006