Click here to Skip to main content
15,868,016 members
Articles / Web Development / Spring
Article

Using ThymeLeaf Page Template Engine with Spring Security

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
16 Feb 2021MIT17 min read 12.4K   164   1  
A tutorial on how to setup an application Spring Security and Spring MVC, and use ThymeLeaf for secured page rendering.
In this tutorial, you will learn how to integrate Spring Security with Thymeleaf Template Engine after getting a brief review of how to add Spring Security into a Spring MVC application.

Introduction

This is my second tutorial on Thymeleaf Template Engine. For this tutorial, I want to show how to integrate Spring Security with Thymeleaf Template Engine. I have written a tutorial on how to add Spring Security into a Spring MVC application. In this tutorial, I will review it. There are some new concepts I have learned. The most important part is how to use Spring Security tags with Thymeleaf Template Engine to secure the page rendering. Overall, the work to get all these to work is not difficult at all. I build this sample based on my previous Thymeleaf tutorial. My previous Thymeleaf tutorial can be found here. This tutorial also borrows heavily from my other tutorial on Spring Security integration. You can find this tutorial here. You don't have to check these two tutorials. The important concepts will be mentioned here. Please read on.

The Application Architecture

The sample application in this tutorial requires a user to log in first, or the user won't be able to access the secure resources provided by the application. The web application will have some new configurations to be added to the startup. In order to add such configuration, I had to add the spring-boot-starter-security jar to the project. Once this jar is added, user must be logged in in order to access the pages. This means that I have to do some more configuration change to allow certain pages to be accessible anonymously, and other pages restricted to the logged in user with property access permissions. I will explain how this is done, and how user authentication and authorization check works.

The more important concepts for this tutorial are the security tags to be used to show or hide parts of a web page. The web pages are rendered using Thymeleaf Template Engine. One thing I was concerned about was what these tags are. I was not at all concerned about how they can be used because I kinda know how. For this sample application, I only used the role based authorization. How the roles are assigned are part of security configuration, and will be explained in details. Then, there are the security tags. These are ThymeLeaf specific tags, very similar to the JSTL/Spring Security tags used in my other tutorial.

Since this tutorial mixes Spring Security and ThymeLeaf, let's begin with maven POM file. There are a couple of things to be discussed about this file.

The Maven POM File

For this sample application, I will package it as a jar file. This is the same as the I did for the previous ThymeLeaf tutorial. With Thymeleaf Template Engine, I don't have to worry about packaging the web application in war format. In order to support the use of Spring Security and security tags in HTML markup, I have to have these in the Maven POM file. First, I need to declare the parent POM for my project, which is spring-boot-starter-parent.

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

Next, I need to add all the necessary dependencies. For Spring Boot application, the needed dependencies are few. Here they are:

XML
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
  <dependency>
     <groupId>org.thymeleaf.extras</groupId>
     <artifactId>thymeleaf-extras-springsecurity5</artifactId>
     <version>3.0.4.RELEASE</version>
  </dependency>
</dependencies>

Here are a couple things to know about this. To enable security for the application, I will need to add the spring-boot-starter-security as a dependency. Once it is added, by default, all request to the application has to be authenticated. Some configuration can make access to requests a little more flexible. I will show you how in later sections. Next, I need to add a dependency of the ThymeLeaf core, which is spring-boot-starter-thymeleaf. Added this dependency allows me to use ThymeLeaf tags in the HTML pages, and allows me to create reusable components, called fragments. To be able to use security tags for the HTML page, I have to add the last dependency, called thymeleaf-extras-springsecurity5.

These five dependencies are all needed to get this application to work. This is the first step. In the next section, I will show you the source code of the start up class, and the security configuration. The security configuration is most important, since it determines what requests can be accessed without authentication, and what cannot.

Start Up and Security Configuration

The start-up class is very simple. Here is the class:

Java
package org.hanbo.boot.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App
{
   public static void main(String[] args)
   {
      SpringApplication.run(App.class, args);
   }
}

