Translations of this page:

Spring Security

Spring security, formerly known as Acegi Security, has made great progress with the version 2.0 in terms of configuration. This setup is based on Spring 2.5.4 and Spring Security 2.0.3 built under Maven 2.0 and running as a Web Application. It is pretty easy to set up but there were one or two things that stumped me.

First thing we will need are some new dependencies in our POM file. These are for Spring Security. Note that Spring Security 2.0.3 was built against 2.0.8 so we exclude the spring-aop dependency otherwise we will end up with different versions of the same Jar.

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-acl</artifactId>
	<version>2.0.3</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-core</artifactId>
	<version>2.0.3</version>
	<!-- spring security 2.0.3 was built with spring 2.0.8 -->
	<exclusions>
		<exclusion>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-core-tiger</artifactId>
	<version>2.0.3</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-taglibs</artifactId>
	<version>2.0.3</version>
</dependency>

If you want to do all that nice AOP (Aspect Oriented Programming) stuff to avoid cluttering your code with Spring Security concerns you will also need the following dependencies.

<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<scope>runtime</scope>
	<version>1.5.4</version>
</dependency>
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<scope>runtime</scope>
	<version>1.6.1</version>
</dependency>
<dependency>
	<groupId>cglib</groupId>
	<artifactId>cglib-nodep</artifactId>
	<version>2.1_3</version>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aop</artifactId>
	<version>2.5.4</version>
</dependency>

Once your Spring application context goes beyond a certain size it is nice to split it up into separate configuration files each dealing with one area. This can also cause us some problems as we will see later. We can tell the configuration loader to look for additional configuration files in the Web App's web.xml file. We also need to add a filter to the chain. Using the pattern ”/*” means that all web requests and responses get passed through this filter before hitting the application.

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>
		/WEB-INF/applicationContext-security.xml
	</param-value>
</context-param>
 
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
 
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

This configures an additional configuration file for security. We might also move database configuration into its own file etc.

Let's take a look at applicationContext-security.xml. Note because this file is concerned largely with security we make Spring Security the default namespace rather than beans. Our application requires a mix of method and URL security.

