Spring Security 5 Overcomplicated

By | September 28, 2018
Reading Time: 20 minutes

In this article I will show how to create a Spring MVC web application that uses Spring Security 5 but that does not use SpringBoot.

The method used to configure Spring Security 5 in this article is not something I recommend and really is doing things in an overly complicated manner. It is purely a learning experience and has been a part of the coding exercises I have undertaken when writing my book Core Spring 5 Certification in Detail.

The complete example application is available on GitHub.

Set Up Tomcat

True to my tradition, this will be a complete example and as so, it is necessary to prepare the Apache Tomcat instance that will be used to host the example web application.
In this article, it is assumed that you are running a Tomcat instance on localhost.

  • Download Apache Tomcat 9.
  • Unpack the archive at the location of your choice.
  • Open the file tomcat-users.xml in the conf directory and add the following XML fragment inside the <tomcat-users> element:
    <role rolename="manager-gui"/> 
    <role rolename="manager-script"/> 
    <user username="admin" password="secret" roles="manager-gui,manager-script"/>
  • Start Tomcat.
    Run the startup.sh or startup.bat script in the bin directory.
  • Tail the catalina.out log in the logs directory.
    In a bash or sh shell, this can be accomplished with the command tail -f catalina.out

Create the Project

In your development environment, create an empty Maven project and replace the contents of the pom.xml file in the root of the project with this:

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>se.ivankrizsan.spring</groupId>
    <artifactId>spring-security5-noboot</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring.version>5.0.8.RELEASE</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- This plug-in creates a WAR file containing the web application. -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <!--
                Deploy with: mvn tomcat7:deploy
                Undeploy with: mvn tomcat7:undeploy
                Redeploy with: mvn tomcat7:redeploy

                Need this in the tomcat-users.xml file:
                <role rolename="manager-gui"/>
                <role rolename="manager-script"/>
                <user username="admin" password="secret" roles="manager-gui,manager-script"/>
            -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <url>http://localhost:8080/manager/text</url>
                    <server>TomcatServer</server>
                    <path>/springsecuritynoboot</path>
                    <username>admin</username>
                    <password>secret</password>
                </configuration>
            </plugin>
            <!-- Specifies the Java version used in the project. -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Note that:

  • The first part of the pom.xml file contains the dependencies needed for the project.
    These are Spring, Spring Security, Servlet API and logging dependencies. No Spring Boot dependencies.
  • The contents of the <build> element contains three plug-ins.
  • The first plug-in, the maven-war-plugin, packages the built project into a WAR file which can be deployed to Tomcat, for instance.
  • The second plug-in, the tomcat7-maven-plugin, aids us in deploying the project’s WAR file to Tomcat.
    Note the configuration specifying the URL, user name and password to use when communicating with the Tomcat instance.
  • The last plug-in, the maven-compiler-plugin, makes it possible to specify the Java version that the project will be configured for.

Web Resources

The example application contains the following web resources located in the src/main/webapp directory:

Web resources of the Spring Security 5 Overcomplicated example application.

Web resources of the Spring Security 5 Overcomplicated example application.

Create index.jsp

The index.jsp file contains what will be the only resource in the example application that is accessible without having logged in.

  • Create the index.jsp file in the src/main/webapp directory with the following contents:
<%@ taglib prefix="c" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Spring Security 5 without using SpringBoot</title>
    </head>
    <body>
        <p>This is the Main Page.</p>
        <p>
            <a href="${pageContext.request.contextPath}/logout">Logout</a>
        </p>
        <p>
            <a href="hello">Get a Greeting from a web controller</a>
        </p>
        <p>
            <a href="static/test.html">Static contents: test.html</a>
        </p>
    </body>
</html>

Create login.html

The login.html file contains the login form which, when Spring Security has been enabled, will be presented to users not having logged in trying to access a secured resource of the application.

It is necessary to have, what is commonly referred to as, a custom login page in an application that uses Spring Security in order for logging out to work properly.
If the browsers built-in basic authentication login mechanism is used, the user name and password will be stored in the browser and enclosed with each request to the web application thus making it impossible to log out properly.

  • Create the login.html file in the src/main/webapp/static directory with the following contents:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Spring Security 5 Without SpringBoot</title>
    </head>
    <body>
        <div>
            You need to login to access that page.<br/>
        </div>
        <p>
        <form action="login" method="post">
            <div><label>User Name: <input type="text" name="username"/> </label></div>
            <div><label>Password : <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
        </p>
    </body>