All I have to do is declare a class called "App". Then use annotation @SpringBootApplication to decorate it. This is passed into SpringApplication.run(). Spring Boot uses dependency injection to find inter-dependencies of object types. This is the place where the dependency mappings are created. The Spring IOC container will do a scan of the packages to find all the class types. As long as the classes are annotated with @Configuration, @Repository, @Service or @Components, as well as Controller and RestController, Spring will find them and create a lookup table for these. Then, the object instantiation will use the mappings to find the objects to be injected.

Let's take a look at the configuration class for setting up Spring Security. This is the hardest part of this sample application. This tutorial provided the most basic configuration for form based authentication integration with the web application. Here is the full source code of the class:

Java
package org.hanbo.boot.app.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.
       method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.
       SavedRequestAwareAuthenticationSuccessHandler;
import org.hanbo.boot.app.security.UserAuthenticationService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
   @Autowired
   private UserAuthenticationService authenticationProvider;
   
   @Autowired
   private AccessDeniedHandler accessDeniedHandler;
   
   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      http
      .authorizeRequests()
          .antMatchers("/assets/**", "/public/**").permitAll()
          .anyRequest().authenticated()
      .and()
      .formLogin()
          .loginPage("/login")
          .permitAll()
          .usernameParameter("username")
          .passwordParameter("userpass")
          .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
          .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
          .and()
      .logout().logoutSuccessUrl("/public/logout")
          .permitAll()
          .and()
      .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
   }
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
}

There are a few interesting things about this class. First, the class is declared as this:

Java
...
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
...
}

The class is decorated with three different annotations. The first marks the class as configuration provider (@Configuration). The second one enable Web Security for the web application (@EnableWebSecurity). The third one is an interesting one, what it does is to enable security annotation on the methods of the classes, especially on the methods of controller or RestController classes, and lock down the access to these methods.

The class extends from WebSecurityConfigurerAdapter. It can be used to provide customization of Web Security and HTTP security. When Spring framework gets this class, it will call the configure() method(s) of this class to do web security configuration. In my WebAppSecurityConfig class, there are two configure() methods. The first one is the security configuration, such as what pages can be accessed anonymously (without logging in) and what pages can only be accessed securely (must log in before access). Here it is:

Java
@Override
protected void configure(HttpSecurity http) throws Exception
{
   http
   .authorizeRequests()
       .antMatchers("/assets/**", "/public/**").permitAll()
       .anyRequest().authenticated()
   .and()
   .formLogin()
       .loginPage("/login")
       .permitAll()
       .usernameParameter("username")
       .passwordParameter("userpass")
       .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
       .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
       .and()
   .logout().logoutSuccessUrl("/public/logout")
       .permitAll()
       .and()
   .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}

This looks like a scary method. And it is a scary method. If I don't set it correctly, it will make me regret later. Let me just discuss one part at a time. The first is this:

Java
...
http
.authorizeRequests()
    .antMatchers("/assets/**", "/public/**").permitAll()
    .anyRequest().authenticated()
...

This part is fairly easy, I want all incoming requests except the ones with the parts of ".../assets/..." and ".../public/..." to be authenticated before access. For the requests with these sub paths, will be publicly accessible. The next part is the configuration of the login page, and what to do when login success and failed:

Java
...
.formLogin()
    .loginPage("/login")
    .permitAll()
    .usernameParameter("username")
    .passwordParameter("userpass")
    .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
    .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
...

I set the login page URL path as "<Application Base URL>/login". And this page is also set as public accessible. If I don't do this (set it as publicly accessible), it will require to be authenticated before it can be accessed, which creates the chick and egg paradox. Actually, there is no paradox. If I don't set the login page as public accessible, the users won't be able to login and won't be able to see the secured page.

The methods usernameParameter() and passwordParameter() set what are the input fields on the login form that could provide the user name and password. If you don't use these two methods to set the field names, there are default field names. I think they are "username" and "password". For my application, I set them as "username" and "userpass". Next, I use method successHandler() to set the action to redirect to the expected sub url if there is one. For example, if I want to access:

