DEV Community

Thomas
Thomas

Posted on • Originally published at bootify.io

Form Login with Spring Boot and Thymeleaf

The form-based login is often the first choice to protect the web frontend of a Spring Boot application. It ensures that certain areas of our application are only accessible once a user has authenticated himself with a username and password, and this status is stored in the session. What are the steps required to add form based login to our Spring Boot application?

Start with a simple application

First, we use the Bootify Builder to create a simple Spring Boot application in the current version 3.3.2 - to do this, we simply click on open project. There we select Thymeleaf + Bootstrap as frontend stack - Thymeleaf is the most used template engine of Spring Boot and allows server-side rendering. Bootstrap will be integrated into our app as a WebJar. Select any database which you would like to connect to - an embedded database will do for now as well.

In the Entities Tab we create the tables User as well as TodoList, and connect them with an N:1 relation. For the TodoList we activate the CRUD option for the frontend - this will be the area we secure with Spring Security afterwards.


  Preview of our very simple database schema

Now we can already download the finished application and import it into our favorite IDE.


  The first version of our application in IntelliJ

Configuration of Spring Security

The form-based login is provided with the help of Spring Security. So we first need the relevant dependencies, which we add to our build.gradle or pom.xml respectively.

implementation('org.springframework.boot:spring-boot-starter-security')
implementation('org.thymeleaf.extras:thymeleaf-extras-springsecurity6')
Enter fullscreen mode Exit fullscreen mode

The module spring-boot-starter-security integrates Spring Security. With thymeleaf-extras-springsecurity6 a little helper is included which provides the authentication state in our Thymeleaf templates - more about that later.

With this we can already provide the central security configuration - here directly in our final version.

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class HttpSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            final AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
        return http.cors(withDefaults())
                .csrf(withDefaults())
                .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
                .formLogin(form -> form
                    .loginPage("/login")
                    .usernameParameter("email")
                    .failureUrl("/login?loginError=true"))
                .logout(logout -> logout
                    .logoutSuccessUrl("/login?logoutSuccess=true")
                    .deleteCookies("JSESSIONID"))
                .exceptionHandling(exception -> exception
                    .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login?loginRequired=true")))
                .build();
    }

}
Enter fullscreen mode Exit fullscreen mode

  Our configuration for the form login

Theoretically, Spring Security can provide the login form with less configuration, but this would lack the design and some messages which we would like to present to the user. But let's go through the config first.

BCryptPasswordEncoder is standard nowadays to store the hash of a password together with its individual salt inside a single field. If we don't have a legacy requirement, we should use this one. Also, we provide the AuthenticationManager as a bean to be able to integrate it into other services.

As a third bean we create the SecurityFilterChain as this is the required approach since Spring 3.0. We should configure both CORS and CSRF properly to close the corresponding attack vectors. The default configuration is usually sufficient for this.

At our config class we have placed the annotation @EnableMethodSecurity and will protect the desired controller endpoints with @PreAuthorize(...) later on. Therefore we allow access to the whole application with permitAll(). Without the annotation based security, we should configure the paths to the protected resources at this place as well.

The formLogin() and the logout() methods are customized for our subsequent controller so that we can always display an appropriate message to the user. Spring Security automatically provides an endpoint for the login where username and password can be submitted to via POST request. Here we change the name of the username field to "email". The logout is modified to redirect back to the login page with a parameter afterwards.

Loading users from our database

To load the users from the already created table, we need to provide an implementation of UserDetailsService as a bean - it will be automatically found and used as a user source.

@Service
public class HttpUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public HttpUserDetailsService(final UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public HttpUserDetails loadUserByUsername(final String username) {
        final User user = userRepository.findByEmailIgnoreCase(username);
        if (user == null) {
            log.warn("user not found: {}", username);
            throw new UsernameNotFoundException("User " + username + " not found");
        }
        final List<SimpleGrantedAuthority> roles = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
        return new HttpUserDetails(user.getId(), username, user.getHash(), roles);
    }

}
Enter fullscreen mode Exit fullscreen mode

  UserDetailsService implementation automatically used as the user source

In our repository we should add a method User findByEmailIgnoreCase(String email) to execute a search query against the database - ignoring upper/lower case allows the user small mistakes when writing their email. The role here is always ROLE_USER for each user. Since we don't have a registration endpoint available at this point, we can add a simple data loader along with our application for now. The profile "local" is required for it to become active.

@Component
@Profile("local")
public class UserLoader implements ApplicationRunner {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserLoader(final UserRepository userRepository,
                      final PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public void run(final ApplicationArguments args) {
        if (userRepository.count() != 0) {
            return;
        }
        final User user = new User();
        user.setEmail("test@test.com");
        user.setHash(passwordEncoder.encode("testtest"));
        userRepository.save(user);
    }

}
Enter fullscreen mode Exit fullscreen mode

