Spring Boot + Spring Security + Thymeleaf Form Login Example
This tutorial demonstrates how to configure spring-boot
, spring-security
and thymeleaf
with form-login
. We secure our web application using spring security form-login. We create a reusable Thymeleaf layout which we can use to create our secured and unsecured pages. When a user accesses a protected resource with insufficient rights we redirect the user to an access-denied
page. Finally we create a login page where the user is able to login to the application. And we create a logout link.
Project Structure
Let’s start by looking at the project structure.
Maven Dependencies
We use Apache Maven to manage our project dependencies. Make sure the following dependencies reside on the class-path. We are upgrading Thymeleaf to version 3 using the following Maven properties.
thymeleaf.version
– specifies the Thymeleaf version.thymeleaf-layout-dialect.version
– specifies the layout templating dialect.thymeleaf-extras-springsecurity4.version
– specifies the spring security integration.
<?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.memorynotfound.spring.security.formlogin</groupId>
<artifactId>thymeleaf</artifactId>
<version>1.0.0-SNAPSHOT</version>
<url>http://memorynotfound.com</url>
<name>Spring Security - ${project.artifactId}</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<!-- upgrade to thymeleaf version 3 -->
<thymeleaf.version>3.0.8.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
<thymeleaf-extras-springsecurity4.version>3.0.2.RELEASE</thymeleaf-extras-springsecurity4.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-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<!-- bootstrap and jquery -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Spring Security Configuration
SecurityConfig
extends WebSecurityConfigurerAdapter
. This allows us to override the configure(HttpSecurity http)
and configure(AuthenticationManagerBuilder auth)
methods. In the first method, we configure the HttpSecurity
. This allows us to configure static resources, form authentication login and logout configurations. We also can add a custom AccessDeniedHandler
. In the second method we configure some default InMemory
users to use in this example.
package com.memorynotfound.spring.security.config;
import com.memorynotfound.spring.security.web.LoggingAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoggingAccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(
"/",
"/js/**",
"/css/**",
"/img/**",
"/webjars/**").permitAll()
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
.and()
.withUser("manager").password("password").roles("MANAGER");
}
}
We create a custom 403: Access Denied Handler. In this handler we log the user who is trying to access a protected resource which he doesn’t have sufficient rights and redirect the request to /access-denied
.
package com.memorynotfound.spring.security.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@Component
public class LoggingAccessDeniedHandler implements AccessDeniedHandler {
private static Logger log = LoggerFactory.getLogger(LoggingAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException ex) throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
log.info(auth.getName()
+ " was trying to access protected resource: "
+ request.getRequestURI());
}
response.sendRedirect(request.getContextPath() + "/access-denied");
}
}
Spring Boot
We start the application using Spring Boot.
package com.memorynotfound.spring.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Run {
public static void main(String[] args) {
SpringApplication.run(Run.class, args);
}
}
Contains the mappings to the views used by the ThymeleafViewResolver
.
package com.memorynotfound.spring.security.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String root() {
return "index";
}
@GetMapping("/user")
public String userIndex() {
return "user/index";
}
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/access-denied")
public String accessDenied() {
return "/error/access-denied";
}
}
Configure Thymeleaf with Spring Boot
We configure Thymeleaf and Spring Boot using the application.yml
file located in the src/main/resources
folder.
server:
port: 8080
spring:
thymeleaf:
cache: false
check-template: true
check-template-location: true
content-type: text/html
enabled: true
encoding: UTF-8
mode: HTML
prefix: classpath:/templates/
suffix: .html
# excluded-view-names:
# template-resolver-order:
# view-names:
logging:
level:
root: WARN
com.memorynotfound: DEBUG
org.springframework.web: INFO
org.springframework.security: INFO
Thymeleaf Templating Configuration
All Thymeleaf templates are located in the src/main/resources/templates/
folder.
Creating the Thymeleaf Layout Template
First, we create the layout template. This layout is used by each page. The layout is located at src/main/resources/templates/fragments/layout.html
.
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/>
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Spring Security Thymeleaf</title>
</head>
<body>
<th:block th:replace="fragments/header :: header"/>
<div class="container">
<th:block layout:fragment="content"/>
</div>
<th:block th:replace="fragments/footer :: footer"/>
</body>
</html>
Creating the header
Next, we create a header. The header is located at src/main/resources/templates/fragments/header.html
.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="header">
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" th:href="@{/}">
<img th:src="@{/img/logo.png}" alt="memorynotfound logo"/>
</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a th:href="@{/}">Home</a>
</li>
</ul>
</div>
</div>
</nav>
</th:block>
</body>
</html>
Creating the footer
Next, we create a footer. The header is located at src/main/resources/templates/fragments/footer.html
.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<body>
<th:block th:fragment="footer">
<footer>
<div class="container">
<p>
© Memorynotfound.com
<span sec:authorize="isAuthenticated()" style="display: inline-block;">
| Logged user: <span sec:authentication="name"></span> |
Roles: <span sec:authentication="principal.authorities"></span> |
<a th:href="@{/logout}">Sign Out</a>
</span>
</p>
</div>
</footer>
<script type="text/javascript" th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</th:block>
</body>
</html>
Creating the Home Page
Next, we create the Home Page. The home page uses the previous layout and overrides the content
fragment. The home page is located at src/main/resources/templates/index.html
.
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{fragments/layout}">
<head>
<title>Index</title>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
<h1>Hello Spring Security</h1>
<p>This is an unsecured page, but you can access the secured pages after authenticating.</p>
<ul>
<li>Go to the <a href="/user" th:href="@{/user}">secured pages</a></li>
</ul>
</div>
</body>
</html>
Creating the Login Page
Next, we create the Login Page. The login page uses the previous layout and overrides the content
fragment. The login page is located at src/main/resources/templates/login.html
.
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{fragments/layout}">
<head>
<title>Login</title>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1>Login page</h1>
<form th:action="@{/login}" method="post">
<div th:if="${param.error}">
<div class="alert alert-danger">
Invalid username or password.
</div>
</div>
<div th:if="${param.logout}">
<div class="alert alert-info">
You have been logged out.
</div>
</div>
<div class="form-group">
<label for="username">Username</label>:
<input type="text"
id="username"
name="username"
class="form-control"
autofocus="autofocus"
placeholder="Username">
</div>
<div class="form-group">
<label for="password">Password</label>:
<input type="password"
id="password"
name="password"
class="form-control"
placeholder="Password">
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<input type="submit"
name="login-submit"
id="login-submit"
class="form-control btn btn-info"
value="Log In">
</div>
</div>
</div>
</form>
</div>
</div>
<p><a href="/" th:href="@{/}">Back to home page</a></p>
</div>
</body>
</html>
Creating the Secured User Page
Next, we create the Secured User Page. This page is secured, and only visible for users who have the ROLE_USER
. The user page is located at src/main/resources/templates/user/index.html
.
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{fragments/layout}">
<head>
<title>Secured</title>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
<h1>This is a secured page!</h1>
<p><a href="/" th:href="@{/}">Back to home page</a></p>
</div>
</body>
</html>
Creating the Access Denied Page
Finally, we create the Access Denied Page. Uses who have insufficient rights will be redirected to this page. The access denied page is located at src/main/resources/templates/error/access-denied.html
.
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{fragments/layout}">
<head>
<title>Access Denied</title>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
<h1>Access denied</h1>
<p><a href="/" th:href="@{/}">Back to home page</a></p>
</div>
</body>
</html>
Creating the Stylesheet
We use the following stylesheet which is located at src/main/resources/static/css/main.css
.
h1 {
color: #78ab46;
}
footer {
position: fixed;
height: 50px;
bottom: 0;
width: 100%;
background-color: #ccc
}
footer p {
padding: 15px;
}
Demo
Start the Spring Boot Web Application.
mvn spring-boot:run
Go to http://localhost:8080/
.
Go to http://localhost:8080/user
and is redirected to http://localhost:8080/login
.
Invalid username or password http://localhost:8080/login?error
.
Successful login http://localhost:8080/login
redirected to http://localhost:8080
.
Logged in with user manager
, access page http://localhost:8080/user
redirected to http://localhost:8080/access-denied
.
Sucessful logout, redirected to http://localhost:8080/login?logout
.
References
- Spring Security Documentation
- Thymeleaf Spring Security Documentation
- Thymeleaf Documentation
- Adding Static Resources Thymeleaf
Thansk for your post is very useful for me
helped a lot
Thank you so much for the useful post. Please add social networking(i.e facebook,linked-in,twitter) into a login.
Thanks in advance.
Greetings, I tried your example, and the login works perfectly, however, the html pages are not rendering with the CSS. What could the issue be?
The thymeleaf is configured in the
applicaiton.properties
orapplication.yml
file. Have you configured the correct path to your Thymeleaf files?I have downloaded the project and imported into Spring Tool Suite and run it just as it is but seem to be getting the “Whitelabel Error Page”. Any idea why this would be happening. It happens for both user and manager login when credentials have bee authorized. No errors on the console though.
Hi Aaron, are you sure that you access the right page? e.g.: http://localhost:8080/
Yeah I discovered it had something to do with Firefox because it worked on IE. So this is a non-issue.
Thanks for your post,
but is seems i cant access into secured pages ..
any ideas ?
thanks for this. Can you please upgrade this to spring boot 2.3.4 and spring security 5 .
needed to use the BCryptPasswordEncoder to encode passwords first.