authenticationProvider:

http://localhost:8080/webapp/secure/page1

And I have not logged in, I will be first directed to the login page. With the invocation of .successHandler(new SavedRequestAwareAuthenticationSuccessHandler()), after I successfully logged in, this will redirect me back to the original url. If there is no url to redirect back, then I use the next method to set the default landing page when user successfully logged in, .defaultSuccessUrl("/secure/index", true). If user failed log in, then I use the last method to set the log in failed page, .failureUrl("/public/authFailed").

The next part is the configuration of the logout behavior:

Java
...
logout().logoutSuccessUrl("/public/logout")
.permitAll()
...

All this is to specify where the user will be redirected when user logs out. And the logout destination page will be set as accessible by public. Again, if the page is not publicly accessible. Then after logout, the logout page will not be accessible and user will get a 403 error.

The last configuration I want to make, is what to do with access denied errors. I would redirect this type of error to another page. Here is how I configured:

Java
...
.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
...

accessDeniedHandler object is a customized object. It is defined on the top of this class as this:

Java
@Autowired
private AccessDeniedHandler accessDeniedHandler;

The class that defines the accessDeniedHandler looks like this:

Java
package org.hanbo.boot.app.config;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// handle 403 page
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler
{
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException e) throws IOException, ServletException
    {
        Authentication auth
           = SecurityContextHolder.getContext().getAuthentication();

        if (auth != null)
        {
            System.out.println("User '" + auth.getName()
                    + "' attempted to access the protected URL: "
                    + httpServletRequest.getRequestURI());
        }

        httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + 
                                         "/public/accessDenied");
    }
}

All this handler does is intercept the occurred AccessDeniedException and instead of showing a Spring Boot error page, it redirects the response to my access denied page, which looks like this:

The handler when intercepted the access denied exception, will output to the terminal the string: "User '<username>' attempted to access the protected URL: <URL user attempted to access>". Here is a screenshot on the terminal:

At last, the question remains. How do I authorize the user? What I meant by authorizing is that after user's identity is confirmed, the application will determine what access the user should be given. Mostly, I use role based authorization. It is enough for all I need for my spare-time projects. If there is a need for a different type of authorization, permissions based authorization, Spring Security provided the necessary mechanism for it. But it will not be covered in this tutorial.

In my WebAppSecurityConfig class, I added an authenticationProvider object and configured for determining the authorization for the user. Here is the declaration and configuration:

Java
...

   @Autowired
   private UserAuthenticationService authenticationProvider;
   
...
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
   
...

The class UserAuthenticationService is defined as the following:

Java
package org.hanbo.boot.app.security;

import java.util.ArrayList;
import java.util.List;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class UserAuthenticationService
implements AuthenticationProvider
{
   @Override
   public Authentication authenticate(Authentication auth) throws AuthenticationException
   {      
      System.out.println("test");
      Authentication retVal = null;
      List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
      
      if (auth != null)
      {
         String name = auth.getName();
         String password = auth.getCredentials().toString();
         System.out.println("name: " + name);
         System.out.println("password: " + password);
         
         if (name.equals("admin") && password.equals("admin12345"))
         {
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_STAFF"));
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant Admin");
         }
         else if (name.equals("staff1") && password.equals("staff12345"))
         {i
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_STAFF"));
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant Staff");
         }
         else if (name.equals("user1") && password.equals("user12345"))
         {
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant User");
         }
      }
      else
      {
         System.out.println("invalid login");
         retVal = new UsernamePasswordAuthenticationToken(
            null, null, grantedAuths
         );
         System.out.println("bad Login");
      }
      
      System.out.println("return login info");
      return retVal;
   }

   @Override
   public boolean supports(Class<?> tokenType)
   {
      return tokenType.equals(UsernamePasswordAuthenticationToken.class);
   }
}

