Spring Boot, Spring Security, Spring JPA, Thymeleaf, and Liquibase example
1. Introduction
In this blog, we will be creating a Spring MVC project where we would use Spring Boot, Spring Security, Spring JPA, Thymeleaf, Liquibase, and Mysql.
- Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can “just run”.
- Spring Security is a powerful and highly customizable authentication and access-control framework. Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.
- Spring JPA part of the larger Spring Data family, makes it easy to easily implement JPA-based repositories. This module deals with enhanced support for JPA-based data access layers. It makes it easier to build Spring-powered applications that use data access technologies.
- Thymeleaf is a modern server-side Java template engine for both web and standalone environments. Thymeleaf’s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
- Liquibase is an open-source database schema change management solution that enables you to manage revisions of your database changes easily.
2. Setup
In this section, we will quickly set up a project using Spring Initializr. Using this tool, we can quickly provide a list of Dependencies we need and download the bootstrapped application:
3. Maven Dependencies
Here is the pom.xml file, you should see the below dependencies added:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.fusion</groupId> <artifactId>thymeleaf-security</artifactId> <version>0.0.1-SNAPSHOT</version> <name>thymeleaf-security</name> <description>Integration thymeleaf, spring security, liquibase</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</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.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> </dependency> <!-- optional, it brings userful tags to display spring security stuff --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!-- optional, for doing layout of the pages --> <dependency> <groupId>nz.net.ultraq.thymeleaf</groupId> <artifactId>thymeleaf-layout-dialect</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Optional, for bootstrap --> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>5.1.3</version> </dependency> <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> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> |
Here’s the project directory:
4. Spring Security
We had used Spring Security for Authentication, Role-based access to URLs, redirect on the particular pages based on role, and configuring custom access denied page. We are using the database for the authentication part.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Autowired private CustomUserDetailsService customUserDetailService; @Autowired private MyAccessDeniedHandler accessDeniedHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http .getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder.userDetailsService(customUserDetailService).passwordEncoder(passwordEncoder()); http.csrf().disable() .authorizeHttpRequests().antMatchers("/", "/about", "/css/**", "/webjars/**", "/signup/**") .permitAll().anyRequest().authenticated() .and() .formLogin().loginPage("/login") .successHandler(myAuthenticationSuccessHandler()).failureUrl("/login?error").permitAll().and().logout() .permitAll() .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler); return http.build(); } @Bean public AuthenticationSuccessHandler myAuthenticationSuccessHandler() { return new ThymeleafUrlAuthenticationSuccessHandler(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } |
1 2 | @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) |
The above annotations are mandatory for enabling role-wise security on the method level.
We had also enabled a success handler for redirecting users based on roles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public class ThymeleafUrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler { protected Log logger = LogFactory.getLog(this.getClass()); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { handle(request, response, authentication); clearAuthenticationAttributes(request); } private void clearAuthenticationAttributes(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return; } session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } private void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String targetUrl = determineTargetUrl(authentication); if (response.isCommitted()) { logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); return; } redirectStrategy.sendRedirect(request, response, targetUrl); } private String determineTargetUrl(Authentication authentication) { Map<String, String> roleTargetUrlMap = new HashMap<>(); roleTargetUrlMap.put(RoleName.ROLE_ADMIN.name(), "/admin"); roleTargetUrlMap.put(RoleName.ROLE_USER.name(), "/user"); final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (final GrantedAuthority grantedAuthority : authorities) { String authorityName = grantedAuthority.getAuthority(); if (roleTargetUrlMap.containsKey(authorityName)) { return roleTargetUrlMap.get(authorityName); } } throw new IllegalStateException(); } |
We had created two users for demo purposes one is for ROLE_ADMIN and another ROLE_USER
For user “ROLE_ADMIN”:
- Able to access /admin
- Unable to access /user page, redirect to 403 access denied page.
For user “ROLE_USER“:
- Able to access /user
- Unable to access /admin page, redirect to 403 access denied page.
Below is the code of the Access denied handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { protected Log logger = LogFactory.getLog(this.getClass()); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { logger.info( "User '" + auth.getName() + "' attempted to access the protected URL: " + request.getRequestURI()); } response.sendRedirect(request.getContextPath() + "/403"); } } |
5. Liquibase
We had integrated Liquibase for managing database revision changes easily throughout the different environments.
Code snippet for db.changelog-master.yaml
1 2 3 4 5 | databaseChangeLog: - include: file: db/changelog/changes/v1.sql - include: file: db/changelog/changes/v2.sql |
6. Thymeleaf
Earlier for page fragments, we were using Apache Tiles, over here we would be using Themeleaf fragments for layout purposes
Below is the config file for Thymeleaf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public class ThymeleafConfig { @Bean @Description("Thymeleaf Template Resolver") public ClassLoaderTemplateResolver htmlTemplateResolver() { ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); resolver.setPrefix("/templates/"); resolver.setSuffix(".html"); resolver.setCacheable(false); resolver.setTemplateMode(TemplateMode.HTML); return resolver; } @Bean @Description("Thymeleaf Template Engine") public SpringTemplateEngine templateEngine() { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(htmlTemplateResolver()); templateEngine.addDialect(new LayoutDialect()); return templateEngine; } @Bean @Description("Thymeleaf View Resolver") public ThymeleafViewResolver viewResolver() { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine()); viewResolver.setOrder(1); return viewResolver; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> <head> <title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Fusion Blogs</title> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script> <div th:replace="fragments/header :: header-css"/> </head> <body> <div th:replace="fragments/header :: header"/> <!-- Custom Content Goes here --> <div class="container"> <section layout:fragment="custom-content"> <p>Your page content goes here</p> </section> </div> <!-- ENDS Custom Content Goes here --> <div th:replace="fragments/footer :: footer"/> </body> </html> |
7. Demo
Start the Spring Boot web application by running the following command:
1 | mvn spring-boot:run |
Access http://localhost:8080/
Access http://localhost:8080/admin , redirect to http://localhost:8080/login
Access http://localhost:8080/signup page for creating users:
Login with the admin user, and that will redirect to the below page http://localhost:8080/admin
If the admin tries to access http://localhost:8080/user, URL he will be redirect to access denied page http://localhost:8080/403
8. Conclusion
In this quick tutorial, we saw how we can integrate Liquibase, Spring Security, and Thymeleaf in the Spring MVC application. The source code for this application is available over on GitHub.