</html>

Create test.html

The test.html page is just a static webpage included in the example application in order to show how to secure static contents in a web application.

  • Create the test.html file in the src/main/webapp/static directory with the following contents:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>This is a Test</title>
</head>
<body>
    <h1>Hello, this is the static test page!</h1>
    <p>
        <a href="../index.jsp">Back to Main Page</a>
    </p>
</body>
</html>

Create the Hello Controller

The hello controller is included in the example application as a representative of non-static web resource. A REST controller could have been used, but I wanted something that can be viewed in a web browser.

  • In the package se.ivankrizsan.spring.web, create the HelloController class implemented like this:
package se.ivankrizsan.spring.web;

import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

/**
 * Simple controller that generates a greeting string.
 * The controller is not annotated with @Controller and thus a Spring bean need to
 * be explicitly created.
 *
 * @author Ivan Krizsan
 * @see WebConfiguration
 */
@RequestMapping("/hello")
public class HelloController {
    @ResponseBody
    @GetMapping
    public String printHello(final ModelMap model) {
        final Date theDate = new Date();
        return "Hello Java-configured web application, the time is now " + theDate;
    }
}

Note that:

  • As the comment says, the HelloController class is not annotated with the @Controller annotation.
    This results in the controller not being autodetected and thus a Spring bean implemented by the HelloController class needs to be explicitly created, as we will see later when we examine the Spring Java configuration files.

MyWebApplicationInitializer Without Security

The web application initializer uses the servlet-3 way of initializing a Java web application using nothing but plain Java code. The first incarnation of the web application initializer in the example program, the version without security, looks like this:

package se.ivankrizsan.spring.web;

import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import javax.servlet.Filter;

/**
 * Web application initializer specifies the configuration files that will make up
 *  the root application Spring context.
 *  In addition this is the place where the servlet filters to be added and mapped
 *  to the dispatcher servlet.
 */
public class MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /* Constant(s): */

    /**
     * Retrieves list of configuration classes which are to be used to create the root application
     * context in the web application.
     *
     * @return Spring Java configuration classes to be used when creating root application context.
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { WebConfiguration.class };
    }

    /**
     * Retrieves list of configuration classes which are to be used to create the servlet application
     * context.
     *
     * @return Returns null, since all beans will be located in the root application context.
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

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

Note that:

  • I have given way to some degree of convenience by using the class AbstractAnnotationConfigDispatcherServletInitializer as superclass to the above configuration class.
    To be even more hardcore in a servlet 3 environment, one can implement the WebApplicationInitializer interface and create the Spring application context as well as the dispatcher servlet.
    This abstract superclass takes care of initializing the dispatcher servlet in a servlet 3 environment and creating the root, and optionally the application, Spring contexts supplying a number of methods that can be overridden to customize the process along the lines of the template method design pattern.
  • The method getRootConfigClasses is overridden and returns a configuration class.
    This configuration class, WebConfiguration, contains the web-related Spring beans of the example application and will be examined in a moment.
    The getRootConfigClasses method is to return the configuration classes which will make up the root Spring application context of the web application.
  • The method getServletConfigClasses is overridden and return null.
    This method is abstract in the parent class so it must be overridden. Null is returned since all the beans of this very small example application are located in the root Spring application context.
  • The method getServletMappings is overridden and return a string array containing only “/”.
    This method is also abstract in the class where it is defined (AbstractDispatcherServletInitializer) so it must be implemented. With “/” as the only mapping, the root path of the web application is mapped to the dispatcher servlet.

WebConfiguration

I have separated the web configuration and the security configuration. The web configuration class is located in the same package as the web application initializer seen above and is implemented like this:

package se.ivankrizsan.spring.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Spring web configuration excluding web security configuration, which is located in a separate class.
 *
 * @author Ivan Krizsan
 */
@EnableWebMvc
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    /* Constant(s): */
    public static final String STATIC_WEBRESOURCES_PATHPATTERN = "/static/**";
    public static final String STATIC_WEBRESOURCES_LOCATION = "/static/";


    @Bean
    public HelloController helloController() {
        return new HelloController();
    }

    /**
     * Adds resource handlers to supplied resource handler registry.
     * In this example a resource handler is added in order to serve static contents
     * located in the webapp/static directory.
     *
     * @param inResourceHandlerRegistry Registry to register new resource handlers in.
     */
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry inResourceHandlerRegistry) {
        inResourceHandlerRegistry
            .addResourceHandler(STATIC_WEBRESOURCES_PATHPATTERN)
            .addResourceLocations(STATIC_WEBRESOURCES_LOCATION)
            .setCachePeriod(0);
    }
}

