Friday, August 15, 2008

Instant WebApp Security - Just Add Acegi

A question came up at work recently about the best time (in terms of cost and quality) to build security into a web application, that got me thinking about this whole thing. Since we use the Spring Framework for our web applications, Acegi is the natural choice, being the de-facto security solution for it.

Acegi is very flexible - it has lots of customization hooks, and can connect to almost any popular authentication backend. However, to paraphrase Spiderman's uncle Ben - "with great flexibility comes great complexity", and Acegi is no exception. The configuration is the complex part - it's done by wiring Acegi beans in the Spring application context, so you need to know what beans are available within Acegi and how to wire them up.

Several excellent guides exist on the Internet (do a search for "acegi tutorials") - they are all worth a read. In addition, you probably also want to read the Acegi Reference Guide for more extensive coverage. It also helps to have the source JAR handy - I found lot of answers to my questions in the code.

I primarily used Bart van Riel's Spring Acegi Tutorial as a guide when securing my app. The approach I propose here is prescriptive - given an insecure (Spring) web application, this post lists out the tasks that need to happen in order to make it secure with a rather basic but working Acegi configuration.

Unlike the other tutorials, this post makes no attempt to explain the purpose of the various beans in the configuration - I simply point out the places where you need to customize it for your own application. Not sure about you, but this format works better for me personally... I like to set up something quickly and fiddle with the code until it does my bidding. Obviously, to extend this configuration, you will need to read up to locate and understand the Acegi components involved.

The Insecure Application

My example application is a Maven2 Web Application using Spring, and contains list and edit screens for two beans - a Content and a User bean. There is a database at the back and thats pretty much it. Because there is going to be very little code change between this and the secure version (most of the changes are adding filters and interceptors), I am going to provide the code listings later (marking the changes made for security appropriately) for the secure version of the application. The application exposes the following URLs:

1
2
3
4
5
http://localhost:8081/myapp/index.jsp
http://localhost:8081/myapp/users/list.do
http://localhost:8081/myapp/users/edit.do?username=${username}
http://localhost:8081/myapp/contents/list.do
http://localhost:8081/myapp/contents/edit.do?id=${contentId}
The directory structure for src/main/webapps is shown below. This is going to change for the secure version, which is why I mention it here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
webapp
  |
  +-- myapp
  |    |
  |    +-- index.jsp
  |    |
  |    +-- contents
  |    |     |
  |    |     +-- edit.jsp
  |    |     |
  |    |     +-- list.jsp
  |    |
  |    +-- users
  |          |
  |          +-- edit.jsp
  |          |
  |          +-- list.jsp
  |
  +-- WEB-INF
       |
       +-- myapp-servlet.xml
       |
       +-- web.xml

Meet The Users

We will have four users in our secure system. Their user names and roles, along with what the roles imply, are listed below:

Username Roles Description
admin ROLE_ADMIN Administrator can view and edit users, but cannot edit content. On login, should be directed to the users list page with edit capability.
mary ROLE_MANAGER, ROLE_EDITOR Can view and edit all content. On login, should be directed to the contents list page with edit capability.
bob ROLE_EDITOR Can view all content, but can only edit his own. On login, should be directed to the list page showing only contents authored by him, with edit capability.
larry ROLE_EDITOR Same as bob.

Apart from these, there is the unauthenticated user, who can view the public portion of the application (only the list contents page without edit capability).

Securing the application

The following are the things I needed to do to add Acegi security to the insecure web application described above. I describe each of these in some more detail and provide code snippets below.

  1. Add a ContextLoaderListener to web.xml
  2. Add Acegi FilterToBeanProxy definition to web.xml
  3. Add a new applicationContext-security.xml to src/main/resources
  4. Import this file into the main applicationContext file
  5. Move JSPs that correspond to secure resources
  6. Change index.jsp (welcome-file) to check for username and redirect
  7. Add new login.jsp and login_header_include.jsp
  8. Add handler mappings and controller for these new JSPs
  9. Change contents/list.jsp to selectively remove Edit link
  10. Add a MethodInterceptor to intercept calls to ContentDao

Add ContextLoaderListener to web.xml

Without this, the Acegi filters would not work and you will see exceptions on startup. I haven't worked with filters much, but I think this is a requirement for filters in general to work with Spring.

Add Acegi FilterToBeanProxy to web.xml