The object instantiated from this class can be used to authenticate user name and password. Once confirmed, I would assign user roles to the user. The admin user will have admin, staff, and user roles. The staff level user will have staff and user role. And the user level user can only have the user role. If the user cannot be authenticated, then no user role can be assigned.

That is all there is for the back end web security configuration. I just want to mention one more issue. In my previous tutorial, I have disabled CSRF for forms and HTTP POST operations. I think that was a bad thing to do. It weakens the security of the application. For this tutorial, I have enabled CSRF. In the next few sections, I will show how to utilize this functionality.

Utilize Spring Security with ThymeLeaf

The biggest reason I write this tutorial is that I want to know how to use the security tags with ThymeLeaf. I want to use ThymeLeaf for my next project, and having Spring Security integrated is vital for success. Before I can use it effectively, I want to test it out. Hence this tutorial is created. Turned out, it was easy to integrate Spring Security tags with ThymeLeaf. In the section on Maven POM, I have mentioned that dependency thymeleaf-extras-springsecurity5 is needed. This is the one that provides the security tags for the pages.

Next, the folder structure should be set up for ThymeLeaf Template Engine. Under the project folder resources, I created a folder called "templates". This is where the page templates and reusable fragments are stored. For this project, I set up four different pages:

  • One page every one can see, but each user with different role would see the sections of this page differently.
  • One page only administrator user can access.
  • One page only the staff level and admin user can see.
  • One page can be seen by admin user, staff level, and normal user. But not by anonymous user.

In order to setup these page templates to make sure different level of users can access the pages, I need to use the Spring Security tags on the page templates. Let me show you the screenshot of the secure index page where all user can access, but different users would see the page differently:

This screenshot is only viewable when user is an admin user. If the logged in user is a non admin level user, for example, a non admin, non staff level user logged in, will see the same page like this:

The difference of these two screenshots is differentiated by the Spring Security tags. This is the full source code of this page template:

HTML
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>Login</title>
    <link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap.min.css}"/>
    <link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap-theme.min.css}"/>
    <link rel="stylesheet" th:href="@{/assets/css/index.css}"/>
</head>
<body>
   <div class="container">
      <div th:replace="parts/pieces::logoutForm">
      </div>
		<div th:replace="parts/pieces::header">
		</div>
      
      <div class="row">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Index Page</h3>
                  <p>You can see this section as long as you are logged in.</p>
               </div>
            </div>
         </div>
      </div>
      
      <div class="row" sec:authorize="hasRole('USER')">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Only User can See This</h3>
                  <p>If you have the "ROLE_USER". 
                  You will be able to see this section. User Section.</p>
               </div>
            </div>
         </div>
      </div>

      <div class="row" sec:authorize="hasRole('STAFF')">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Only Staff can See This</h3>
                  <p>If you have the "ROLE_STAFF". 
                  You will be able to see this section. Staff Section.</p>
               </div>
            </div>
         </div>
      </div>
      
      <div class="row" sec:authorize="hasRole('ADMIN')">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <h3>Only Admin can See This</h3>
                  <p>If you have the "ROLE_ADMIN". 
                  You will be able to see this section. Admin Section.</p>
               </div>
            </div>
         </div>
      </div>

   </div>
   
   <script type="text/javascript" th:src="@{/assets/jquery/js/jquery.min.js}"></script>
   <script type="text/javascript" th:src="@{/assets/bootstrap/js/bootstrap.min.js}"></script>
</body>
</html>

The biggest parts of this page are the three sections, where they are only displayed based on the user's security roles. The section displays are enclosed like these:

HTML
<div class="row" sec:authorize="hasRole('USER')">
...
</div>

<div class="row" sec:authorize="hasRole('STAFF')">
...
</div>

<div class="row" sec:authorize="hasRole('ADMIN')">
...
</div>

