1. Intro to the Spring Security Tutorial: Form Login Java Config

This post is a Spring Security form login tutorial which uses the Spring Java Configuration annotations rather than the XML Configuration. The post builds on the previous Form Login post translating all the XML Configuration into Java Configuration. Check out the original post for detailed information on how to configure the Form Login:

This post will recreate all the configuration made available in previous post as Java Configuration, this includes examples on how to store credentials as:

  • Hardcoded with plain text
  • Hardcoded using SHA1 encoded passwords
  • JDBC based
  • MongoDB based (these principles could be applied to any database)

The Java Configuration will also be used to configure the necessary Spring MVC controllers as well as the Database Repositories (both MySQL and MongoDB).

2. Ground Work: Configuring Your Web.xml

As always we begin by setting up the web.xml. The main thing you’ll notice here is that the web.xml is creatly simplified when compared to the original Form Login post. That’s because we are going to use the Spring Initializers to add the Spring MVC’s DispatcherServlet and Spring Security’s DelegatingFilterProxy.

Here’s the full web.xml configuration:

<?xml version="1.0" encoding="UTF-8"?>
<web-app  xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0" metadata-complete="true">
	
	<!-- JSPs -->
	<servlet>
		<servlet-name>403Jsp</servlet-name>
		<jsp-file>/403.jsp</jsp-file>
	</servlet>
	<servlet-mapping>
		<servlet-name>403Jsp</servlet-name>
		<url-pattern>/403</url-pattern>
	</servlet-mapping>


	<!-- The error page -->
	<error-page>
		<error-code>403</error-code>
		<location>/403</location>
	</error-page>
</web-app>

Note: the source code download also includes Spring’s CharacterEncodingFilter which ensures everything is UTF-8 encoded.

3. Application, Security, MVC and Repository Java Configuration

This is the heart of the post. I’ve broken down the Java Configuration into 4 different classes:

  • AppConfiguration: this class brings all the configuration classes together and ensures that @ComponentScan is activated.
  • MvcConfiguration: this class configures Spring MVC.
  • RepositoryConfiguration: this class configures MongoDB and Morphia for our custom UserDetailsService.
  • SecurityConfiguration: this class configures Spring Security. The code will provide examples of how to setup Spring Security using all the different credential storage options mentioned above.

Let’s get started with the AppConfiguration!

3.1 Application Configuration

The AppConfiguration provides the overall configuration for the application. This is where all the configuration files are imported and component scanning is activated. Below is a brief explanation of the annotations used:

  • @Configuration: this annotation lets Spring know that this is configuration class.
  • @ComponentScan: this annotation is used to enable Component Scanning.
  • @Import: this annotation is used to let Spring know that there are other configuration classes it should be aware of.

Here’s the code:

package org.codehustler.configuration;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@ComponentScan({ "org.codehustler.*" })
@Import({ MvcConfiguration.class, RepositoryConfiguration.class, SecurityConfiguration.class })
public class AppConfiguration
{
}

The first configuration class to be processed will be the Spring MVC configuration.

3.2 MVC Configuration

The MvcConfiguration enables Spring MVC and configures the relevant beans, namely the InternalResourceViewResolver. Here is an explanation of the different annotations and methods:

  • @EnableWebMvc: this annotation is added to have the Spring MVC configuration defined in WebMvcConfigurationSupport imported.
  • configureDefaultServletHandling(): we use this method to enable forwarding to the “default” Servlet. The “default” Serlvet is used to handle static content such as CSS, HTML and images.
  • getInternalResourceViewResolver(): this method is responsible for instantiating the InternalResourceViewResolver bean.

Here’s the code:

package org.codehustler.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter
{

	@Override
	public void configureDefaultServletHandling( DefaultServletHandlerConfigurer configurer )
	{
		configurer.enable();
	}


	@Bean
	public InternalResourceViewResolver getInternalResourceViewResolver()
	{
		InternalResourceViewResolver resolver = new InternalResourceViewResolver();
		resolver.setPrefix("/WEB-INF/jsp/");
		resolver.setSuffix(".jsp");
		return resolver;
	}
}

Now let’s take a look at the Repository configuration.

3.3 Repository Configuration

The RepositoryConfiguration is used to configure the repositories required by our application. In this case we initialize MongoDB for our custom UserDetailsService and a regular JDBC DataSource to be used with the default Spring Security authentication manager.

The methods in this class are all used to instantiate beans which are used in the application, so let’s cut to the chase, here’s the code:

package org.codehustler.configuration;

import java.net.UnknownHostException;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import com.github.jmkgreen.morphia.AdvancedDatastore;
import com.github.jmkgreen.morphia.Morphia;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.ServerAddress;
import com.mongodb.WriteConcern;