  Helper class to initialize a first user locally

Add login controller

With this we can already add the LoginController. Since the POST endpoint is automatically provided by Spring Security, a GET endpoint is sufficient here to show the template to the user.

@Controller
public class AuthenticationController {

    @GetMapping("/login")
    public String login(@RequestParam(name = "loginRequired", required = false) final Boolean loginRequired,
            @RequestParam(name = "loginError", required = false) final Boolean loginError,
            @RequestParam(name = "logoutSuccess", required = false) final Boolean logoutSuccess,
            final Model model) {
        model.addAttribute("authentication", new AuthenticationRequest());
        if (loginRequired == Boolean.TRUE) {
            model.addAttribute(WebUtils.MSG_INFO, WebUtils.getMessage("authentication.login.required"));
        }
        if (loginError == Boolean.TRUE) {
            model.addAttribute(WebUtils.MSG_ERROR, WebUtils.getMessage("authentication.login.error"));
        }
        if (logoutSuccess == Boolean.TRUE) {
            model.addAttribute(WebUtils.MSG_INFO, WebUtils.getMessage("authentication.logout.success"));
        }
        return "authentication/login";
    }

}
Enter fullscreen mode Exit fullscreen mode

  Backend for rendering the login page

The request parameters that we had already specified in our security configuration are converted to corresponding messages here. In our simple application from Bootify the corresponding helpers are already included. Here we also need the AuthenticationRequest object with getters and setters.

public class AuthenticationRequest {

    @NotNull
    @Size(max = 255)
    private String email;

    @NotNull
    @Size(max = 255)
    private String password;

}
Enter fullscreen mode Exit fullscreen mode

The corresponding template for our controller could then look like this.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout}">
    <head>
        <title>[[#{authentication.login.headline}]]</title>
    </head>
    <body>
        <div layout:fragment="content">
            <h1 class="mb-4">[[#{authentication.login.headline}]]</h1>
            <div th:replace="~{fragments/forms::globalErrors('authentication')}" />
            <form th:action="${requestUri}" method="post">
                <div th:replace="~{fragments/forms::inputRow(object='authentication', field='email')}" />
                <div th:replace="~{fragments/forms::inputRow(object='authentication', field='password', type='password')}" />
                <input type="submit" th:value="#{authentication.login.headline}" class="btn btn-primary mt-4" />
            </form>
        </div>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

As Thymeleaf doesn't allow direct access to request object anymore, we're providing the requestUri in the model.

@ModelAttribute("requestUri")
String getRequestServletPath(final HttpServletRequest request) {
    return request.getRequestURI();
}
Enter fullscreen mode Exit fullscreen mode

_  Providing the requestUri - as part of the AuthenticationController or a general ControllerAdvice _

With this template we send a POST request to the /login endpoint. The INFO or ERROR messages are automatically displayed by the layout. All used messages have to be present in our messages.properties.

authentication.login.headline=Login
authentication.email.label=Email
authentication.password.label=Password
authentication.login.required=Please login to access this area.
authentication.login.error=Your login was not successful - please try again.
authentication.logout.success=Your logout was successful.
navigation.login=Login
navigation.logout=Logout
Enter fullscreen mode Exit fullscreen mode

Last we can extend our layout.html. With this we also always show a login / logout link in the header. Spring Security also automatically provides a /logout endpoint, but we have to address it via POST.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
            xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <!-- ... -->    
    <a sec:authorize="!isAuthenticated()" th:href="@{/login}" class="nav-link">[[#{navigation.login}]]</a>
    <form sec:authorize="isAuthenticated()" th:action="@{/logout}" method="post" class="nav-link">
        <input th:value="#{navigation.logout}" type="submit" class="unset" />
    </form>
    <!-- ... -->
</html>
Enter fullscreen mode Exit fullscreen mode

  Adding login / logout links to our layout

In the html tag we've extended the namespace to use the helpers from the thymeleaf-extras-springsecurity6 module. As a final step we only need to add the annotation @PreAuthorize("hasAuthority('ROLE_USER')") at our TodoListController.

Starting our application

With this we have all needed pieces of our puzzle together! Now we start our application and when we want to see the todo lists, we should be redirected to the login page. Here we can log in with test@test.com / testtest.


  Automatic redirect to the login

In the Free plan of Bootify, Spring Boot prototypes with its own database schema, REST API and frontend can be generated. In the Professional plan, among other things, Spring Security with the form-based login is available to generate the setup described here - exactly matching the created database and the selected settings. A registration endpoint and role source can be specified as well.

» See Features and Pricing

Top comments (0)