How to get started with the Acegi framework and implement your own Security provider?
In the old days folks used the J2EE securing capabilities of the app server. This is of course still an option, but there are superior alternatives like the Acegi framework. Acegi is far from new and with the latest releases it has become a very stable and easy-to-use framework, especially when combined with Spring. I had to implement a custom security provider for a customer and was very surprised how easy this was accomplished. This blog describes the steps I took to get started with Acegi.
The starting point is a -very- simple web application using the Spring MVC framework. This is the web.xml:
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/acegi-security.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>blog-acegi-basic</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>blog-acegi-basic</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
</web-app>
And the blog-acegi-basic-servlet.xml configuration.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/WEB-INF/views/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<bean name="/site/admin.html"
class="com.xebia.mvc.AdminController">
</bean>
<bean name="/site/public.html"
class="com.xebia.mvc.PublicController">
</bean>
</beans>
The two Controllers are also very simple (for demo purposes):
public class AdminController implements Controller {
/*
* (non-Javadoc)
*
* @see org.springframework.web.servlet.mvc.Controller#handleRequest(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
return new ModelAndView("admin", "info", "adminInfo");
}
}
public class PublicController implements Controller {
/*
* (non-Javadoc)
*
* @see org.springframework.web.servlet.mvc.Controller#handleRequest(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
return new ModelAndView("public", "info", "publicInfo");
}
}
As are the views admin.jsp
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="authz" uri="http://acegisecurity.org/authz" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Admin</title>
</head>
<body>
Hello Admin
<c:out value='${info}' />
</body>
</html>
and public.jsp that are placed in the folder WEB-INF/views:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="authz" uri="http://acegisecurity.org/authz" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Public</title>
</head>
<body>
Hello Public
<c:out value='${info}'/>
</body>
</html>
For now we’ll leave the acegi-security.xml empty.
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> </beans>
When you use maven2 to build your web-application you can use the jetty plugin to test it. The following pom.xml has all dependencies and the plugin configuration to use jetty with the jdk1.5:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xebia</groupId>
<artifactId>acegi-blog</artifactId>
<packaging>war</packaging>
<version>0-1-SNAPSHOT</version>
<name>acegi-blog</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-mock</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.acegisecurity</groupId>
<artifactId>acegi-security</artifactId>
<version>1.0.3</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-dao</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-support</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-remoting</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.1</version>
<configuration>
<scanIntervalSeconds>1</scanIntervalSeconds>
<contextPath>/blog-acegi</contextPath>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
The transitive dependency mechanism of maven2 pulls in a lot of the Spring dependencies because of the Acegi 1.0.3 pom configuration. Because I want to use the latest release of Spring I have excluded them from Acegi. When you fire up jetty:
$ mvn jetty:start
You can view the public and admin page with the url’s http://localhost:8080/blog-acegi/site/public.html and http://localhost:8080/blog-acegi/site/admin.html. Now we will introduce Acegi into the equation. We will secure the admin page with an user/password combination, while the public page may remain unsecured. First we add an Acegi Filter to the 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>
The FilterToBeanProxy is configured with a targetClass. Acegi expects an instance of this target class configured in the Spring bean context. We will code the Acegi beans in the Spring context file acegi-security.xml.
As specified in the Acegi Filter we need at least an instance of the FilterChainProxy in the acegi-secutity.xml.
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="filterChainProxy"
class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
<![CDATA[
PATTERN_TYPE_APACHE_ANT
/**=exceptionTranslationFilter,filterInvocationInterceptor,authenticationProcessingFilter
]]>
</value>
</property>
</bean>
<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.jsp" />
<property name="forceHttps" value="false" />
</bean>
</property>
</bean>
<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager"
ref="authenticationManager" />
<property name="authenticationFailureUrl"
value="/login.jsp?login_error=1" />
<property name="defaultTargetUrl" value="/site/public.html" />
<property name="filterProcessesUrl"
value="/j_acegi_security_check" />
</bean>
<bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"
ref="authenticationManager" />
<property name="accessDecisionManager">
<bean class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions"
value="false" />
<property name="decisionVoters">
<list>
<bean class="org.acegisecurity.vote.RoleVoter" />
<bean
class="org.acegisecurity.vote.AuthenticatedVoter" />
</list>
</property>
</bean>
</property>
<property name="objectDefinitionSource">
<value>
<![CDATA[
PATTERN_TYPE_APACHE_ANT
/site/admin.html=ROLE_ADMIN
]]>
</value>
</property>
</bean>
<bean id="authenticationManager"
class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="authenticationProvider" />
</list>
</property>
</bean>
<bean id="authenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsServiceImpl"/>
</bean>
<bean id="userDetailsServiceImpl"
class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userProperties">
<props>
<prop key="admin">secret,ROLE_ADMIN</prop>
</props>
</property>
</bean>
</beans>
This rather large xml document configures Acegi to prevent people to see the admin part of the application. The configured beans with some details:
Because auto-formatting the acegi-secutiry.xml file can mess up the ANT like configuration I have coded those parts within CDATA sections. The one thing missing is the login.jsp; the login.jsp needs to be placed in the root of war file. When using maven2 for packaging the war the login.jsp must be placed in the webapp directory.
<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core_rt'%>
<%@ page import="org.acegisecurity.ui.AbstractProcessingFilter"%>
<%@ page
import="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"%>
<%@ page import="org.acegisecurity.AuthenticationException"%>
<html>
<head>
<title>Login</title>
</head>
<body>
<c:if test="${not empty param.login_error}">
<font color="red"> Your login attempt was not successful, try
again.<BR>
<BR>
Reason: <%=((AuthenticationException) session
.getAttribute(AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY))
.getMessage()%> </font>
</c:if>
<form action="<c:url value='j_acegi_security_check'/>" method="POST">
<center>
<table align="center" cellpadding="4" cellspacing="0" border="0"
class="loginform">
<tr>
<td bgcolor="f0f0f0" colspan="2">Enter your details below to
login to admin site:</td>
</tr>
<tr />
<tr>
<td nowrap align="right" valign="top"><label class="label"><u>U</u>sername:</label></td>
<td><input type='text' name='j_username' accessKey="U"></td>
</tr>
<tr>
<td nowrap align="right" valign="top"><label class="label"><u>P</u>assword:</label></td>
<td><input type='password' name='j_password' accessKey="P"></td>
</tr>
<tr>
<td valign="middle" align="center" colspan="2"><input
id="loginButton" type="submit" value="Log In" /></td>
</tr>
</table>
</center>
</form>
</body>
</html>
Again using the jetty plugin we can verify the result. When trying to access the admin page we will be prompted for an username and password and when providing the correct combination we will be forwarded to the admin page. The public page is still accessible for everyone. I have attached the source code of this example. The InMemoryDaoImpl is of course not very sophisticated. In a next blog I’ll show you how to implement your own security provider.
Filed under Security | 3 Comments »
hello okkke
I did not understand a thing in your example: if I directly launch the admin.jsp page how what my filter is will know that “/site/admin.html” is related to “admin.jsp” in the property objectDefinitionSource:
/site/admin.html =ROLE_ADMIN.
I will be reconnaisant if you answer me
thiks
Dali,
You can not access the admin.jsp page directly. You can only request it through the servlet mapping url configured in the blog-acegi-basic-servlet.xml; being /site/admin.html. The InternalResourceViewResolver links the url to a bean instance and the url is secured using the acegi interceptor.
Thank you for the tutorial, it helped me way.
Just a little remark for those as new to webapps as I am:
once you change the location of your context config like this:
contextConfigLocation
/WEB-INF/acegi-security.xml
your applicationContext.xml no longer gets loaded. So watch out blindly following the steps of the tutorial if you already have Spring configured.