@Configuration
public class RepositoryConfiguration
{
	private String dbName = "codehustler";
	private String mongoDbAddress = "127.0.0.1";

	@Bean
	public Morphia getMorphia() throws UnknownHostException
	{
		return new Morphia();
	}

	@Bean
	public AdvancedDatastore getAdvancedDatastore() throws UnknownHostException
	{
		return getMorphia().createAdvancedDatastore( getMongo(), dbName );
	}

	@Bean
	public MongoClient getMongo() throws UnknownHostException
	{
		MongoClientOptions options = MongoClientOptions.builder()
				.connectionsPerHost( 150 )
				.autoConnectRetry( true )
				.writeConcern( WriteConcern.ERRORS_IGNORED ).build();

		ServerAddress severAddress = new ServerAddress( mongoDbAddress );

		return new MongoClient( severAddress, options );
	}


	@Bean
	public DataSource getDataSource()
	{
		DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
		driverManagerDataSource.setDriverClassName( "com.mysql.jdbc.Driver" );
		driverManagerDataSource.setUrl( "jdbc:mysql://localhost:3306/codehustler" );
		driverManagerDataSource.setUsername( "root" );
		driverManagerDataSource.setPassword( "password" );
		return driverManagerDataSource;
	}
}

With that out of the way, we can move onto the Spring Security Configuration.

3.4 Security Configuration

The SecurityConfiguration replaces the security-applicationContext.xml from the previous post. As there are a number of different options in the file below I’ve decided to put the explanation inline with the code. I’ve added all the different Authentication Providers and left them all commented out, except for the MongoDB based provider. If you’d like me to add a method breakdown list before the code, just let me know in the comments and I’ll add it in! 🙂

Here’s the code:

package org.codehustler.configuration;

import javax.sql.DataSource;

import org.codehustler.security.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.encoding.ShaPasswordEncoder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity( prePostEnabled = true )
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
	@Autowired
	private LoginService loginService;

	@Autowired
	private DataSource dataSource;

	@Autowired
	public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception
	{
		// The authentication provider below uses MongoDB to store SHA1 hashed passwords
		// To see how to configure users for the example below, please see the README file
		auth
			.userDetailsService( loginService )
			.passwordEncoder( new ShaPasswordEncoder() );


		// The authentication provider below is the simplest provider you can use
		// The users, their passwords and roles are all added as clear text
//		auth
//			.inMemoryAuthentication()
//			.withUser( "admin" )
//				.password( "admin" )
//				.roles( "ADMIN" )
//				.and()
//			.withUser( "user" )
//				.password( "user" )
//				.roles( "USER" );


		// The authentication provider below hashes incoming passwords using SHA1
		// The users passwords below are hashed using SHA1 (see README for values)
//		auth
//			.inMemoryAuthentication()
//			.passwordEncoder( new ShaPasswordEncoder() )
//			.withUser( "admin" )
//				.password( "d033e22ae348aeb5660fc2140aec35850c4da997" )
//				.roles( "ADMIN" )
//				.and()
//			.withUser( "user" )
//				.password( "12dea96fec20593566ab75692c9949596833adc9" )
//				.roles( "USER" );


		// The authentication provider below uses JDBC to retrieve your credentials
		// The data source bean configuration can be found at the bottom of this file
		// The first example uses the default Spring Security tables, see link below
		// http://docs.spring.io/spring-security/site/docs/3.0.x/reference/appendix-schema.html
//		auth
//			.jdbcAuthentication()
//			.dataSource( dataSource )
//			.passwordEncoder( new ShaPasswordEncoder() );


		// The second example shows how you can override the default queries in order
		// to use your own tables rather than Spring Security's default tables
		// The SQL is relatively simple and should be easy to figure out and map to your needs
//		auth
//			.jdbcAuthentication()
//			.dataSource( dataSource )
//			.usersByUsernameQuery( "select username,password from users where username=?" )
//			.authoritiesByUsernameQuery( "select u.username, r.authority from users u, roles r where u.userid = r.userid and u.username =?" );
	}


	@Override
	public void configure( WebSecurity web ) throws Exception
	{
		// This is here to ensure that the static content (JavaScript, CSS, etc)
		// is accessible from the login page without authentication
		web
			.ignoring()
				.antMatchers( "/static/**" );
	}


	@Override
	protected void configure( HttpSecurity http ) throws Exception
	{
		http

			// access-denied-page: this is the page users will be
			// redirected to when they try to access protected areas.
			.exceptionHandling()
				.accessDeniedPage( "/403" )
				.and()

			// The intercept-url configuration is where we specify what roles are allowed access to what areas.
			// We specifically force the connection to https for all the pages, although it could be sufficient
			// just on the login page. The access parameter is where the expressions are used to control which
			// roles can access specific areas. One of the most important things is the order of the intercept-urls,
			// the most catch-all type patterns should at the bottom of the list as the matches are executed
			// in the order they are configured below. So /** (anyRequest()) should always be at the bottom of the list.
			.authorizeRequests()
				.antMatchers( "/login**" ).permitAll()
				.antMatchers( "/admin/**" ).hasRole( "ADMIN" )
				.anyRequest().authenticated()
				.and()
			.requiresChannel()
				.anyRequest().requiresSecure()
				.and()

			// This is where we configure our login form.
			// login-page: the page that contains the login screen
			// login-processing-url: this is the URL to which the login form should be submitted
			// default-target-url: the URL to which the user will be redirected if they login successfully
			// authentication-failure-url: the URL to which the user will be redirected if they fail login
			// username-parameter: the name of the request parameter which contains the username
			// password-parameter: the name of the request parameter which contains the password
			.formLogin()
				.loginPage( "/login" )
				.loginProcessingUrl( "/login.do" )
				.defaultSuccessUrl( "/" )
				.failureUrl( "/login?err=1" )
				.usernameParameter( "username" )
				.passwordParameter( "password" )
				.and()

			// This is where the logout page and process is configured. The logout-url is the URL to send
			// the user to in order to logout, the logout-success-url is where they are taken if the logout
			// is successful, and the delete-cookies and invalidate-session make sure that we clean up after logout
			.logout()
				.logoutRequestMatcher( new AntPathRequestMatcher( "/logout" ) )
				.logoutSuccessUrl( "/login?out=1" )
				.deleteCookies( "JSESSIONID" )
				.invalidateHttpSession( true )
				.and()

			// The session management is used to ensure the user only has one session. This isn't
			// compulsory but can add some extra security to your application.
			.sessionManagement()
				.invalidSessionUrl( "/login?time=1" )
				.maximumSessions( 1 )
				.and()

			// As PipoBB-8 points out in the comments, in Spring 4.x upwards we need to explicitly
			// disable CSRF (as below) or alternatively add some HTML to the page to enable CSRF.
			.csrf().disable();
	}

}

