logo

Build a Basic Login Form With Spring Security, Thymeleaf, and Java

Published on
Authors

Hello Java developers! If you are developing Spring boot-based applications, Spring Security is the de-facto standard for securing your Spring-based applications.

In this short tutorial, we'll be looking at building a basic login form using Spring Boot, Spring Security, and Thymeleaf.

First, let's briefly cover some project files that are of less interest:

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

Dependencies

As with all Spring boot application's, there are many 'Starter' libraries that make it easy to add jars to your classpath:

  • spring-boot-starter-parent — brings in all the required spring dependencies and manages their 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

Here is the entire pom.xml file:

<?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>dev.mwhyte</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.6.1</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>17</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>

MvcConfig

package dev.mwhyte.spring.sec.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@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");
  }
}

The MvcConfig.java 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

package dev.mwhyte.spring.sec.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@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");
  }
}

A lot is going on in this class as it contains the main security configuration to enable and configure our basic login form.

Extending WebSecurityConfigurerAdapterAnother 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 method configure .

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

  • authorizeRequests() — Allows us to configure which resources on our webserver 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 URL we want to redirect to if the authentication is successful, and finally, we permit access to the login and logout endpoints.
  • We are disabling cross-site request forgery protection, which is enabled by default. We will cover this later in the series.

configureGlobal *_**methodThis method allows us to autowire a _AuthenticationManager* class 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.htmlfile:

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"
></html>

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

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

The thymeleaf if the 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


Unit Testing

package dev.mwhyte.spring.sec;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.FormLoginRequestBuilder;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

@ExtendWith(SpringExtension.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());
  }
}

@ExtendWith(SpringExtension.class)Tells JUnit5 to run unit tests with Spring's testing support

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

@AutoConfigureMockMvcCreates a Test helper class called MockMvc. We can imitate a front-end client making requests to the server from this.

@WithMockUserProvides the ability to mock certain users—an authenticated user in our case.

FormLoginRequestBuilderA utility class that allows us to create a form-based login request.


Demo

To run the demo, you can find the complete code for this example on GitHub

Open the Application.java class and right-click run. The port 8080 will need to be available on your machine to start the demo. 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.

Alternatively, you can watch this short video:


That’s all. 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 entitlements.

Spring Security-User Roles and ThymeLeaf Extras


Resources

GitHub - mwhyte-dev/spring-security at 1.basic-form-login

Spring Security

Thymeleaf + Spring Security integration basics