Spring boot and Thymeleaf Integration

What is Thymeleaf
Integrating Spring Boot with Thymeleaf and Spring Security Taglibs

What is Thymeleaf

Thymeleaf is a modern server-side Java template engine for both web and standalone environments.

It helps to develop simple web application with HTML pages by enriching dynamic attributes within it.

HTML templates written in Thymeleaf still look and work like HTML.

Steps To integrate Spring boot with Thymeleaf

Add maven dependencies to your pom.xml

pom.xml
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</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>

Add WebConfig.java for Java based configuration of Thymeleaf

WebConfig.java
package com.tvajjala.toggles.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import java.util.List;

/**
 * @author ThirupathiReddy V
 */
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * Reference to logger
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(WebConfig.class);


    @Override
    public void addViewControllers(final ViewControllerRegistry registry) {
        registry.addRedirectViewController("/", "/ui/toggle-ui");
    }


    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry
                .addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCachePeriod(3600)
                .resourceChain(true)
                .addResolver(new PathResourceResolver());
    }


    @Override
    public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {
        final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.registerModule(new JavaTimeModule());
        converter.setObjectMapper(objectMapper);
        converters.add(converter);
    }


    //@formatter:off

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        // final TemplateResolver resolver = new TemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode("HTML");
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCacheable(false);
        resolver.setOrder(1);
        return resolver;
    }


    @Bean
    public SpringTemplateEngine templateEngine() {
        final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());

        // to enable JSTL tag library
        templateEngine.addDialect(new SpringSecurityDialect());

        templateEngine.addDialect(new Java8TimeDialect());

        /**
         * Layout Dialect gives people the possibility of using hierarchical approach, but from a Thymeleaf-only perspective and without the need to use
         * external libraries, like Apache Tiles. Thymeleaf Layout Dialect uses layout/decorator templates to style the content, as well as it can pass entire
         * fragment elements to included pages. Concepts of this library are similar to SiteMesh or JSF with Facelets.
         */

        return templateEngine;
    }


    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        final ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        return resolver;
    }


    @Override
    public void configureDefaultServletHandling(final DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }


}

Add Spring Security Configuration as shown below

SecurityConfig.java
package com.tvajjala.toggles.config;

import com.tvajjala.toggles.config.security.ApplicationSecurity;
import com.tvajjala.toggles.config.security.CustomUserDetailService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * Security Configuration
 *
 * @author ThirupathiReddy Vajjala
 */
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(ApplicationSecurity.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    ApplicationSecurity applicationSecurity;

    private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);


    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();

        http
                .authorizeRequests()
                .antMatchers("/ui/**")
                .hasAnyAuthority("ADMIN", "DEVELOPER", "QA")
                .and().formLogin()

                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.PUT, "/toggles/**")
                .hasAnyAuthority("ADMIN")
                .and().httpBasic()

                .and()
                .logout()
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .logoutSuccessUrl("/login")
                .logoutUrl("/logout");
    }


   /* @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new CustomAuthenticationEntryPoint("TOGGLE");
    }*/


    @Bean
    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder(); (1)
    }

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailService()).passwordEncoder(passwordEncoder()); (2)
    }


    @Bean
    public UserDetailsService customUserDetailService() {
        LOGGER.info("Users {} ", applicationSecurity.getUsers());
        return new CustomUserDetailService(applicationSecurity.getUsers());(3)
    }

}
  1. Password encryption bean

  2. Enabling Password encryption

  3. In-Memory Users from application.yaml

Create your thymeleaf templates under resources/template folder

index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> (1)

...

 <small>[<span sec:authentication="name"/>]</small>  (2)

....

</html>
  1. spring security taglib directories inside html.

  2. Logged in username .

Spring Security Integration with Spring boot

add In-Memory users section to your application.yml file

application.yaml
application:
  security:
    users:
      - username: admin
        password: '$2a$10$2pplRaD1nh3u96UEEhRshO3sCSsY17hBGn8Mqk4JDgYbuBgzRHZwi' #password
        role: ADMIN
      - username: developer
        password: '$2a$10$2pplRaD1nh3u96UEEhRshO3sCSsY17hBGn8Mqk4JDgYbuBgzRHZwi' #password
        role: DEVELOPER
      - username: quality
        password: '$2a$10$2pplRaD1nh3u96UEEhRshO3sCSsY17hBGn8Mqk4JDgYbuBgzRHZwi' #password
        role: QA

Create Java Representation of this class, which extends org.springframework.security.core.userdetails.UserDetails

ApplicationUser.java
package com.tvajjala.toggles.config.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

/**
 * @author ThirupathiReddy Vajjala
 */
public class ApplicationUser implements UserDetails {


    private String role;

    private String username;

    private String password;

    public String getRole() {
        return role;
    }

    public void setRole(final String role) {
        this.role = role;
    }

    public void setUsername(final String username) {
        this.username = username;
    }

    public void setPassword(final String password) {
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singletonList(new SimpleGrantedAuthority(role));
    }


    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }


    @Override
    public boolean isEnabled() {
        return true;
    }


    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public String toString() {
        return "ApplicationUser{" +
                "role='" + role + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

Bind this class with properties to automatically load into application context using ConfigurationProperties

ApplicationSecurity.java
package com.tvajjala.toggles.config.security;

import org.springframework.boot.context.properties.ConfigurationProperties;

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

/**
 * @author ThirupathiReddy Vajjala
 */
@ConfigurationProperties(prefix = "application.security")
public class ApplicationSecurity {


    private List<ApplicationUser> users = new ArrayList<>();


    public List<ApplicationUser> getUsers() {
        return users;
    }

    public void setUsers(final List<ApplicationUser> users) {
        this.users = users;
    }
}

Write your CustomUserDetailService which extends org.springframework.security.core.userdetails.UserDetailsService

CustomUserDetailService.java
package com.tvajjala.toggles.config.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


/**
 * @author ThirupathiReddy Vajjala
 */
public class CustomUserDetailService implements UserDetailsService {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomUserDetailService.class);

    private final Map<String, ApplicationUser> appUsers = new HashMap<>();

    public CustomUserDetailService(final List<ApplicationUser> users) {
        appUsers.putAll(users.stream().collect(Collectors.toMap(user -> user.getUsername(), user -> user)));
        LOGGER.warn("Users {} ", appUsers);
    }

    @Override
    public ApplicationUser loadUserByUsername(final String username) throws UsernameNotFoundException {

        if (!appUsers.containsKey(username)) {
            LOGGER.warn("User {} not found", username);
            throw new UsernameNotFoundException("User " + username + " Not found");
        }

        LOGGER.info("User with username {} found ", username);
        return appUsers.get(username);
    }
}

Refer Thymeleaf spring boot integration Source code for complete implementation

Comments

Popular posts from this blog

IBM Datapower GatewayScript

Spring boot Kafka Integration

Spring boot SOAP Web Service Performance