Note that:

  • The configuration class is annotated with @EnableWebMvc.
    Imports the Spring MVC configuration from WebMvcConfigurationSupport.
  • The configuration class implements the WebMvcConfigurer interface.
    The WebMvcConfigurer interface defines a number of callback methods, with default implementations that do nothing, that allows for customizing the Spring MVC configuration when the @EnableWebMvc annotation is used.
  • A Spring bean is created using the HelloController class implemented earlier.
    As before, since the controller class is not annotated with @Controller a bean needs to be created like this in order for the controller to be discovered by Spring MVC.
  • There is a method addResourceHandlers that override a method from the WebMvcConfigurer interface.
    This method allows for adding resource handlers to the supplied resource handler registry.
    In this example a resource handler is added in order for the application to serve static content (HTML pages in this application) located in the webapp/static directory when there is a request to /static/ in the web application or any underlying resource.

First Run – Without Security

The first version of the application, a version without any security, is now ready. To try it out in the local Tomcat instance do the following, assuming that Tomcat is running:

  • Open a terminal window.
  • Go to the root directory of the example project.
  • Build and push the example web application using the following command:
    mvn clean tomcat7:redeploy
    This works both the first time the application is deployed and also for subsequent re-deploys.
  • Open the URL http://localhost:8080/springsecuritynoboot/ in a browser.
    A message saying “This is the Main Page” should appear with three links below it.
  • Click the “Get a Greeting from a web controller” link.
    A greeting message should appear.
  • Press the back button in the browser.
  • Click the “Static contents: test.html” link.
  • A message should appear saying “Hello, this is the static test page!”.
  • Click the “Back to Main Page” link.
  • Finally, click the “Logout” link.
    The result should be a HTTP status 404 – Not Found.

We have now taken the example web application for a spin and can note that all parts of it are accessible without having to login or authenticate in any other way.

Add (Spring) Security

Some parts of adding security to the example web application have already been made, such as adding a login page and the necessary Spring Security dependencies. The time has now come to add some Java configuration as to activate Spring Security.

Add log4j2.xml

Before looking at the security configuration, I’ll add a Log4J2 configuration file that will cause Spring Security to write debug-level logging.

  • In src/main/resources create a file named log4j2.xml with the following contents:
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration>
    <Appenders>
        <Console name="CONSOLE" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %-5p %-30C - %m%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.springframework.security" level="DEBUG"/>

        <Root level="DEBUG">
            <AppenderRef ref="CONSOLE" />
        </Root>
    </Loggers>
</Configuration>

Add Web Security Configuration

Now we’ve come to the core of this article, the part in which I show you how not to use Spring Security 5 – the web security configuration. It is quite extensive, so let’s dive right in.

  • In the package se.ivankrizsan.spring.web create a file named WebSecurityConfiguration.java with this contents:
package se.ivankrizsan.spring.web;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;

import static se.ivankrizsan.spring.web.MyWebApplicationInitializer.SPRINGSECURITY_FILTERCHAINPROXY_BEANNAME;

/**
 * Spring Security configuration for a Java web application that does not use Spring Boot.
 * This configuration has deliberately been written as to expose all the details of
 * configuring Spring Security in a web application.
 * This is not an example of how you would want to configure Spring Security in your
 * web application.
 *
 * @author Ivan Krizsan
 */