Dealing with URL security first. The auto-config line does some basic set-up like configuring default login and logout pages. These can be overridden. We then give a list of URLs that we will secure with specific roles. The convention of writing ROLE_ does not need to be adhered to but is common practice. We can secure both dynamic and static URLs with Spring Security. For example ”/js/nodeTree.html” is a GWT administration application. No Spring code there. Finally the line /** says that all other URLs are accessible by the default anonymous user.

We don't want users to share logins so we also set session control to enforce the rule that only one instance of a login can connect at any one time. So if a user accesses a resource for which he doesn't have sufficient privileges the Spring Security manager will redirect him to a login page.

Securing methods proved a little more complicated. Despite all the magic of AOP you cannot secure methods on any random class. It has to be a Spring Bean, that is an object that is created by Spring and injected into another Bean. This caused me a lot of grief as I tried to intercept methods in my MainController class. Although this is a Spring bean it is not accessed by other beans through the Application Context or injected to other beans. This point seems crucial.

<?xml version="1.0" encoding="UTF-8"?>
 
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
		xmlns="http://www.springframework.org/schema/security"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
	http://www.springframework.org/schema/security
	http://www.springframework.org/schema/security/spring-security-2.0.xsd">
 
	<http auto-config="true">
		<intercept-url pattern="/editPage"
				access="ROLE_ADMIN,ROLE_EDITOR" />
		<intercept-url pattern="/savePage" access="ROLE_EDITOR" />
		<intercept-url pattern="/js/nodeTree.html" access="ROLE_ADMIN" />
		<intercept-url pattern="/**"
				access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!--
			Uncomment to enable X509 client authentication support
			<x509 />
		-->
		<concurrent-session-control max-sessions="1"
				exception-if-maximum-exceeded="true" />
	</http>
 
	<beans:bean id="securityObject" class="com.abcseo.magneato.utils.SecurityObject">
		<intercept-methods>
			<protect access="ROLE_EDITOR" method="getSecretData"/>
		</intercept-methods>
	</beans:bean>
 
	<authentication-provider>
		<password-encoder hash="md5" />
		<user-service id="userDetailsService" properties="WEB-INF/passwd.txt"/>
	</authentication-provider>
</beans:beans>

We declare the bean: securityObject and intercept the method getSecretData applying the access role: ROLE_EDITOR. If you don't have this role the unchecked AccessDeniedException is thrown. You can either catch this or leave to Spring Security to intercept and show a login page.

Our SecurityObject is very simple

package com.abcseo.magneato.utils;
 
public class SecurityObject {
	public String getSecretData() {
		return "For Your Eyes Only";
	}
}

We then inject this object into our Main Controller class in the servlet.xml configuration.

<bean id="mainController" class="com.abcseo.magneato.mvc.MainController">	
	<property name="securedObject">
		<ref bean="securedObject" />
	</property>
</bean>

Finally we need some usernames and passwords. For our simple application we will just store this in a file. Our Authentication Provider is configured to load /WEB-INF/passwd.txt and the passwords are encrypted using the MD5 algorithm. Passwd.txt has the format: username=password with a comma separated list of roles and an enabled/disabled flag.

#
# admin/admin
# editor/admin
# user/wombat
#
admin=21232f297a57a5a743894a0e4a801fc3,ROLE_ADMIN,ROLE_USER,ROLE_EDITOR,enabled
editor=21232f297a57a5a743894a0e4a801fc3,ROLE_USER,ROLE_EDITOR,enabled
user=2b58af6dddbd072ed27ffc86725d7d3a,ROLE_USER,enabled

Problems

2008-08-15 14:09:47.810::WARN:  failed springSecurityFilterChain
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' is defined
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:387)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:968)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:246)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:185)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:168)
        at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:885)

I got this because I mis-declared the context parameters in the Web.xml, specifying each external config with its own param-value.

<context-param>
	<param-name>contextConfigLocation</param-name>  
	<param-value>/WEB-INF/applicationContext-jcr.xml</param-value>
	<param-value>/WEB-INF/applicationContext-security.xml</param-value>
</context-param>
08-15 14:17:37,466 ERROR FrameworkServlet[290] Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name '_authenticationManager': Error setting property values;
nested exception is org.springframework.beans.PropertyBatchUpdateException;
nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'providers' threw exception;
nested exception is java.lang.IlegalArgumentException: A list of AuthenticationProviders is required

I never got to the bottom of this error. It occurred when I split my application context into separate files. If I referred to any Spring Security elements in a file outside of where the core Spring Security elements were declared I got this error. In particular the method interceptors. The solution was to use method annotations. These need to be enabled in the Application Context security.xml file:

<global-method-security secured-annotations="enabled"></:global-method-security>

Then you tag methods with the security roles you want to apply.

@Secured ({"ROLE_EDITOR"})
public class SecurityObject {
	public String getSecretData() {
		return "For Your Eyes Only";
	}
}

The main disadvantage is that if we want to change the roles that applied to a method we have to recompile the application rather than change the configuration file.

An Authentication object was not found in the SecurityContext

This seems to cause a number of issues. HTTP is a stateless protocol. Once you've logged in your Servlet Container (Tomcat etc) keeps a session object with any information about the current state. A JSESSION token with a randomly generated value is sent to the client (browser) either via a cookie or by url rewriting and by passing this token back and forth with each request you recover the user's session. If you log in an Authentication object is added to the session and it is the job of the Security Filters to recover this and reinsert it into the SecurityContextHolder context with each request.

If you have other filters in your filter chain that deal with certain types of request the filter order is important. The Security Filters must come above the other filters or they may be bypassed and your Authentication Object is lost. You'll see this exception even though you are correctly authenticated with the application.

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
	at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:321)
	at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:195)
	at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:64)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
	at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:621)
	at org.magneato.service.Repository$$EnhancerByCGLIB$$3d3ae3a.getParentForCreate(<generated>)

AccessDeniedException getting swallowed by MVC container

If you are using security annotations you should apply these to your service layer methods not your controllers. Controllers are configured in a different application context to other beans and you'll find the annotations don't get applied. When your service method throws and AccessDenied Exception you'll need to handle this in the controller and redirect to and error page. You can do this directly in the controller class with an exception handler

@ExceptionHandler(AccessDeniedException.class)
public ModelAndView myExceptionHandler(AccessDeniedException exception) {
	exception.printStackTrace();
	return new ModelAndView("accessDenied");
}
tech/java/spring-security.txt · Last modified: 2012/01/20 13:06 by davidof
Recent changes RSS feed