Now that we have all of our configuration classes ready, we need to ensure that Spring detects them when it boots up. In order to do this we need to configure the relevant Spring Initializers.

Note: as per PipoBB-8’s comment, if you would like to keep CSRF enabled, then you can eliminate the last portion of the configuration above and add to your Login page’s form the following HTML:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

4. Spring Initializers

In order to initialize the Spring application without using any XML whatsoever (including no web.xml), we must register the DispatcherServlet and the DelegatingFilterProxy using special Initializer classes. This section covers the two Initializers required in order to boot up the application correctly.

4.1 Spring Configuration Initializer

The SpringConfigurationInitializer is used to register the DispatcherServlet. This ensures the correct configuration files are picked up and that Spring MVC has the correct Servlet mappings. Here is an explanation of the methods:

  • getRootConfigClasses(): this method is used to specify the @Configuration and/or @Component classes which should be provided to the root application context. This is where we provide the AppConfiguration class which in turn imports all the other configuration classes.
  • getServletConfigClasses(): this method is used to specify the @Configuration and/or @Component classes which should be provided to the dispatcher servlet application context.
  • getServletMappings(): this method is used to specify the Servlet mappings for the DispatcherServlet.

Here’s the code:

package org.codehustler.configuration.initializer;

import org.codehustler.configuration.AppConfiguration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpringConfigurationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer
{
	@Override
	protected Class<?>[] getRootConfigClasses()
	{
		return new Class[] { AppConfiguration.class };
	}

	@Override
	protected Class<?>[] getServletConfigClasses()
	{
		return null;
	}

	@Override
	protected String[] getServletMappings()
	{
		return new String[] { "/" };
	}

}

Spring Security requires its own Initializer. Let’s have a look at that.

4.2 Spring Security Initializer

The SpringSecurityInitializer is used to register the DelegatingFilterProxy. No additional code is required other than extending the AbstractSecurityWebApplicationInitializer.

Here’s the code:

package org.codehustler.configuration.initializer;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer
{
}

All the configuration and initializers are in place. We can now take the application for a spin!

5. Conclusion

That’s it for this post! Remember to have a look at the previous post (Spring Security Tutorial: Form Login) if you want to have a deep dive into all the configuration options used above. As always, if you have any questions please leave a comment and I’ll try to answer as soon as I have a moment.

The sample code used in this post (and all other posts with code samples) is available by subscribing below. The downloaded source code usually comes with a few goodies which aren’t in the post! For example, this bundle shows you how to configure Log4J logging for your Spring application, and in the login.jsp you’ll see how you can add a CSRF token to the login form!
The source code is all configured using Maven, so you can be up and running by simply typing “mvn clean install tomcat7:run-war-only”. This will spin up the demo on http://localhost:8080/.

Alessandro