@Configuration
@EnableWebSecurity(debug = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    /* Constant(s): */
    /** Relative URL to custom login web-page. */
    public static final String LOGIN_PAGE_URL = "/static/login.html";
    /**
     * Relative URL to which form on login web-page will be POSTed when user attempts to log in.
     * Note the relationship to the above login web-page URL.
     */
    public static final String LOGIN_PAGE_POST_URL = "/static/login";
    /** Relative URL to which user will be directed after having logged out. */
    public static final String LOGOUT_SUCCESS_URL = "/index.jsp";
    /** Name of user that can log in to the example application. */
    public static final String USER_ADMIN_NAME = "admin";
    /** Password of user that can log in to the example application. */
    public static final String USER_ADMIN_PASSWORD = "secret";

    /**
     * Default constructor.
     * Disables default configuration since I want to show all the details of
     * a Java-based Spring Security configuration in a web application that does not
     * use Spring Boot.
     */
    public WebSecurityConfiguration() {
        super(true);
    }

    /**
     * Creates the Spring Security filter chain proxy bean configured with the supplied HTTP
     * firewall and the supplied security filters.
     * Note that the name of this bean must match the name supplied to the delegating
     * filter proxy created in the web application initializer.
     *
     * @param inHttpFirewall HTTP firewall with application-specific configuration.
     * @param inFilterSecurityInterceptor Responsible for security handling of HTTP resources.
     * @param inExceptionTranslationFilter Translates security-related exceptions to HTTP responses
     * and handles redirection to login page.
     * @param inSecurityContextPersistenceFilter Create and populate the security context.
     * @param inLogoutFilter Handles user log out.
     * @return Spring Security filter chain proxy bean.
     * @see MyWebApplicationInitializer
     */
    @Bean(name = SPRINGSECURITY_FILTERCHAINPROXY_BEANNAME)
    public Filter springSecurityFilterChain(
        final HttpFirewall inHttpFirewall,
        final FilterSecurityInterceptor inFilterSecurityInterceptor,
        final ExceptionTranslationFilter inExceptionTranslationFilter,
        final SecurityContextPersistenceFilter inSecurityContextPersistenceFilter,
        final UsernamePasswordAuthenticationFilter inUsernamePasswordAuthenticationFilter,
        final LogoutFilter inLogoutFilter) {

        /* Create the default filter chain that allow only logged in user to access pages in the application. */
        final MvcRequestMatcher theDefaultRequestMatcher = new MvcRequestMatcher(
            new HandlerMappingIntrospector(),
            "/**");
        final DefaultSecurityFilterChain theDefaultSecurityFilterChain = new DefaultSecurityFilterChain(
            theDefaultRequestMatcher,
            /* Filters start here. */
            inSecurityContextPersistenceFilter,
            inLogoutFilter,
            inUsernamePasswordAuthenticationFilter,
            inExceptionTranslationFilter,
            inFilterSecurityInterceptor);

        /* Create the security filter chain that allows anyone access to the login page. */
        final MvcRequestMatcher theLoginRequestMatcher = new MvcRequestMatcher(
            new HandlerMappingIntrospector(),
            LOGIN_PAGE_URL);
        final DefaultSecurityFilterChain theLoginSecurityFilterChain = new DefaultSecurityFilterChain(
            theLoginRequestMatcher);

        /*
         * Create the filter chain proxy with both the filter chains created above.
         * Note that the ordering of the filter chains in the list supplied to the
         * constructor of {@code FilterChainProxy} is significant:
         * The security filter chain allowing for access to the login page must be
         * placed before the default security filter chain, or else access to the
         * login page will not be allowed when a user needs to login.
         */
        final FilterChainProxy theFilterChainProxy = new FilterChainProxy(
            Arrays.asList(theLoginSecurityFilterChain, theDefaultSecurityFilterChain)
        );
        /*
         * Set the customized HTTP firewall on the filter chain proxy.
         * See {@link #customHttpFirewall} for details.
         */
        theFilterChainProxy.setFirewall(inHttpFirewall);

        /* Wrap the filter chain proxy in a debug filter as to get additional debug log. */
        final DebugFilter theDebugFilter = new DebugFilter(theFilterChainProxy);
        return theDebugFilter;
    }

    /**
     * Creates the logout filter bean that will invoke a set of logout handlers
     * when a user wants to log out and, upon successful logout, redirect to the
     * main application page.
     *
     * @return Logout filter.
     */
    @Bean
    public LogoutFilter logoutFilter() {
        /*
         * Create the list of logout handlers that are to be run when user logs out.
         * The {@code SecurityContextLogoutHandler} invalidates the HTTP session and
         * clears the current authentication from the security context.
         * The {@code CookieClearingLogoutHandler} clears cookies upon logout.
         * In this example, the JSESSIONID cookie will be cleared.
         */
        final List<LogoutHandler> theLogoutHandlers = new ArrayList<>();
        theLogoutHandlers.add(new SecurityContextLogoutHandler());
        theLogoutHandlers.add(new CookieClearingLogoutHandler("JSESSIONID"));

        /*
         * The logout filter only allows for one single logout handler to be configured,
         * so a {@code CompositeLogoutHandler} is created that will iterate over
         * the logout handlers in the supplied list.
         */
        final CompositeLogoutHandler theCompositeLogoutHandler =
            new CompositeLogoutHandler(theLogoutHandlers);

        /*
         * Create the logout filter that invokes the supplied logout handler when a user
         * logs out and, upon successful logout, redirects to the supplied URL.
         * In this example the user will be redirected to the main page (index.jsp).
         */
        return new LogoutFilter(LOGOUT_SUCCESS_URL, theCompositeLogoutHandler);
    }

    /**
     * Security context persistence filter bean responsible for retrieving information from
     * a security context repository and storing it in the security context prior to a request
     * and then store the information in the repository once the request has completed.
     * Uses the default {@link HttpSessionSecurityContextRepository} to store the security context.
     *
     * @return Security context persistence filter bean.
     */
    @Bean
    public SecurityContextPersistenceFilter securityContextPersistenceFilter() {
        final SecurityContextPersistenceFilter theSecurityContextPersistenceFilter =
            new SecurityContextPersistenceFilter();
        theSecurityContextPersistenceFilter.setForceEagerSessionCreation(true);
        return theSecurityContextPersistenceFilter;
    }

    /**
     * Exception translation filter bean that translates security-related
     * exceptions to HTTP responses.
     * The login page URL is configured on the exception translation filter, as to
     * ensure redirection to the login page in the case where the user is unauthorized
     * to view a resource.
     *
     * @return Exception translation filter bean.
     */
    @Bean
    public ExceptionTranslationFilter exceptionTranslationFilter() {
        final LoginUrlAuthenticationEntryPoint theLoginUrlAuthenticationEntryPoint =
            new LoginUrlAuthenticationEntryPoint(LOGIN_PAGE_URL);

        return new ExceptionTranslationFilter(theLoginUrlAuthenticationEntryPoint);
    }

    /**
     * Filter security interceptor bean responsible for handling security of HTTP resources
     * using the supplied authentication and access decision managers.
     *
     * @param inAuthenticationManager Authenticates users.
     * @param inAccessDecisionManager Determines whether access to HTTP resource is allowed or not.
     * @return Filter security interceptor bean.
     */
    @Bean
    public FilterSecurityInterceptor filterSecurityInterceptor(
        final AuthenticationManager inAuthenticationManager,
        final AccessDecisionManager inAccessDecisionManager) {

        /*
         * Create the security metadata source which contains mappings between web
         * resource URL patterns and, for each URL pattern, one or more security
         * configurations specifying what is required to access the resource(s)
         * behind the URL pattern.
         * Note that the ordering of these mappings is significant and should be
         * specific to general.
         */
        final LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> theRequestMap =
            new LinkedHashMap<>();
        /* Allow users with the role ADMIN to access anything in the web application. */
        theRequestMap.put(new AntPathRequestMatcher("/**"),
            Collections.singleton(new SecurityConfig("ROLE_ADMIN")));
        final DefaultFilterInvocationSecurityMetadataSource theSecurityMetadataSource =
            new DefaultFilterInvocationSecurityMetadataSource(theRequestMap);

        /* Create and configure the filter security interceptor. */
        final FilterSecurityInterceptor theFilterSecurityInterceptor = new FilterSecurityInterceptor();
        theFilterSecurityInterceptor.setAuthenticationManager(inAuthenticationManager);
        theFilterSecurityInterceptor.setSecurityMetadataSource(theSecurityMetadataSource);

        theFilterSecurityInterceptor.setAccessDecisionManager(inAccessDecisionManager);

        return theFilterSecurityInterceptor;
    }

    /**
     * Access decision manager bean responsible for making access control decisions.
     *
     * @return Access decision manager bean.
     */
    @Bean AccessDecisionManager accessDecisionManager() {
        final List<AccessDecisionVoter<?>> theAccessDecisionVoters = Arrays.asList(
            new AuthenticatedVoter(),
            new RoleVoter(),
            new WebExpressionVoter());

        return new AffirmativeBased(theAccessDecisionVoters);
    }

    /**
     * User name and password authentication filter bean that allows users to authenticate
     * using a login page and the supplied authentication manager.
     * This authentication filter allows session creation.
     *
     * @param inAuthenticationManager Authenticates users.
     * @return User name and password authentication filter bean.
     */
    @Bean
    public UsernamePasswordAuthenticationFilter usernamePasswordAuthenicationFilter(
        final AuthenticationManager inAuthenticationManager) {
        final UsernamePasswordAuthenticationFilter theUsernamePasswordAuthenticationFilter =
            new UsernamePasswordAuthenticationFilter();
        theUsernamePasswordAuthenticationFilter.setAuthenticationManager(inAuthenticationManager);
        theUsernamePasswordAuthenticationFilter.setAllowSessionCreation(true);
        /*
         * Set the URL which should be intercepted as a login attempt by the username-password
         * authentication filter.
         * The default is a POST request to /login but since I have placed the login page
         * in the file webapp/static/login.html, the POST request will be made to
         * /static/login instead of just /login.
         */
        theUsernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(
            new AntPathRequestMatcher(LOGIN_PAGE_POST_URL, HttpMethod.POST.name()));

        return theUsernamePasswordAuthenticationFilter;
    }

    /**
     * Creates an authentication manager responsible for authenticating users.
     * An authentication manager delegates user authentication to one or more authentication providers.
     * In this example, only one single authentication provider is used.
     *
     * @param inAuthenticationProvider Authentication provider that authentication manager will delegate
     * user authentication to.
     * @return Authentication manager bean.
     */
    @Bean
    public AuthenticationManager authenticationManager(final AuthenticationProvider inAuthenticationProvider) {
        return new ProviderManager(Collections.singletonList(inAuthenticationProvider));
    }

    /**
     * Creates the authentication provider responsible for authenticating users.
     * Retrieves user information from the supplied user details service.
     *
     * @param inUserDetailsService Service from which to retrieve user information.
     * @return Authentication provider bean.
     */
    @Bean
    public AuthenticationProvider authenticationProvider(final UserDetailsService inUserDetailsService) {
        final DaoAuthenticationProvider theAuthenticationProvider = new DaoAuthenticationProvider();
        theAuthenticationProvider.setUserDetailsService(inUserDetailsService);

        /*
         * Use the no-op password encoder, since we do not care about passwords being in
         * plaintext in this example program. This is of course not recommended in a
         * production environment.
         */
        theAuthenticationProvider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());
        return theAuthenticationProvider;
    }

    /**
     * Creates the user details service responsible for reading user information into memory.
     * In this particular example an {@code InMemoryUserDetailsManager} is used that retains
     * all user data in memory only.
     *
     * @return User details service bean.
     */
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        /* User information only stored in memory. */
        final InMemoryUserDetailsManager theUserDetailsManager = new InMemoryUserDetailsManager();

        /*
         * Create a user "admin" with the password "secret" in the "ADMIN" role.
         * Note that the role name must be prefixed with "ROLE_".
         */
        final SimpleGrantedAuthority theGrantedAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
        final UserDetails theUserDetails =
            new User(USER_ADMIN_NAME, USER_ADMIN_PASSWORD, Collections.singleton(theGrantedAuthority));

        /* Add the new user to the user details manager. */
        theUserDetailsManager.createUser(theUserDetails);

        return theUserDetailsManager;
    }

    /**
     * Creates a slightly relaxed version of the {@code StrictHttpFirewall} that allows
     * using semicolon in URLs, which Spring MVC normally does not.
     * With Spring Boot, creating this bean is enough and no further configuration is needed.
     * When not using Spring Boot, the custom HTTP firewall must be set on the {@code FilterChainProxy},
     * as can be seen in the method
     * {@link #springSecurityFilterChain(HttpFirewall, FilterSecurityInterceptor, ExceptionTranslationFilter, SecurityContextPersistenceFilter, UsernamePasswordAuthenticationFilter, LogoutFilter)}
     *
     * @return Slightly relaxed instance of the {@code StrictHttpFirewall}.
     */
    @Bean
    public HttpFirewall customHttpFirewall() {
        final StrictHttpFirewall theHttpFirewall = new StrictHttpFirewall();
        theHttpFirewall.setAllowSemicolon(true);
        return theHttpFirewall;
    }
}