The use of the attribute sec:authorize="hasRole('ADMIN')" is the use of Spring Security tag. The expression (written using SpEL) which assigned is very easy to understand, when the user has the role defined by such expression will make the authorization true to display the section. And this is exactly the same way Spring Security used with taglib. Surprise!

That is one interesting part of the page markup. Another one is adding fragments into the page template. These fragments can also use Spring Security tags. On the top of the above page markup, you will see this:

HTML
<div th:replace="parts/pieces::logoutForm">
</div>
   <div th:replace="parts/pieces::header">
   </div>

These are the places where I insert fragments from another file. The first <div> will add a new form. It is used to log out the user. The other <div> is to insert the navigation menu on top of the page. Forget about the form for now. I will explain that later. Using fragment to add navigation menu is one of the most common operations, because navigation menu can be shared in multiple pages, better to create it once and use it in all places it is needed.

I defined my fragments file named "pieces.html". It is stored in the sub folder "parts". The file is defined as this:

HTML
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
   <head>
      <meta charset="UTF-8"/>
      <title>Spring Boot Thymeleaf Application - Fragments</title>
   </head>
   <body>
      <div th:fragment="logoutForm">
         <form id="logoutForm" th:action="@{/logout}" 
          method="post" th:hidden="true" name="logoutForm">
            <input type="submit" value="Logout" />
         </form>
      </div>
      <div th:fragment="header">
         <div class="row">
            <div class="col-xs-12 text-right">
               <a href="/secure/adminPage">Admin Page</a>
               <a href="/secure/staffPage">Staff Page</a>
               <a href="/secure/userPage">User Page</a>
               <a href="javascript: document.logoutForm.submit();" 
                sec:authorize="isAuthenticated()">Logout</a>
            </div>
         </div>
      </div>
      <div th:fragment="logout_header">
         <div class="row">
            <div class="col-xs-12 text-right">
               <a href="javascript: document.logoutForm.submit();" 
                sec:authorize="isAuthenticated()">Logout</a>
            </div>
         </div>
      </div>
   </body>
</html>

In this file, I created three fragments. The first is the form that will log out the user. Again, please forget about this for now. I will explain what this does in the next section. The other two are simple navigation menus. One has four links. The other one has only one link. What I want to show is the other Spring Security tag that I use, and use frequently: sec:authorize="isAuthenticated()". This tag is used to render or not render elements when user's roles do not matter, as long as the user is logged in, then this tag will return true always, and allow the tagged element to be rendered.

Let's take a look at the controller class for rendering these pages:

Java
package org.hanbo.boot.app.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;"

@Controller
public class SecuredPageController
{
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/index", method = RequestMethod.GET)
   public ModelAndView index1()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("indexPage");
        return retVal;
   }
   
   @PreAuthorize("hasRole('ADMIN')")
   @RequestMapping(value="/secure/adminPage", method = RequestMethod.GET)
   public ModelAndView adminPage()
   {
        ModelAn      <div th:replace="parts/pieces::logoutForm">
      </div>dView retVal = new ModelAndView();
        retVal.setViewName("AdminPage");
        return retVal;
   }
   
   @PreAuthorize("hasRole('STAFF')")
   @RequestMapping(value="/secure/staffPage", method = RequestMethod.GET)
   public ModelAndView staffPage()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("StaffPage");
        return retVal;
   }
   
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/userPage", method = RequestMethod.GET)
   public ModelAndView userPage()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("UserPage");
        return retVal;
   }
}

These methods that handle the user requests all use annotation @PreAuthorize("hasRole('<User Role Name>')"). It means, the request must be authenticated and have user role(s) associated to be handled by the back end code. Have you noticed that in my UserAuthenticationService class, the roles assigned to the user are prefixed with "ROLE_" In Spring Security version 5 and above, the prefix "ROLE_" is no longer necessary in the SPeL hasRole() expression. This is one last point I want to reveal.

Lastly, I want to talk about CSRF token and logout.

CSRF Token and Log Out Operation