This adds filters to specific URLs that would need to be authenticated. My complete web.xml is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<!-- Source: src/main/webapp/WEB-INF/web.xml -->
<web-app 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 
         http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">

  <!-- Acegi filters won't work without the ContextLoaderListener -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/myapp-servlet.xml</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <filter>
    <filter-name>acegi_filterchain_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>

  <!-- 
    The mapping below means that /all/ urls, including JS, HTML, CSS, etc
    will pass through the filter. Consider matching specific patterns for
    performance.
  -->
  <filter-mapping>
    <filter-name>acegi_filterchain_proxy</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- The original Spring servlet config (listens only on .do URLs) -->
  <servlet>
    <servlet-name>myapp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>myapp</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping> 

  <!-- 
    The original welcome-file was a list of links to the list pages.
    They now redirect based on the value of the username in the 
    SecurityContext
  -->
  <welcome-file-list>
    <welcome-file>/myapp/index.jsp</welcome-file>
  </welcome-file-list>
  
</web-app>

The applicationContext-security.xml file

As promised, there are no explanations for the various beans. Refer to the many online tutorials and blog posts, the Acegi reference guide or the source code for more information about these. The lines marked with <!-- CUSTOM: nn > comments are customizations which are explained below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?xml version="1.0" encoding="UTF-8"?>
<!-- Source: src/main/resources/applicationContext-security.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/util 
       http://www.springframework.org/schema/util/spring-util-2.0.xsd">

  <bean id="messageSource" 
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="org/acegisecurity/messages"/>
  </bean>
  
  <!-- Acegi Filter Chain Proxy -->
  <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
    <property name="filterInvocationDefinitionSource"><!-- CUSTOM:1 -->
      <value>
        CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
        PATTERN_TYPE_APACHE_ANT
        /j_acegi_logout=logoutFilter
        /**=httpSessionContextIntegrationFilter,
            authenticationProcessingFilter,
            exceptionTranslationFilter,filterSecurityInterceptor
      </value>
    </property>
  </bean>

  <!-- 
    List of filters corresponding to form of authentication in use per the
    filterInvocationDefinitionSource bean above (Listed under pattern=...
    The chain may be different based on the type of authentication being
    used. Beans listed below correspond to the filters listed in the pattern.
  -->
  <bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
    <property name="allowSessionCreation" value="true"/>
  </bean>

  <bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
    <property name="filterProcessesUrl" value="/j_acegi_security_check"/>
    <property name="authenticationFailureUrl" value="/login.do?id=1"/><!-- CUSTOM:2 -->
    <property name="defaultTargetUrl" value="/myapp/index.jsp"/><!-- CUSTOM:3 -->
    <property name="authenticationManager" ref="authenticationManager"/>
  </bean>

  <bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
    <property name="authenticationEntryPoint" ref="formLoginAuthenticationEntryPoint"/>
    <property name="accessDeniedHandler" ref="accessDeniedHandler"/>
    <property name="createSessionAllowed" value="true"/>
  </bean>
  
  <bean id="accessDeniedHandler" class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
    <property name="errorPage" value="/login.do?id=1"/><!-- CUSTOM:4 -->
  </bean>
  
  <bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
    <property name="authenticationManager" ref="authenticationManager"/>
    <property name="accessDecisionManager" ref="accessDecisionManager"/>
    <property name="objectDefinitionSource"><!-- CUSTOM:5 -->
      <value>
        CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
        PATTERN_TYPE_APACHE_ANT
        /secure/users/*=ROLE_ADMIN
        /secure/contents/*=ROLE_EDITOR
      </value>
    </property>
  </bean>

  <!--
    Authentication Manager uses a chain of providers to extract the Principal, if
    one exists. As before the chain would differ based on what kind of authentication
    is being used. 
  -->
  <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
    <property name="providers">
      <list>
        <ref local="daoAuthenticationProvider"/>
      </list>
    </property>
  </bean>
  
  <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
    <property name="userDetailsService" ref="userDetailsService"/>
  </bean>
  
  <bean id="userDetailsService" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  
  <bean id="accessDecisionManager" class="org.acegisecurity.vote.UnanimousBased">
    <property name="decisionVoters">
      <list>
        <ref bean="roleVoter"/>
      </list>
    </property>
  </bean>
  
  <bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter">
    <property name="rolePrefix" value="ROLE_"/>
  </bean>

  <bean id="formLoginAuthenticationEntryPoint" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
    <property name="loginFormUrl" value="/login.do"/><!-- CUSTOM:6 -->
    <property name="forceHttps" value="false"/>
  </bean>
  
  <bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
    <constructor-arg value="/myapp/index.jsp"/><!-- CUSTOM:7 -->
    <constructor-arg>
      <list>
       <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
      </list>
    </constructor-arg>
  </bean>
</beans>

[1]: The pattern /j_acegi_logout=logoutFilter means that when the URL is /j_acegi_logout, the logoutFilter (defined below) will be activated. The /**= line is actually one long line of filters (broken up into multiple lines for readability) forming a filter chain that would be activated for any other URL. Each of these filters are configured in subsequent bean definitions in this file.

[2]: authenticationProcessingFilter.authenticationFailureUrl defines the URL that would be triggered when authentication fails. This is a page that needs to be created and defined for your application. Note that we don't specify the context for it, it is /login.do?id=1 not /myapp/login.do?myapp=1

[3]: authenticationProcessingFilter.defaultTargetUrl is the URL which will be pulled up if the authentication is successful. Here we point to /myapp/index.jsp since it has logic to redirect to the appropriate page depending on the User in the SecurityContext.

[4]: accessDeniedHandler.errorPage is the page that will be pulled up if access is denied for some reason. Here it goes to the same page as the authenticationFailureUrl[2] above.

[5]: The patterns defined in the objectDefinitionSource specify which URL pattern is allowed for which role. In our case, all user URLs are owned by admin (ROLE_ADMIN), and all content URLs are owned by editors (ROLE_EDITOR). We accomodate the powers of ROLE_MANAGER and the constraint that an editor cannot edit content authored by another editor in Java code, as we show later.

[6]: formLoginAuthenticationEntryPoint.loginFormUrl is self explanatory - it is the URL of the login form. Note that we don't need to specify the context for it, it is simply /login.do, not /myapp/login.do.

[7]: logoutFilter.constructor-arg[0] is set to /myapp/index.jsp, this is the URL that the chain will go to on logging out.

Import security config into main applicationContext

For ease of testing, I typically move all my non-web beans into its own applicationContext-xxx.xml file in src/main/resources, but to keep this post short, I decided to put them into the main myapp-servlet.xml file (configuration for DispatcherServlet). Into this file, I import the applicationContext-security.xml file with the import resource tag. The full myapp-servlet.xml file is shown later below.

1
  <import resource="classpath:applicationContext-security.xml"/>

Move JSPs into secure subdirectories

In our filterSecurityInterceptor configuration, our secure pages have URLs that begin with /secure/* so our secure URLs will look like this now (compare with the original list of URLs).

1
2
3
4
5
http://localhost:8081/myapp/index.jsp
http://localhost:8081/myapp/secure/users/list.do
http://localhost:8081/myapp/secure/users/edit.do?username=${username}
http://localhost:8081/myapp/contents/list.do
http://localhost:8081/myapp/secure/contents/edit.do?id=${contentId}

Since we use a vanilla Spring InternalResourceViewResolver to figure out where our views should go to, we need to move the JSP files around a bit. We basically create a secure sub-directory under src/main/webapp/myapp and create a tree of all the JSPs that need to be made secure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
webapp
  |
  +-- myapp
  |    |
  |    +-- index.jsp
  |    |
  |    +-- contents
  |    |     |
  |    |     +-- list.jsp
  |    |
  |    +-- secure
  |          |
  |          +-- contents
  |          |     |
  |          |     +-- edit.jsp
  |          |
  |          +-- users
  |                |
  |                +-- edit.jsp
  |                |
  |                +-- list.jsp
  +-- WEB-INF
       |
       +-- myapp-servlet.xml
       |
       +-- web.xml

Change index.jsp to do redirect

Originally, index.jsp was just a plain HTML page with a list of links. Now, we want to make it smart, so when we pull this page up, it will redirect either to the admin home page (secure/users/list.do) or the content home page (/contents/list.do) depending on what it finds in the SecurityContext.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<%-- Source: src/main/webapp/myapp/index.jsp --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="authz" uri="http://acegisecurity.org/authz" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>MyApp :: Home</title>
</head>
<body>
  <c:set var="username">
    <authz:authentication operation="username"/>
  </c:set>
  <c:choose>
    <c:when test="${username eq 'admin'}">
      <c:redirect url="/secure/users/list.do"/>
    </c:when>
    <c:otherwise>
      <c:redirect url="/contents/list.do"/>
    </c:otherwise>
  </c:choose>
</body>
</html>

New login_header_include.jsp

We create a component that shows if the user is logged on, and provides a logout button. We use Acegi's tag libraries to detect the username. The code is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<%-- Source: src/main/webapp/myapp/login_header_include.jsp --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="authz" uri="http://acegisecurity.org/authz" %>
<c:set var="username">
  <authz:authentication operation="username"/>
</c:set>
<c:choose>
  <c:when test="${not empty username}">
<%-- Note: Acegi specific for LogoutFilter --%>
Hello, <b>${username}</b>. Please <a href="/myapp/j_acegi_logout">Logout</a> when finished.
  </c:when>
  <c:otherwise>
Hello, <b>anonymous</b>. Please <a href="/myapp/login.do">Login</a> to update content.
  </c:otherwise>
</c:choose>
<br/>
<hr/> 

We import this into all our JSP files (see tree above) using a JSTL <c:import url="..."/> tag. If we were using Tiles or Sitemesh, it could probably be done more elegantly. Here are a couple of screen shots to show what this looks like with user logged out and in.

New login.jsp

We need a new login.jsp to allow the user to enter his username and password. We use several Acegi "reserved words" in here to use it's built-in modules to do the backend authentication work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<%-- Source: src/main/webapp/myapp/login.jsp --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form method="post" action="j_acegi_security_check">
  <c:if test="${not empty param.id}">
    <c:set var="username" value="${sessionScope.ACEGI_SECURITY_LAST_USERNAME}"/>
    <c:if test="${empty username}">
      <c:set var="username" value="anonymous"/>
    </c:if>
    <c:set var="errorMessage" value="${ACEGI_SECURITY_403_EXCEPTION.message}"/>
    <c:if test="${empty errorMessage}">
        <c:set var="errorMessage" value="Access Denied! Please contact Administrator."/>
    </c:if>
    <font color="red"><b>Hello ${username}. Message from server: ${errorMessage}</b></font><br/><br/>
  </c:if>
  <b>User: </b><input type="text" name="j_username"/><br/>
  <b>Pass: </b><input type="password" name="j_password"/><br/>
  <input type="submit" value="Login"/>
</form>
<br/><a href="/myapp/">Home</a>
</body>
</html>

New handler mappings and new Spring built-in controller

For the two new JSPs we added, we declare a UrlFilenameViewController to handle them and define new mappings for them, like so (We will show the complete myapp-servlet.xml file later).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  <bean id="urlFilenameViewController"
    class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>

  <bean id="urlMapping"
    class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <props>
        ...
        <prop key="login_header_include.do">urlFilenameViewController</prop>
        <prop key="login.do">urlFilenameViewController</prop>
      </props>
    </property>
  </bean>

Changes contents/list.jsp

We want to provide a slightly different view of the contents/list.do page to non-authenticated users than we want for authenticated users. For authenticated users, we should display the edit link, and for non-authenticated users, we should suppress the edit link. This is done in the JSP with Acegi tags, as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<%-- $Source: src/main/webapp/myapp/contents/list.jsp -%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="authz" uri="http://acegisecurity.org/authz" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Contents :: List</title>
</head>
<body>
  <h1>Contents :: List</h1>
  <c:import url="/login_header_include.do"/>
  <c:set var="username">
    <authz:authentication operation="username"/>
  </c:set>
  <table cellpadding="1" cellspacing="1" border="1">
    <tr>
      <th>Title</th>
      <th>Summary</th>
      <th>Author</th>
      <c:if test="${not empty username}">
        <th>Edit?</th>
      </c:if>
    </tr>
    <c:forEach items="${contents}" var="content">
      <tr>
        <td><a href="${content.url}">${content.title}</a></td>
        <td>${content.summary}</td>
        <td>${content.author}</td>
        <c:if test="${not empty username}">
          <td><a href="/myapp/secure/contents/edit.do?id=${content.id}">Edit</a></td>
        </c:if>
      </tr>
    </c:forEach>
  </table>
</body>
</html>

Screenshots below illustrate the different views of the home page for different users.


View of the home page for "Anonymous" (unauthenticated user)


View of the home page for "Admin"


View of the home page for "Bob" (an editor)

New method interceptor for ContentDao

At this point, our user pages are behind login and only admin can access them. Users with ROLE_EDITOR are able to access the contents/list.do page and are able to edit. However, we want to make sure that bob sees only his own stories and cannot edit edit larry's stories and vice versa, and that Mary can see and edit both.

I am not sure if we can just use some Acegi functionality to do this, but I opted for writing a MethodInterceptor around ContentDao.list() and ContentDao.edit() to build this. Here is the code for the interceptor. R J Lorimer's blog post Spring: A quick journey through AOP was very helpful.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// Source: src/main/java/com/mycompany/myapp/MySecurityInterceptor.java
package com.mycompany.myapp;

import java.util.ArrayList;
import java.util.List;

import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class MySecurityInterceptor implements MethodInterceptor {

  private final Log log = LogFactory.getLog(getClass());

  public Object invoke(MethodInvocation invocation) throws Throwable {
    // get username and role from the SecurityContext, if one exists
    String username = null;
    boolean isEditor = false;
    boolean isManager = false;
    Authentication auth = 
      SecurityContextHolder.getContext().getAuthentication();
    if (auth != null) {
      // not anonymous user, so needs to be checked
      UsernamePasswordAuthenticationToken token =
        (UsernamePasswordAuthenticationToken) auth;
      username = token.getName();
      GrantedAuthority[] authorities = token.getAuthorities();
      for (GrantedAuthority authority : authorities) {
        String role = authority.getAuthority();
        if (role.equals("ROLE_MANAGER")) {
          isManager = true;
        }
        if (role.equals("ROLE_EDITOR")) {
          isEditor = true;
        }
      }
    }
    Object returnValue = invocation.proceed();
    String methodName = invocation.getMethod().getName();
    if (methodName.equals("list")) {
      List<Content> contents = (List<Content>) returnValue;
      List<Content> postProcessedContents = 
        new ArrayList<Content>();
      if (isEditor && !isManager) {
        log.debug("Post processing list for user:[" + username + "]");
        // only if role is non-null and ROLE_USER, we want to filter by
        // author name, else we want to show all
        for (Content content : contents) {
          if (content.getAuthor().equals(username)) {
            postProcessedContents.add(content);
          }
        }
      } else {
        postProcessedContents.addAll(contents);
      }
      return postProcessedContents;
    } else if (methodName.equals("getById")) {
      Content content = (Content) returnValue;
      if (isEditor && !isManager) {
        // make sure he owns the content he is editing
        log.debug("Checking that user is allowed to edit");
        if (! content.getAuthor().equals(username)) {
          throw new AccessDeniedException("Don't covet thy neighbor's data");
        }
      }
      return content; 
    } else {
      return returnValue;
    }
  }
}

The interception for the list() method is to simply post-process the List of Content objects returned and filtering by user name. Probably not the most efficient approach - if I was working with real data, I would have opted to refactor out the SQL building logic from ContentDao.list() to append a WHERE author=${username} and wrap that instead. So Mary's data passes through unchanged because she has ROLE_MANAGER, but Larry's data gets removed from the list returned to Bob. You can see the difference below:

The interception of the edit() method checks to see if the returned Content has an author equal to ${username}, and if not, throws a AccessDeniedException with an informative message, as you can see below. The screenshots below show the behavior when Bob tries to access Larry's content by changing the id parameter in the URL of the edit form.

The myapp-servlet.xml file

I show below the full myapp-servlet.xml file, including the configuration for the interceptor described above. I use CGLIB proxying to avoid having to create an artificial interface for ContentDao.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?xml version="1.0" encoding="UTF-8"?>
<!-- $Source: src/main/webapp/WEB-INF/myapp-servlet.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/util 
       http://www.springframework.org/schema/util/spring-util-2.0.xsd">

  <!-- Security -->
  <import resource="classpath:applicationContext-security.xml"/>

  <!-- Datasources and DAOs -->
  <bean id="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/securitydb"/>
    <property name="username" value="insecure"/>
    <property name="password" value="depressed"/>
  </bean>
  
  <bean id="userDao" class="com.mycompany.myapp.UserDao">
    <property name="dataSource" ref="dataSource"/>
  </bean>  
  
  <bean id="contentDao"
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="contentDaoTarget"/>
    <property name="proxyTargetClass" value="true"/>
    <property name="interceptorNames">
      <list>
        <value>mySecurityInterceptor</value>
      </list>
    </property>
  </bean>
  <bean id="contentDaoTarget" class="com.mycompany.myapp.ContentDao">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="mySecurityInterceptor"
    class="com.mycompany.myapp.MySecurityInterceptor"/>
  
  <!-- View resolver -->
  <bean id="viewResolver"
   class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/myapp/" />
    <property name="suffix" value=".jsp" />
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  </bean>

  <!-- Controllers -->
  <bean id="userController" class="com.mycompany.myapp.UserController">
    <property name="userDao" ref="userDao"/>
  </bean>

  <bean id="contentController"
    class="com.mycompany.myapp.ContentController">
    <property name="contentDao" ref="contentDao"/>
  </bean>
    
  <!-- Add this one for Acegi JSPs -->
  <bean id="urlFilenameViewController"
    class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
  
  <!-- URL Mappings -->
  <bean id="urlMapping"
    class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <props>
        <prop key="/secure/users/list.do">userController</prop>
        <prop key="/secure/users/edit.do">userController</prop>
        <prop key="/secure/users/save.do">userController</prop>
        <prop key="/contents/list.do">contentController</prop>
        <prop key="/secure/contents/edit.do">contentController</prop>
        <prop key="/secure/contents/save.do">contentController</prop>
        <!-- Add these mappings for Acegi -->
        <prop key="login_header_include.do">urlFilenameViewController</prop>
        <prop key="login.do">urlFilenameViewController</prop>
      </props>
    </property>
  </bean>

</beans>

Database tables

There are three database tables in our application - two of them mandated by Acegi, although we reuse it to power our User bean. Here they are:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mysql> desc users;
+----------+-------------+------+-----+---------+-------+
| Field    | Type        | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| username | varchar(32) | NO   | PRI | NULL    |       | 
| password | varchar(32) | NO   |     | NULL    |       | 
| enabled  | tinyint(1)  | NO   |     | NULL    |       | 
+----------+-------------+------+-----+---------+-------+

mysql> desc authorities;
+-----------+-------------+------+-----+---------+-------+
| Field     | Type        | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+-------+
| username  | varchar(32) | NO   | PRI | NULL    |       | 
| authority | varchar(32) | NO   | PRI | NULL    |       | 
+-----------+-------------+------+-----+---------+-------+

mysql> desc content;
+---------+--------------+------+-----+---------+-------+
| Field   | Type         | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| id      | int(32)      | NO   | PRI | NULL    |       | 
| title   | varchar(128) | NO   |     | NULL    |       | 
| summary | varchar(255) | NO   |     | NULL    |       | 
| url     | varchar(128) | NO   |     | NULL    |       | 
| author  | varchar(32)  | NO   |     | NULL    |       | 
+---------+--------------+------+-----+---------+-------+

The contents of the USERS and AUTHORITIES tables are shown below. As you can see, Mary has both ROLE_EDITOR and ROLE_MANAGER roles, which is why she can edit content, but does not have the constraint Bob and Larry (ROLE_EDITOR only) do.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> select * from users;
+----------+----------+---------+
| username | password | enabled |
+----------+----------+---------+
| admin    | admin    |       1 | 
| bob      | bob      |       1 | 
| larry    | larry    |       1 | 
| mary     | mary     |       1 | 
+----------+----------+---------+
4 rows in set (0.00 sec)

mysql> select * from authorities;
+----------+--------------+
| username | authority    |
+----------+--------------+
| admin    | ROLE_ADMIN   | 
| bob      | ROLE_EDITOR  | 
| larry    | ROLE_EDITOR  | 
| mary     | ROLE_EDITOR  | 
| mary     | ROLE_MANAGER | 
+----------+--------------+
5 rows in set (0.00 sec)

Other code (for reference)

The rest of the code (not covered above) are pretty vanilla, and are provided here for completeness and just in case you like to cut and paste things. All code has a comment line which points to its relative location within the Maven2 web application.

The content set is a ContentController, which delegates to the ContentDao for most of the heavy lifting, and Content bean object which is what is populated and sent to the view. The main changes between the insecure version and the secure one are the changes in the check for various servlet paths and the corresponding views. Knowing what I do now, I would have probably factored out these patterns so changing them is simpler when security is applied.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Source: src/main/java/com/mycompany/myapp/ContentController.java
package com.mycompany.myapp;

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.ServletRequestUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class ContentController implements Controller {

  private final Log log = LogFactory.getLog(getClass());
  
  private ContentDao contentDao;
  
  public void setContentDao(ContentDao contentDao) {
    this.contentDao = contentDao;
  }
  
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {
    String action = request.getServletPath();
    ModelAndView mav = new ModelAndView();
    if (action.equals("/contents/list.do")) {
      mav.addObject("contents", contentDao.list());
      mav.setViewName("contents/list");
    } else if (action.equals("/secure/contents/edit.do")) {
      Integer id = 
        ServletRequestUtils.getRequiredIntParameter(request, "id");
      mav.addObject("content", contentDao.getById(id));
      mav.setViewName("secure/contents/edit");
    } else if (action.equals("/secure/contents/save.do")) {
      Integer id = 
        ServletRequestUtils.getRequiredIntParameter(request, "id");
      String title = 
        ServletRequestUtils.getRequiredStringParameter(request, "title");
      String summary = 
        ServletRequestUtils.getRequiredStringParameter(request, "summary");
      String url = 
        ServletRequestUtils.getRequiredStringParameter(request, "url");
      String author = 
        ServletRequestUtils.getRequiredStringParameter(request, "author");
      contentDao.save(id, title, summary, url, author);
      mav.addObject("contents", contentDao.list());
      mav.setViewName("contents/list");
    } else {
      return null;
    }
    return mav;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Source: src/main/java/com/mycompany/myapp/ContentDao.java
package com.mycompany.myapp;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

public class ContentDao extends JdbcDaoSupport {

  private final Log log = LogFactory.getLog(getClass());
  
  @SuppressWarnings("unchecked")
  public List<Content> list() {
    log.debug("Running list()");
    List<Map<String,Object>> rows = 
      getJdbcTemplate().queryForList(
      "select id, author, summary, title, url from content"); 
    List<Content> contents = new ArrayList<Content>();
    for (Map<String,Object> row : rows) {
      Content content = new Content();
      content.setId((Integer) row.get("ID"));
      content.setAuthor((String) row.get("AUTHOR"));
      content.setSummary((String) row.get("SUMMARY"));
      content.setTitle((String) row.get("TITLE"));
      content.setUrl((String) row.get("URL"));
      contents.add(content);
    }
    return contents;
  }

  @SuppressWarnings("unchecked")
  public Content getById(Integer id) {
    log.debug("running getById()");
    try {
      Map<String,Object> row = getJdbcTemplate().queryForMap(
        "select id, author, summary, title, url from content " +
        "where id=?", new Integer[] {id});
      Content content = new Content();
      content.setId((Integer) row.get("ID"));
      content.setAuthor((String) row.get("AUTHOR"));
      content.setSummary((String) row.get("SUMMARY"));
      content.setTitle((String) row.get("TITLE"));
      content.setUrl((String) row.get("URL"));
      return content;
    } catch (DataAccessException e) {
      return null;
    }
  }

  public void save(Integer id, String title, String summary, String url, String author) {
    Content content = getById(id);
    if (content == null) {
      long nextId = getNextId();
      getJdbcTemplate().update(
        "insert into content(id,author,summary,title,url)" +
        "values(?,?,?,?,?)",
        new Object[] {nextId, title, summary, url, author});
    } else {
      getJdbcTemplate().update(
        "update content set author = ?, summary = ?, title = ?, " +
        "url = ? where id = ?", 
        new Object[] {author, summary, title, url, id});
    }
  }

  private int getNextId() {
    return getJdbcTemplate().queryForInt("select max(id) + 1 from content");
  }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Source: src/main/java/com/mycompany/myapp/Content.java
package com.mycompany.myapp;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

public class Content {

  private Integer id;
  private String title;
  private String summary;
  private String url;
  private String author;

  // ... getters and setters omitted
}

And here is the content edit.jsp. We have already seen the list.jsp file above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%-- Source: src/main/webapp/myapp/secure/contents/edit.jsp --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Contents :: Edit</title>
</head>
<body>
  <h1>Contents :: Edit</h1>
  <c:import url="/login_header_include.do"/>
  <form method="post" action="/myapp/secure/contents/save.do">
    <input type="hidden" name="id" value="${content.id}"/>
    <b>Title: </b><input type="text" name="title" value="${content.title}"/><br/>
    <b>URL: </b><input type="text" name="url" value="${content.url}"/><br/>
    <b>Author: </b><input type="text" name="author" value="${content.author}"/><br/>
    <b>Summary: </b><br/>
    <textarea name="summary">${content.summary}</textarea><br/>
    <input type="submit" value="Save"/>
  </form> 
</body>
</html>

The User side is similar to the Content side, ie, follows a similar pattern. The Controller, Dao and bean, and the list and edit JSPs are shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Source: src/main/java/com/mycompany/myapp/UserController.java
package com.mycompany.myapp;

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.ServletRequestUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class UserController implements Controller {

  private final Log log = LogFactory.getLog(getClass());
  
  private UserDao userDao;
  
  public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
  }
  
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {
    String action = request.getServletPath();
    ModelAndView mav = new ModelAndView();
    if (action.equals("/secure/users/list.do")) {
      mav.addObject("users", userDao.list());
      mav.setViewName("secure/users/list");
    } else if (action.equals("/secure/users/edit.do")) {
      String name = 
        ServletRequestUtils.getRequiredStringParameter(request, "name");
      mav.addObject("user", userDao.getByName(name));
      mav.setViewName("secure/users/edit");
    } else if (action.equals("/secure/users/save.do")) {
      String name = 
        ServletRequestUtils.getRequiredStringParameter(request, "name");
      String pass = 
        ServletRequestUtils.getRequiredStringParameter(request, "pass");
      String role = 
        ServletRequestUtils.getStringParameter(request, "role", "ROLE_USER");
      userDao.save(name, pass, role);
      mav.addObject("users", userDao.list());
      mav.setViewName("secure/users/list");
    } else {
      return null;
    }
    return mav;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Source: src/main/java/com/mycompany/myapp/UserDao.java
package com.mycompany.myapp;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

public class UserDao extends JdbcDaoSupport {

  private final Log log = LogFactory.getLog(getClass());
  
  @SuppressWarnings("unchecked")
  public List<User> list() {
    List<Map<String,String>> rows = 
      getJdbcTemplate().queryForList(
      "select username, password " +
      "from users " +
      "where enabled = 1");
    List<User> users = new ArrayList<User>();
    for (Map<String,String> row : rows) {
      User user = new User();
      user.setName(row.get("USERNAME"));
      user.setPass(row.get("PASSWORD"));
      user.setRole(getRoles(user.getName()));
      users.add(user);
    }
    return users;
  }

  @SuppressWarnings("unchecked")
  public User getByName(String name) {
    try {
      Map<String,String> row = getJdbcTemplate().queryForMap(
        "select username, password " +
        "from users " +
        "where enabled = 1 " +
        "and username = ?", new String[] {name});
      User user = new User();
      user.setName(row.get("USERNAME"));
      user.setPass(row.get("PASSWORD"));
      user.setRole(getRoles(user.getName()));
      return user;
    } catch (DataAccessException e) {
      log.error("Can't find user:" + name);
      return null;
    }
  }
  
  public void save(String name, String pass, String role) {
    User user = getByName(name);
    if (user == null) {
      getJdbcTemplate().update(
        "insert into users(username,password,enabled) values (?,?,?)",
        new Object[] {name, pass, new Integer(1)});
    } else {
      getJdbcTemplate().update(
        "update users set password = ? where username = ?", 
        new String[] {pass, name});
    }
    updateAuthorities(name, role);
  }
  
  @SuppressWarnings("unchecked")
  private String getRoles(String username) {
    List<Map<String,String>> authRows = 
      getJdbcTemplate().queryForList(
      "select authority from authorities where username = ?", 
      new String[] {username});
    List<String> authorities = new ArrayList<String>();
    for (Map<String,String> authRow : authRows) {
      authorities.add(authRow.get("AUTHORITY"));
    }
    Collections.sort(authorities);
    return StringUtils.join(authorities.iterator(), ",");
  }

  private void updateAuthorities(String name, String role) {
    getJdbcTemplate().update(
      "delete from authorities where username=?", 
      new String[] {name});
    String[] authorities = StringUtils.split(role, ",");
    for (String authority : authorities) {
      getJdbcTemplate().update(
        "insert into authorities(username, authority) values (?,?)",
        new String[] {name, authority});
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Source: src/main/java/com/mycompany/myapp/User.java
package com.mycompany.myapp;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

public class User {

  private String name;
  private String pass;
  private String role;
  
  // ... getters and setters omitted
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<%-- Source: src/main/webapp/myapp/secure/users/edit.jsp --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Users :: Edit</title>
</head>
<body>
  <h1>Users :: Edit</h1>
  <c:import url="/login_header_include.do"/>
  <form method="post" action="/myapp/secure/users/save.do">
    <b>Username: </b><input type="text" name="name" value="${user.name}" readonly/><br/>
    <b>Password: </b><input type="text" name="pass" value="${user.pass}"/><br/>
    <b>Role: </b><input type="text" name="role" value="${user.role}"/><br/>
    <input type="submit" value="Save"/>
  </form> 
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<%-- Source: src/main/webapp/myapp/secure/users/list.jsp --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Users :: List</title>
</head>
<body>
  <h1>Users :: List</h1>
  <c:import url="/login_header_include.do"/>
  <table cellspacing="1" cellpadding="1" border="1">
  <tr>
    <th>User</th>
    <th>Pass</th>
    <th>Role</th>
    <th>Edit?</th>
  </tr>
  <c:forEach items="${users}" var="user">
    <tr>
      <td>${user.name}</td>
      <td>${user.pass}</td>
      <td>${user.role}</td>
      <td><a href="/myapp/secure/users/edit.do?name=${user.name}">Edit</a></td>
    </tr>
  </c:forEach>
  </table>
</body>
</html>

What's next?

This was a long post. If you came this far, thank you for sticking around, and I hope the article helped. While the setup presented in this blog is fairly basic and to some extent, unrealistic, but it gives a good feel for Acegi configuration. I plan to look at the following things in the future and blog about them if they work out:

  • Add Remember-me support - this is built into Acegi, and is simply a matter of adding some filters to the filter chain. It should be fairly easy, though.
  • Add Legacy Database support - The current implementation uses the Acegi preferred database tables USERS and AUTHORITIES. Organizations are very likely to have existing tables with this information.
  • Add LDAP support - Most organizations use LDAP to store employee email addresses and passwords. Being able to connect to the LDAP store to authenticate would be really good if one wanted to do a single-signon setup across the company Intranet.
  • Add X509 support - I would like to apply X509 authentication to a webservice application I am building.

Be the first to comment. Comments are moderated to prevent spam.