Note that:

  • The configuration class is long, very long.
    This is due to the fact that I have deliberately avoided using Spring Boot and most means of aid in configuration supplied by Spring Security.
  • The configuration class is annotated with @EnableWebSecurity.
    This annotation enables web security and allow the configuration class to either implement the WebSecurityConfigurer interface or extend the class WebSecurityConfigurerAdapter. The latter class does implement the former interface.
  • The debug attribute of the @EnableWebSecurity annotation is set to true.
    Setting this attribute to true enables debug support in Spring Security.
    This in combination with setting the debug level to DEBUG in the Log4J2 configuration file earlier is done in order to have additional, more detailed, logging from Spring Security when running the example web application.
  • The configuration class extend the WebSecurityConfigurerAdapter class.
    The WebSecurityConfigurerAdapter class sets up a default configuration of Spring Security 5 for a web application and provides a number of methods that can be overridden in a subclass in order to customize the security configuration.
  • There is a default constructor in the configuration class.
    This constructor calls the superclass constructor enclosing a the boolean true that instructs the superclass to disable default configuration. This is in order to forcing this configuration class to have to perform all necessary configuration of Spring Security.
  • There is a method named springSecurityFilterChain that creates a Spring bean with the same name.
    The Spring Security filter chain where all the security filters, all parameters except the HttpFirewall, are brought together. It is a servlet filter through which all requests to the application will pass.
  • A Spring Security filter chain contain one or more filter chains.
    Each filter chain has a request matcher that determines whether the filters in the filter chain are to be applied to a request or not.
  • In the springSecurityFilterChain method there are two filter chains created.
  • The first filter chain has a request matcher configured with the URL pattern “/**”, which matches all requests to the application.
    This filter chain has a minimum set of Spring Security filters configured that will ensure that only logged in users can access the pages/resources in the application.
  • The second filter chain has a request matcher configured with the URL pattern “/static/login.html”, which match only the login page URL.
    This filter chain has no filters configured, which means that it will allow all requests to the login page of the application.
  • In the springSecurityFilterChain method an instance of FilterChainProxy is created.
    A list containing the two filter chains created earlier is passed as parameter to the constructor. The order of the filter chains in the list is significant – the login filter chain is before the default filter chain, otherwise the default filter chain would match all requests and users of the application would never be able to login.
    The FilterChainProxy instance is what will be the bean instance created in this method.
  • In the springSecurityFilterChain method, after the FilterChainProxy instance has been created, a HTTP firewall instance is set on the FilterChainProxy instance. The HttpFirewall instance is a bean that is autowired into this bean method. Details on the HTTP firewall bean will be discussed later.
  • Finally in the springSecurityFilterChain method, the FilterChainProxy instance is wrapped in a DebugFilter instance.
    The debug filter, commonly only used when debugging or during development, generates additional log when requests are received.
  • The next bean is of the type LogoutFilter.
    The logout filter bean contains one logout handler, a CompositeLogoutHandler, which in turn contains two logout handlers that will be invoked when a user logs out of the application. The first logout handler, the SecurityContextLogoutHandler, will invalidate the HTTP session and clears the authentication from the security context. The second logout handler, the CookieClearingLogoutHandler, clears the JSESSIONID cookie.
    In addition to the composite logout handler being supplied to the logout filter, an URL to which the filter will redirect upon a successful logout.
  • The next bean is of the type SecurityContextPersistenceFilter.
    This bean is responsible for retrieving information from a security context repository and storing it in the security context prior to a request being processed and then store the information back in the repository once the request has completed.
  • The next bean is a ExceptionTranslationFilter bean.
    This bean translates security-related exceptions to HTTP responses. A login page URL is configured on the exception translation filter, as to ensure redirection to the login page in the case where the user is not authorized to view a page or access a resource.
  • The filterSecurityInterceptor bean handles security for web pages and resources using the an authentication manager and an access decision manager.
    In addition a security metadata source is created, and set on the FilterSecurityInterceptor, which is configured to allow users in the ADMIN role to access any webpage or resource (matching the URL pattern /**) in the application.
  • The accessDecisionManager bean is responsible for making access control decisions.
    The particular type of access decision manager is a AffirmativeBased access decision manager which holds a number of access decision voters and allows access if any (one) of these voters returns an affirmative response.
  • The ability to log in using a username and a password is provided by the next bean, the usernamePasswordAuthenicationFilter bean.
    On this bean the login URL to which login requests are POSTed is also set. The default is /login but since I have placed the login webpage in webapp/static/login.html, the POST request will be made to /static/login instead of just /login.
  • The authenticationManager bean is responsible for authenticating users.
    It accomplishes this by delegating to one or more authentication providers. In this example, just one single authentication provider is used.
  • The next bean, authenticationProvider, is the single authentication provider in this application.
    The DaoAuthenticationProvider instance allows for setting a password encoder used when encoding and validating passwords. Such a password encoder could calculate the hash of the password, in order to avoid storing the password in plain-text. In this example, the NoOpPasswordEncoder is used. As the name implies, it does nothing, which makes it unsuitable for use in a production environment.
  • The userDetailsService bean is responsible for reading user information into memory.
    Normally, user data, such as login name and password, is stored in a database or such. In this example the user data is only retained in memory by the InMemoryUserDetailsManager. A user with the login name “admin”, the password “secret” and the role “ADMIN” is also created and added to the user details service.
  • The final bean is the customHttpFirewall.
    The reason why a custom HTTP firewall bean needs to be created is in order to allow the use of semicolon in URLs, which Spring MVC normally does not allow.

Update the Web Application Initializer

The web application initializer implemented in the class MyWebApplicationInitializer needs to be updated as to accomplish two more things:

  • Add the web security configuration class to the list of classes that will make up the application root Spring context.
  • Add a servlet filter, implemented by DelegatingFilterProxy, that will delegate web request filtering to the Spring Security filter chain bean.

Since the MyWebApplicationInitializer class is relatively short, the complete updated version of the class is listed here, with the additions highlighted.

package se.ivankrizsan.spring.web;

import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import javax.servlet.Filter;

/**
 * Web application initializer specifies the configuration files that will make up
 *  the root application Spring context.
 *  In addition this is the place where the servlet filters to be added and mapped
 *  to the dispatcher servlet.
 */
public class MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /* Constant(s): */
    public static final String SPRINGSECURITY_FILTERCHAINPROXY_BEANNAME = "springSecurityFilterChain";


    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new DelegatingFilterProxy(SPRINGSECURITY_FILTERCHAINPROXY_BEANNAME) };
    }

    /**
     * Retrieves list of configuration classes which are to be used to create the root application
     * context in the web application.
     *
     * @return Spring Java configuration classes to be used when creating root application context.
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { WebConfiguration.class, WebSecurityConfiguration.class };
    }

    /**
     * Retrieves list of configuration classes which are to be used to create the servlet application
     * context.
     *
     * @return Returns null, since all beans will be located in the root application context.
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

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

Second Run – With Security

The example application is now ready for another test-run.
If you want to see some additional information concerning what is happening behind the scenes when you issue requests to the example application, open a terminal window and tail the catalina.out log.

  • Open a terminal window.
    The terminal in IntelliJ IDEA can also be used.
  • Go to the root directory of the example project.
  • Build and push the example web application using the following command:
    mvn clean tomcat7:redeploy
    This works both the first time the application is deployed and also for subsequent re-deploys.
  • Open the URL http://localhost:8080/springsecuritynoboot/ in a browser.
    A message saying “This is the Main Page” should appear with three links below it.
  • Click the “Get a Greeting from a web controller” link.
    The login page with a message saying “You need to login to access that page” should appear.
  • Enter the user name “admin” and the password “secret”, without quotes, and click the Sign In button.
    The greeting from the web controller should appear.
  • Click the browser’s back button twice.
  • Click the “Static contents: test.html” link.
    A message should appear saying “Hello, this is the static test page!”.
    Since you have logged in, you should be able to access this page too.
  • Click the “Back to Main Page” link.
  • Click the “Logout” link.
    You will be redirected to the Main Page.
  • Click the “Static contents: test.html” link.
    The login page should appear again. Since you logged out, you are no longer authorized to view the test.heml page.

We see that the secured resources and web pages in the example application are now indeed unaccessible without having presented a valid user name and password. Updating the web application initializer and adding the web security configuration does successfully add security to the web application without having alter the application itself.

Final Words

Before leaving this overcomplicated Spring Security 5 example, let’s have a look at the relationships between the Spring beans and additional objects created by the web security configuration and how these are connected to the servlet mechanism:

Spring Security 5 configuration in the example program.

Spring Security 5 configuration in the example program (click to enlarge).

The green rectangles are Spring beans, the blue rectangles are Java objects – both created as a result of the web security configuration.
This concludes this article. I hope you have gained additional understanding of Spring Security 5 and what tremendous aid it supplies when adding security to web applications.

Happy coding!

One thought on “Spring Security 5 Overcomplicated

  1. Atul

    Nice tutorial and really a great explanation about the topic.
    Keep posting. 🙂

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *