Spring Security - Basic Login Form

In this series, I hope to show you some techniques for using spring security and the broader spring ecosystem to build and develop secure application web servers.

Today, we’ll be looking at building a basic login form page using:

  • Spring Boot
  • Spring Security, and
  • Thymeleaf
Source code for this example can be found on github:
codenerve-com/spring-security
An introduction to spring security. Contribute to codenerve-com/spring-security development by creating an account on GitHub.

First off lets briefly cover some of the project files that are of less interest:

  • Application.java — contains our main method and the @SpringBootApplication annotation.
  • A CSS stylesheet is located under src/main/resources/static/css/ to make the demo pretty
  • Some Thymeleaf HTML templates are located under src/main/resources/ for demo purposes also
  • An application.properties file is located under src/main/resources where we can pass configuration parameters to our spring boot application. A full list of options can be found here

Dependencies

As with all Spring boot application’s, there are a number of ‘Starter’ libraries that make it easy to add jars to your classpath.
In addition, these can auto-configure various spring beans and behaviours that we can make use of:

<?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>com.codenerve</groupId>
	<artifactId>spring-security-basic</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<name>spring-security-basic</name>
	<description>An introduction to spring security</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.5.RELEASE</version>
	</parent>

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

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>


		<!-- TEST -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
  • spring-boot-starter-parent — brings in all the required spring dependencies and manages there versions
  • spring-boot-starter-thymeleaf — adds thymeleaf-spring5 and thymeleaf-extras-java8time dependencies (more on thymeleaf)
  • spring-boot-starter-security — adds spring-security-config, spring-security-web and spring-aop dependencies
  • spring-boot-starter-test — adds spring-boot-test, junit, hamcrest and mockito
  • spring-security-test — adds the ability to mock user and user roles

MvcConfig

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/index").setViewName("index");
        registry.addViewController("/login").setViewName("login");
    }
}

This class implements spring’s WebMvcConfigurer interface. This allows you to override the method addViewControllers (as well as others) which is a way to configure simple automated controllers. In this example, we have mapped them to our Thymeleaf views (under src/main/resources).

WebSecurityConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers( "/css/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/index")
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and().csrf().disable(); // we'll enable this in a later blog post
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user").password("{noop}pass").roles("USER");
    }
}

There’s a lot going on in this class as it contains the main security configuration to enable and configure our basic login form.

@EnableWebSecurity
A Marker which allows spring to find your custom web application security configuration

WebSecurityConfigurerAdapter
Another abstract base class that allows you to override certain aspects of spring security’s default configuration. In our case, we are going to override the methods configure and configureGlobal.

configure(HttpSecurity http) method
Note the parameter passed to this method. There are several different configure methods. We will be overriding and configuring HttpSecurity specifically.

  • authorizeRequests() — Allows us to configure which resources on our web server to secure. You can see from our example code we have allowed un-secured access to our CSS directory and requested that all other resources are secured and can only be accessed by an authenticated user.
  • formLogin() — Tells spring security that we wish to use a login form. We provide the login URL, the URL we want to redirect to if the authentication is successful and finally permit access to the login and logout endpoints.
  • csrf() — For now we are disabling cross-site request forgery protection which by default is enabled. We will cover this later in the series.

configureGlobal method
Allows us to autowire an AuthenticationManager which will be used globally throughout our application. For this example, we are using a basic in-memory approach with one user and one user role.

Thymeleaf namespace

A few things to note in our login.html
The thymeleaf spring security namespace:

<html xmlns="http://www.w3.org/1999/xhtml"  
xmlns:th="http://www.thymeleaf.org"    
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">

The thymeleaf action which instructs the form to do a POST request to the URL provided (/login):

<form th:action="@{/login}" method="post" class="form login">

The thymeleaf if condition can check for URL parameters, error and log out responses and display content if they return true:

<div class="text--center" th:if="${param.error}">
    Invalid username and password.
</div>
<div class="text--center" th:if="${param.logout}">
     You have been logged out.
</div>

Full list of what's available on the thymeleaf website

Demo

To run the demo open the Application.java class and right click run. In order to start the example, port 8080 will need to be available on your machine. If it is not you can change this default in the application.properties file using:

server.port=8081

Set this to whatever value you wish.


Unit Testing

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void loginWithValidUserThenAuthenticated() throws Exception {
        FormLoginRequestBuilder login = formLogin()
                .user("user")
                .password("pass");

        mockMvc.perform(login)
                .andExpect(authenticated().withUsername("user"));
    }

    @Test
    public void loginWithInvalidUserThenUnauthenticated() throws Exception {
        FormLoginRequestBuilder login = formLogin()
                .user("invalid")
                .password("invalidpassword");

        mockMvc.perform(login)
                .andExpect(unauthenticated());
    }

    @Test
    public void accessUnsecuredResourceThenOk() throws Exception {
        mockMvc.perform(get("/css/style.css"))
                .andExpect(status().isOk());
    }

    @Test
    public void accessSecuredResourceUnauthenticatedThenRedirectsToLogin() throws Exception {
        mockMvc.perform(get("/hello"))
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrlPattern("**/login"));
    }

    @Test
    @WithMockUser
    public void accessSecuredResourceAuthenticatedThenOk() throws Exception {
        mockMvc.perform(get("/index"))
                .andExpect(status().isOk());
    }
}

@RunWith(SpringRunner.class)
Tells JUnit to run unit tests with Spring’s testing support

@SpringBootTest
Run as spring boot app. i.e. load application.properties and spring beans

@AutoConfigureMockMvc
Creates a Test helper class called MockMvc. From this, we can imitate a front-end client making requests to the server.

@WithMockUser
Provides the ability to mock certain users. An authenticated user in our case.

FormLoginRequestBuilder
A utility class that allows us to create a form based login request.

Next

Next up, we will be covering spring security’s user roles and the ability to hide and show content on our site based on the user’s permissions.

Michael Whyte

Michael Whyte