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.
Download
From:一号门
COMMENTS