CSRF is a type of attack. You can search it online and read all about it. To prevent this, Spring Security provided the CSRF token as a mitigation mechanism. That is, every time a page that contains a form is being rendered using page template engine (like ThymeLeaf), the back end Spring MVC/Spring Security would create a CSRF attached as a hidden field on the form. When the form is submitted, the CSRF will be sent back to the back end, then the form will be processed successfully. The token serves as a handshake signal between front end and back end.

I believe since Spring Security 5, with CSRF token enabled, the logout can only be done with an HTTP POST, not an HTTP GET. This is a bit tricky, but searching online, I was able to find an answer. The solution proposed online is to create a hidden form. And all it had is a submit button which invokes a HTTP POST for logout.

Since the logout form is common to all the four pages. I defined it as a fragment:

HTML
<div th:fragment="logoutForm">
    <form id="logoutForm" th:action="@{/logout}" 
     method="post" th:hidden="true" name="logoutForm">
    <input type="submit" value="Logout" />
    </form>
</div>

It is placed on to the target page with the following:

HTML
<div th:replace="parts/pieces::logoutForm">
</div>

The question is, how do I invoke this form. It can be done with a one-line JavaScript code. Here it is:

HTML
<a href="javascript: document.logoutForm.submit();" sec:authorize="isAuthenticated()">Logout</a>

Here, I invoke document.logoutForm.submit();. This will invoke the form and perform the logout. This is a very primitive way of performing the logout. But for this simple tutorial, it will do. The question is, if I replace it with a single page web application using AngularJS or some other web application framework, what would I do? Don't have a good answer for this yet.

Anyways, the post operation was not handled by my controller method. Spring Security provided the handling method internally. However, if the logout operation is successful, it will redirect the page to /public/logout. This URL is handled by my controller method, which looks like this:

Java
@RequestMapping(value="/public/logout", method = RequestMethod.GET)
public ModelAndView logout()
{
   ModelAndView retVal = new ModelAndView();
   retVal.setViewName("logoutPage");
   return retVal;
}

At this point, all there is for this web application has been discussed. Next, I will discuss how to test this.

How to Test

After you download the full source code, and unzip it, please rename all the files that are *.sj back to *.js files.

Once it is done, make sure you have JDK 14 or above installed. This web application is set for compiled and packaged with JDK 14 or above. Use the following command to compile and package the application:

mvn clean install

When the build finishes successfully, run the following command to start up the application:

java -jar target/thymeleaf-security-sample-1.0.0.jar

When the command runs, it will spit out a lot of log messages. When you can confirm the application started successfully, you can test the application by navigating to:

http://localhost:8080

Note that it is possible that the secure session won't form. In this case, you can utilize HTTPS instead of HTTP. Some research is necessary, but it is very easy to do. Good luck.

Summary

This tutorial article discusses the integration of ThymeLeaf Template Engine and Spring Security. The sample application uses the standard form based authentication and role based authorization. I wrote this for myself, so that I can reference back in case I need to setup a project that uses similar mechanism. Unlike some of my previous tutorial, I focused on the most important aspects of this sample application. Some of the things were discussed in my previous tutorials. They were important. Also, they are new for Spring Security version 5 and above.

While I was writing this, I thought it would be cool to enhance it with OAuth, that is, using Google Authentication to authenticate the user log in. That will be a good tutorial. When I researched into the matter, it seemed it can be solved easily. The hard part might be the authorization piece. Also, I wonder if CSRF token would also be needed. I don't know. Another question I had was what I need to do to get CSRF token added into the AngularJS application. There must be a way to get a token then use with HTTP POST method to interact with the back end. But this kind of sample application can be tedious to setup. So please stay tuned.

Besides these two ideas, I have another tutorial lined up. JavaScript with ES6 syntax for AngulaJS application. This will be the next tutorial for this year. I have lots of ideas all packed for this year. It will be a fantastic one, stay tuned. And good luck running the sample application.

History

  • 15th February, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Team Leader The Judge Group
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --