{{announcement.body}}
{{announcement.title}}

Dynamic Multi-Tenancy Using Spring Security and JWTs

DZone 's Guide to

Dynamic Multi-Tenancy Using Spring Security and JWTs

In this article, we discuss how to enable multi-tenancy in a web application with Spring Security and JWTs.

· Java Zone ·
Free Resource

Purpose

I wanted a solution where multi-tenancy is achieved by having a database per-tenant and all user information (username, password, client Id, etc.) for authentication and authorization stored in a user table in the respective tenant databases. This means that not only did I need a multi-tenant application, but also a secure application like any other web application secured by Spring Security.

I know how to use Spring Security to secure a web application and how to use Hibernate to connect to a database. The requirement further dictates that all users belonging to a tenant need to be stored in the tenant database and not a separate or central database. This would allow for complete data isolation for each tenant.

Goal

  • Archive Application SaaS Model client wise different database.
  • Focus Spring Security and JWT
  • You can connect multiple schemas with a single database, like MySQL — testdb, testdb2.
  • You can connect multiple databases, like MySQL, PostgreSQL, or Oracle.

What Is Multi-Tenancy?

Multi-tenancy is an architecture in which a single instance of a software application serves multiple customers. Each client is called a tenant. Tenants may be given the ability to customize some parts of the application.

A multi-tenant application is where a tenant (i.e. users in a company) feels that the application has been created and deployed for them. In reality, there are many such tenants, and they too are using the same application but get a feeling that it's built just for them.

Dynamic Multi-Tenant High-Level Diagram: Dynamic Multi-Tenant High-Level DiagramHere,

  • Client requests to login to the system.
  • The system checks with the master database using client Id.
  • If it's successful, set the current database to context based on the driver class name.
  • If this fails, the user gets the message, "unauthorized".
  • After successful authentication, the user gets a JWT for the next execution.

The whole process executes in the following workflow:

Authentication workflow

Now, Let's start developing a multi-tenancy application step-by-step with Spring Security and JWT.

1. Technology and Project Structure:

  • Java 11.
  • Spring Boot.
  • Spring Security.
  • Spring AOP.
  • Spring Data JPA.
  • Hibernate.
  • JWT.
  • MySQL, PostgreSQL.
  • IntliJ.

You can get started quickly by using https://start.spring.io/.

Project Structure:

Initial project structure

2. Now, Create a Master Database and a tenant database.

Master Database:

In the master database, we only have one table (tbl_tenant_master), where all tenant information is storeed in the table.

MySQL
 




xxxxxxxxxx
1
11


 
1
create database master_db;
2
CREATE TABLE  `master_db`.`tbl_tenant_master` (
3
  `tenant_client_id` int(10) unsigned NOT NULL,
4
  `db_name` varchar(50) NOT NULL,
5
  `url` varchar(100) NOT NULL,
6
  `user_name` varchar(50) NOT NULL,
7
  `password` varchar(100) NOT NULL,
8
  `driver_class` varchar(100) NOT NULL,
9
  `status` varchar(10) NOT NULL,
10
  PRIMARY KEY (`tenant_client_id`) USING BTREE
11
) ENGINE=InnoDB;



Tenant Database (1) in MySQL:

Create a table for client login authentication(tbl_user).

Create another table (tbl_product) to retrieve data using a JWT (for Authorization checks).

MySQL
 




xxxxxxxxxx
1
20


 
1
create database testdb;
2
DROP TABLE IF EXISTS `testdb`.`tbl_user`;
3
CREATE TABLE  `testdb`.`tbl_user` (
4
  `user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
5
  `full_name` varchar(100) NOT NULL,
6
  `gender` varchar(10) NOT NULL,
7
  `user_name` varchar(50) NOT NULL,
8
  `password` varchar(100) NOT NULL,
9
  `status` varchar(10) NOT NULL,
10
  PRIMARY KEY (`user_id`)
11
) ENGINE=InnoDB;
12
 
          
13
DROP TABLE IF EXISTS `testdb`.`tbl_product`;
14
CREATE TABLE  `testdb`.`tbl_product` (
15
  `product_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
16
  `product_name` varchar(50) NOT NULL,
17
  `quantity` int(10) unsigned NOT NULL DEFAULT '0',
18
  `size` varchar(3) NOT NULL,
19
  PRIMARY KEY (`product_id`)
20
) ENGINE=InnoDB;



Tenant Database (2) in PostgreSQL:

Create a table for client login authentication (tbl_user).

Create another table (tbl_product) to retrieve data using a JWT (for authorization checks).

MySQL
 




xxxxxxxxxx
1
20


 
1
create database testdb_pgs;
2
CREATE TABLE public.tbl_user
3
(
4
    user_id integer NOT NULL,
5
    full_name character varying(100) COLLATE pg_catalog."default" NOT NULL,
6
    gender character varying(10) COLLATE pg_catalog."default" NOT NULL,
7
    user_name character varying(50) COLLATE pg_catalog."default" NOT NULL,
8
    password character varying(100) COLLATE pg_catalog."default" NOT NULL,
9
    status character varying(10) COLLATE pg_catalog."default" NOT NULL,
10
    CONSTRAINT tbl_user_pkey PRIMARY KEY (user_id)
11
)
12
 
          
13
CREATE TABLE public.tbl_product
14
(
15
    product_id integer NOT NULL,
16
    product_name character varying(50) COLLATE pg_catalog."default" NOT NULL,
17
    quantity integer NOT NULL DEFAULT 0,
18
    size character varying(3) COLLATE pg_catalog."default" NOT NULL,
19
    CONSTRAINT tbl_product_pkey PRIMARY KEY (product_id)
20
)



Database creation and table creation are done!

3. Check the pom.xml file.

XML
 




xxxxxxxxxx
1
99


 
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4
    <modelVersion>4.0.0</modelVersion>
5
    <parent>
6
        <groupId>org.springframework.boot</groupId>
7
        <artifactId>spring-boot-starter-parent</artifactId>
8
        <version>2.2.6.RELEASE</version>
9
        <relativePath></relativePath> <!-- lookup parent from repository -->
10
    </parent>
11
    <groupId>com.amran.dynamic.multitenant</groupId>
12
    <artifactId>dynamicmultitenant</artifactId>
13
    <version>0.0.1-SNAPSHOT</version>
14
    <packaging>war</packaging>
15
    <name>dynamicmultitenant</name>
16
    <description>Dynamic Multi Tenant project for Spring Boot</description>
17
 
          
18
    <properties>
19
        <java.version>11</java.version>
20
    </properties>
21
 
          
22
    <dependencies>
23
        <dependency>
24
            <groupId>org.springframework.boot</groupId>
25
            <artifactId>spring-boot-starter-data-jpa</artifactId>
26
        </dependency>
27
        <dependency>
28
            <groupId>org.springframework.boot</groupId>
29
            <artifactId>spring-boot-starter-security</artifactId>
30
        </dependency>
31
        <dependency>
32
            <groupId>io.jsonwebtoken</groupId>
33
            <artifactId>jjwt</artifactId>
34
            <version>0.9.1</version>
35
        </dependency>
36
        <dependency>
37
            <groupId>org.springframework.boot</groupId>
38
            <artifactId>spring-boot-starter-web</artifactId>
39
        </dependency>
40
 
          
41
        <dependency>
42
            <groupId>org.springframework.boot</groupId>
43
            <artifactId>spring-boot-devtools</artifactId>
44
            <scope>runtime</scope>
45
            <optional>true</optional>
46
        </dependency>
47
        <dependency>
48
            <groupId>mysql</groupId>
49
            <artifactId>mysql-connector-java</artifactId>
50
            <scope>runtime</scope>
51
        </dependency>
52
        <dependency>
53
            <groupId>org.postgresql</groupId>
54
            <artifactId>postgresql</artifactId>
55
            <scope>runtime</scope>
56
        </dependency>
57
        <dependency>
58
            <groupId>joda-time</groupId>
59
            <artifactId>joda-time</artifactId>
60
            <version>2.10</version>
61
        </dependency>
62
        <dependency>
63
            <groupId>org.apache.commons</groupId>
64
            <artifactId>commons-lang3</artifactId>
65
        </dependency>
66
        <dependency>
67
            <groupId>org.springframework.boot</groupId>
68
            <artifactId>spring-boot-starter-tomcat</artifactId>
69
            <scope>provided</scope>
70
        </dependency>
71
        <dependency>
72
            <groupId>org.springframework.boot</groupId>
73
            <artifactId>spring-boot-starter-test</artifactId>
74
            <scope>test</scope>
75
            <exclusions>
76
                <exclusion>
77
                    <groupId>org.junit.vintage</groupId>
78
                    <artifactId>junit-vintage-engine</artifactId>
79
                </exclusion>
80
            </exclusions>
81
        </dependency>
82
        <dependency>
83
            <groupId>org.springframework.security</groupId>
84
            <artifactId>spring-security-test</artifactId>
85
            <scope>test</scope>
86
        </dependency>
87
    </dependencies>
88
 
          
89
    <build>
90
        <plugins>
91
            <plugin>
92
                <groupId>org.springframework.boot</groupId>
93
                <artifactId>spring-boot-maven-plugin</artifactId>
94
            </plugin>
95
        </plugins>
96
    </build>
97
 
          
98
</project>
99
 
          



4. Configure the master database or common database into our Spring Boot application ( application.yml).

XML
 




xxxxxxxxxx
1
13


 
1
multitenancy:
2
  mtapp:
3
    master:
4
      datasource:
5
        url: jdbc:mysql://192.168.0.115:3306/master_db?useSSL=false
6
        username: root
7
        password: test
8
        driverClassName: com.mysql.cj.jdbc.Driver
9
        connectionTimeout: 20000
10
        maxPoolSize: 250
11
        idleTimeout: 300000
12
        minIdle: 5
13
        poolName: masterdb-connection-pool



5. Spring Security and Enabling JWT:

WebSecurityConfigurerAdapter allows users to configure web-based security for a certain selection (in this case all) requests. It allows configuring things that impact our application's security. WebSecurityConfigurerAdapter is a convenience class that allows customization to both WebSecurity  and  HttpSecurity.

WebSecurityConfig.java 

Java
 




xxxxxxxxxx
1
96


 
1
package com.amran.dynamic.multitenant.security;
2
 
          
3
import org.springframework.beans.factory.annotation.Autowired;
4
import org.springframework.boot.web.servlet.FilterRegistrationBean;
5
import org.springframework.context.annotation.Bean;
6
import org.springframework.context.annotation.Configuration;
7
import org.springframework.security.authentication.AuthenticationManager;
8
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
9
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
10
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
13
import org.springframework.security.config.http.SessionCreationPolicy;
14
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
15
import org.springframework.security.crypto.password.PasswordEncoder;
16
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
17
import org.springframework.web.cors.CorsConfiguration;
18
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
19
import org.springframework.web.filter.CorsFilter;
20
 
          
21
/**
22
 * @author Md. Amran Hossain
23
 */
24
@Configuration
25
@EnableWebSecurity
26
@EnableGlobalMethodSecurity(prePostEnabled = true)
27
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
28
 
          
29
    @Autowired
30
    private JwtUserDetailsService jwtUserDetailsService;
31
    @Autowired
32
    private JwtAuthenticationEntryPoint unauthorizedHandler;
33
 
          
34
    @Override
35
    @Bean
36
    public AuthenticationManager authenticationManagerBean() throws Exception {
37
        return super.authenticationManagerBean();
38
    }
39
 
          
40
    @Autowired
41
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
42
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
43
    }
44
 
          
45
    @Bean
46
    public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
47
        return new JwtAuthenticationFilter();
48
    }
49
 
          
50
    @Override
51
    protected void configure(HttpSecurity http) throws Exception {
52
        http.cors().and().csrf().disable().
53
                authorizeRequests()
54
                .antMatchers("/api/auth/**").permitAll()
55
                .antMatchers("/api/product/**").authenticated()
56
                .and()
57
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
58
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
59
        http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
60
    }
61
 
          
62
//    @Bean
63
//    public PasswordEncoder passwordEncoder() {
64
//        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
65
//        return encoder;
66
//    }
67
 
          
68
    @Bean
69
    public PasswordEncoder passwordEncoder() {
70
        return new BCryptPasswordEncoder();
71
    }
72
 
          
73
    @Bean
74
    public FilterRegistrationBean platformCorsFilter() {
75
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
76
 
          
77
        CorsConfiguration configAutenticacao = new CorsConfiguration();
78
        configAutenticacao.setAllowCredentials(true);
79
        configAutenticacao.addAllowedOrigin("*");
80
        configAutenticacao.addAllowedHeader("Authorization");
81
        configAutenticacao.addAllowedHeader("Content-Type");
82
        configAutenticacao.addAllowedHeader("Accept");
83
        configAutenticacao.addAllowedMethod("POST");
84
        configAutenticacao.addAllowedMethod("GET");
85
        configAutenticacao.addAllowedMethod("DELETE");
86
        configAutenticacao.addAllowedMethod("PUT");
87
        configAutenticacao.addAllowedMethod("OPTIONS");
88
        configAutenticacao.setMaxAge(3600L);
89
        source.registerCorsConfiguration("/**", configAutenticacao);
90
 
          
91
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
92
        bean.setOrder(-110);
93
        return bean;
94
    }
95
}
96
 
          



The class, OncePerRequestFilter, is a filter base class that aims to guarantee a single execution per request dispatch on any servlet container. As of Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatch that occurs in separate threads.

JwtAuthenticationFilter.java

Java
 




xxxxxxxxxx
1
79


 
1
package com.amran.dynamic.multitenant.security;
2
3
import com.amran.dynamic.multitenant.constant.JWTConstants;
4
import com.amran.dynamic.multitenant.mastertenant.config.DBContextHolder;
5
import com.amran.dynamic.multitenant.mastertenant.entity.MasterTenant;
6
import com.amran.dynamic.multitenant.mastertenant.service.MasterTenantService;
7
import com.amran.dynamic.multitenant.util.JwtTokenUtil;
8
import io.jsonwebtoken.ExpiredJwtException;
9
import io.jsonwebtoken.SignatureException;
10
import org.springframework.beans.factory.annotation.Autowired;
11
import org.springframework.security.authentication.BadCredentialsException;
12
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
13
import org.springframework.security.core.authority.SimpleGrantedAuthority;
14
import org.springframework.security.core.context.SecurityContextHolder;
15
import org.springframework.security.core.userdetails.UserDetails;
16
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
17
import org.springframework.stereotype.Component;
18
import org.springframework.web.filter.OncePerRequestFilter;
19
20
import javax.servlet.FilterChain;
21
import javax.servlet.ServletException;
22
import javax.servlet.http.HttpServletRequest;
23
import javax.servlet.http.HttpServletResponse;
24
import java.io.IOException;
25
import java.util.Arrays;
26
27
/**
28
 * @author Md. Amran Hossain
29
 */
30
@Component
31
public class JwtAuthenticationFilter extends OncePerRequestFilter {
32
33
    @Autowired
34
    private JwtUserDetailsService jwtUserDetailsService;
35
    @Autowired
36
    private JwtTokenUtil jwtTokenUtil;
37
    @Autowired
38
    MasterTenantService masterTenantService;
39
40
    @Override
41
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
42
        String header = httpServletRequest.getHeader(JWTConstants.HEADER_STRING);
43
        String username = null;
44
        String audience = null; //tenantOrClientId
45
        String authToken = null;
46
        if (header != null && header.startsWith(JWTConstants.TOKEN_PREFIX)) {
47
            authToken = header.replace(JWTConstants.TOKEN_PREFIX,"");
48
            try {
49
                username = jwtTokenUtil.getUsernameFromToken(authToken);
50
                audience = jwtTokenUtil.getAudienceFromToken(authToken);
51
                MasterTenant masterTenant = masterTenantService.findByClientId(Integer.valueOf(audience));
52
                if(null == masterTenant){
53
                    logger.error("An error during getting tenant name");
54
                    throw new BadCredentialsException("Invalid tenant and user.");
55
                }
56
                DBContextHolder.setCurrentDb(masterTenant.getDbName());
57
            } catch (IllegalArgumentException ex) {
58
                logger.error("An error during getting username from token", ex);
59
            } catch (ExpiredJwtException ex) {
60
                logger.warn("The token is expired and not valid anymore", ex);
61
            } catch(SignatureException ex){
62
                logger.error("Authentication Failed. Username or Password not valid.",ex);
63
            }
64
        } else {
65
            logger.warn("Couldn't find bearer string, will ignore the header");
66
        }
67
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
68
            UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);
69
            if (jwtTokenUtil.validateToken(authToken, userDetails)) {
70
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
71
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
72
                logger.info("authenticated user " + username + ", setting security context");
73
                SecurityContextHolder.getContext().setAuthentication(authentication);
74
            }
75
        }
76
        filterChain.doFilter(httpServletRequest, httpServletResponse);
77
    }
78
}
79



ExceptionTranslationFilter is used to catch any Spring Security exceptions so that either an HTTP error response can be returned, or an appropriate AuthenticationEntryPoint can be launched. The AuthenticationEntryPoint will be called if the user requests a secure HTTP resource, but they are not authenticated.

Java
 




xxxxxxxxxx
1
26


 
1
package com.amran.dynamic.multitenant.security;
2
3
import org.springframework.security.core.AuthenticationException;
4
import org.springframework.security.web.AuthenticationEntryPoint;
5
import org.springframework.stereotype.Component;
6
7
import javax.servlet.ServletException;
8
import javax.servlet.http.HttpServletRequest;
9
import javax.servlet.http.HttpServletResponse;
10
import java.io.IOException;
11
import java.io.Serializable;
12
13
/**
14
 * @author Md. Amran Hossain
15
 */
16
@Component
17
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
18
19
    private static final long serialVersionUID = -7858869558953243875L;
20
21
    @Override
22
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
23
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
24
    }
25
}
26



6. Configure Master Database :

Master Data Source Configuration:

ThreadLocals is used to maintain some context related to the current thread. For example, when the current transaction is stored in a ThreadLocal, you don't need to pass it as a parameter through every method call in case someone down the stack needs access to it. 

Web applications might store information about the current request and session in a ThreadLocal, so that the application has easy access to them. ThreadLocals can be used when implementing custom scopes for injected objects.

ThreadLocals are one sort of global variables (although slightly less evil because they are restricted to one thread), so you should be careful when using them to avoid unwanted side-effects and memory leaks. 

DBContextHolder.java 

Java
 




xxxxxxxxxx
1
23


 
1
package com.amran.dynamic.multitenant.mastertenant.config;
2
3
/**
4
 * @author Md. Amran Hossain
5
 * The context holder implementation is a container that stores the current context as a ThreadLocal reference.
6
 */
7
public class DBContextHolder {
8
9
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
10
11
    public static void setCurrentDb(String dbType) {
12
        contextHolder.set(dbType);
13
    }
14
15
    public static String getCurrentDb() {
16
        return contextHolder.get();
17
    }
18
19
    public static void clear() {
20
        contextHolder.remove();
21
    }
22
}
23



Create another class, MasterDatabaseConfigProperties.java. It holds a connection-related parameter, as defined in the application.yml file.

Java
 




xxxxxxxxxx
1
130


 
1
package com.amran.dynamic.multitenant.mastertenant.config;
2
3
import org.springframework.boot.context.properties.ConfigurationProperties;
4
import org.springframework.context.annotation.Configuration;
5
6
/**
7
 * @author Md. Amran Hossain
8
 */
9
@Configuration
10
@ConfigurationProperties("multitenancy.mtapp.master.datasource")
11
public class MasterDatabaseConfigProperties {
12
13
    private String url;
14
    private String username;
15
    private String password;
16
    private String driverClassName;
17
    private long connectionTimeout;
18
    private int maxPoolSize;
19
    private long idleTimeout;
20
    private int minIdle;
21
    private String poolName;
22
23
    //Initialization of HikariCP.
24
    @Override
25
    public String toString() {
26
        StringBuilder builder = new StringBuilder();
27
        builder.append("MasterDatabaseConfigProperties [url=");
28
        builder.append(url);
29
        builder.append(", username=");
30
        builder.append(username);
31
        builder.append(", password=");
32
        builder.append(password);
33
        builder.append(", driverClassName=");
34
        builder.append(driverClassName);
35
        builder.append(", connectionTimeout=");
36
        builder.append(connectionTimeout);
37
        builder.append(", maxPoolSize=");
38
        builder.append(maxPoolSize);
39
        builder.append(", idleTimeout=");
40
        builder.append(idleTimeout);
41
        builder.append(", minIdle=");
42
        builder.append(minIdle);
43
        builder.append(", poolName=");
44
        builder.append(poolName);
45
        builder.append("]");
46
        return builder.toString();
47
    }
48
49
    public String getUrl() {
50
        return url;
51
    }
52
53
    public MasterDatabaseConfigProperties setUrl(String url) {
54
        this.url = url;
55
        return this;
56
    }
57
58
    public String getUsername() {
59
        return username;
60
    }
61
62
    public MasterDatabaseConfigProperties setUsername(String username) {
63
        this.username = username;
64
        return this;
65
    }
66
67
    public String getPassword() {
68
        return password;
69
    }
70
71
    public MasterDatabaseConfigProperties setPassword(String password) {
72
        this.password = password;
73
        return this;
74
    }
75
76
    public String getDriverClassName() {
77
        return driverClassName;
78
    }
79
80
    public MasterDatabaseConfigProperties setDriverClassName(String driverClassName) {
81
        this.driverClassName = driverClassName;
82
        return this;
83
    }
84
85
    public long getConnectionTimeout() {
86
        return connectionTimeout;
87
    }
88
89
    public MasterDatabaseConfigProperties setConnectionTimeout(long connectionTimeout) {
90
        this.connectionTimeout = connectionTimeout;
91
        return this;
92
    }
93
94
    public int getMaxPoolSize() {
95
        return maxPoolSize;
96
    }
97
98
    public MasterDatabaseConfigProperties setMaxPoolSize(int maxPoolSize) {
99
        this.maxPoolSize = maxPoolSize;
100
        return this;
101
    }
102
103
    public long getIdleTimeout() {
104
        return idleTimeout;
105
    }
106
107
    public MasterDatabaseConfigProperties setIdleTimeout(long idleTimeout) {
108
        this.idleTimeout = idleTimeout;
109
        return this;
110
    }
111
112
    public int getMinIdle() {
113
        return minIdle;
114
    }
115
116
    public MasterDatabaseConfigProperties setMinIdle(int minIdle) {
117
        this.minIdle = minIdle;
118
        return this;
119
    }
120
121
    public String getPoolName() {
122
        return poolName;
123
    }
124
125
    public MasterDatabaseConfigProperties setPoolName(String poolName) {
126
        this.poolName = poolName;
127
        return this;
128
    }
129
}
130



@EnableTransactionManagement  and  <tx:annotation-driven/>  are responsible for registering the necessary Spring components that power annotation-driven transaction management, such as the TransactionInterceptor and the proxy, or an AspectJ-based advice that weaves the interceptor into the call stack when JdbcFooRepository's  @Transactional  methods are invoked.

MasterDatabaseConfig.java

Java
 




xxxxxxxxxx
1
100


 
1
package com.amran.dynamic.multitenant.mastertenant.config;
2
3
import com.amran.dynamic.multitenant.mastertenant.entity.MasterTenant;
4
import com.amran.dynamic.multitenant.mastertenant.repository.MasterTenantRepository;
5
import com.zaxxer.hikari.HikariDataSource;
6
import org.slf4j.Logger;
7
import org.slf4j.LoggerFactory;
8
import org.springframework.beans.factory.annotation.Autowired;
9
import org.springframework.beans.factory.annotation.Qualifier;
10
import org.springframework.context.annotation.Bean;
11
import org.springframework.context.annotation.Configuration;
12
import org.springframework.context.annotation.Primary;
13
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
14
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
15
import org.springframework.orm.jpa.JpaTransactionManager;
16
import org.springframework.orm.jpa.JpaVendorAdapter;
17
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
18
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
19
import org.springframework.transaction.annotation.EnableTransactionManagement;
20
21
import javax.persistence.EntityManagerFactory;
22
import javax.sql.DataSource;
23
import java.util.Properties;
24
25
/**
26
 * @author Md. Amran Hossain
27
 */
28
@Configuration
29
@EnableTransactionManagement
30
@EnableJpaRepositories(basePackages = {"com.amran.dynamic.multitenant.mastertenant.entity", "com.amran.dynamic.multitenant.mastertenant.repository"},
31
        entityManagerFactoryRef = "masterEntityManagerFactory",
32
        transactionManagerRef = "masterTransactionManager")
33
public class MasterDatabaseConfig {
34
35
    private static final Logger LOG = LoggerFactory.getLogger(MasterDatabaseConfig.class);
36
37
    @Autowired
38
    private MasterDatabaseConfigProperties masterDbProperties;
39
40
    //Create Master Data Source using master properties and also configure HikariCP
41
    @Bean(name = "masterDataSource")
42
    public DataSource masterDataSource() {
43
        HikariDataSource hikariDataSource = new HikariDataSource();
44
        hikariDataSource.setUsername(masterDbProperties.getUsername());
45
        hikariDataSource.setPassword(masterDbProperties.getPassword());
46
        hikariDataSource.setJdbcUrl(masterDbProperties.getUrl());
47
        hikariDataSource.setDriverClassName(masterDbProperties.getDriverClassName());
48
        hikariDataSource.setPoolName(masterDbProperties.getPoolName());
49
        // HikariCP settings
50
        hikariDataSource.setMaximumPoolSize(masterDbProperties.getMaxPoolSize());
51
        hikariDataSource.setMinimumIdle(masterDbProperties.getMinIdle());
52
        hikariDataSource.setConnectionTimeout(masterDbProperties.getConnectionTimeout());
53
        hikariDataSource.setIdleTimeout(masterDbProperties.getIdleTimeout());
54
        LOG.info("Setup of masterDataSource succeeded.");
55
        return hikariDataSource;
56
    }
57
58
    @Primary
59
    @Bean(name = "masterEntityManagerFactory")
60
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory() {
61
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
62
        // Set the master data source
63
        em.setDataSource(masterDataSource());
64
        // The master tenant entity and repository need to be scanned
65
        em.setPackagesToScan(new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()});
66
        // Setting a name for the persistence unit as Spring sets it as
67
        // 'default' if not defined
68
        em.setPersistenceUnitName("masterdb-persistence-unit");
69
        // Setting Hibernate as the JPA provider
70
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
71
        em.setJpaVendorAdapter(vendorAdapter);
72
        // Set the hibernate properties
73
        em.setJpaProperties(hibernateProperties());
74
        LOG.info("Setup of masterEntityManagerFactory succeeded.");
75
        return em;
76
    }
77
78
    @Bean(name = "masterTransactionManager")
79
    public JpaTransactionManager masterTransactionManager(@Qualifier("masterEntityManagerFactory") EntityManagerFactory emf) {
80
        JpaTransactionManager transactionManager = new JpaTransactionManager();
81
        transactionManager.setEntityManagerFactory(emf);
82
        return transactionManager;
83
    }
84
85
    @Bean
86
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
87
        return new PersistenceExceptionTranslationPostProcessor();
88
    }
89
90
    //Hibernate configuration properties
91
    private Properties hibernateProperties() {
92
        Properties properties = new Properties();
93
        properties.put(org.hibernate.cfg.Environment.DIALECT, "org.hibernate.dialect.MySQL5Dialect");
94
        properties.put(org.hibernate.cfg.Environment.SHOW_SQL, true);
95
        properties.put(org.hibernate.cfg.Environment.FORMAT_SQL, true);
96
        properties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO, "none");
97
        return properties;
98
    }
99
}
100



7. Configure Tenant Database.

In this section, we'll work to understand multitenancy in Hibernate. There are three approaches to multitenancy in Hibernate:

  • Separate Schema — one schema per tenant in the same physical database instance.
  • Separate Database — one separate physical database instance per tenant.
  • Partitioned (Discriminator) Data — the data for each tenant is partitioned by a discriminator value.

As usual, Hibernate abstracts the complexity around the implementation of each approach.
All we need is to provide an implementation of these two interfaces:

  •  MultiTenantConnectionProvider  – provides connections per tenant.
  •  CurrentTenantIdentifierResolver  – resolves the tenant identifier to use.

MultiTenantConnectionProvider

If Hibernate cannot resolve the tenant identifier to use, it will use the method, getAnyConnection, to get a connection. Otherwise, it will use the method, getConnection.

Hibernate provides two implementations of this interface depending on how we define the database connections:

  • Using Datasource interface from Java – we would use the DataSourceBasedMultiTenantConnectionProviderImpl implementation
  • Using the ConnectionProvider interface from Hibernate – we would use the AbstractMultiTenantConnectionProvider implementation

CurrentTenantIdentifierResolver

Hibernate calls the method, resolveCurrentTenantIdentifier, to get the tenant identifier. If we want Hibernate to validate that all the existing sessions belong to the same tenant identifier, the method validateExistingCurrentSessions should return true. 

Schema Approach
In this strategy, we'll use different schemas or users in the same physical database instance. This approach should be used when we need the best performance for our application and can sacrifice special database features such as backup per tenant. 

Database Approach
The Database multi-tenancy approach uses different physical database instances per tenant. Since each tenant is fully isolated, we should choose this strategy when we need special database features, like backup per tenant more than we need the best performance.

CurrentTenantIdentifierResolverImpl.java 

Java
 




xxxxxxxxxx
1
25


 
1
package com.amran.dynamic.multitenant.tenant.config;
2
3
import com.amran.dynamic.multitenant.mastertenant.config.DBContextHolder;
4
import org.apache.commons.lang3.StringUtils;
5
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
6
7
/**
8
 * @author Md. Amran Hossain
9
 */
10
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
11
12
    private static final String DEFAULT_TENANT_ID = "client_tenant_1";
13
14
    @Override
15
    public String resolveCurrentTenantIdentifier() {
16
        String tenant = DBContextHolder.getCurrentDb();
17
        return StringUtils.isNotBlank(tenant) ? tenant : DEFAULT_TENANT_ID;
18
    }
19
20
    @Override
21
    public boolean validateExistingCurrentSessions() {
22
        return true;
23
    }
24
}
25



DataSourceBasedMultiTenantConnectionProviderImpl.java 

Java
 




xxxxxxxxxx
1
79


 
1
package com.amran.dynamic.multitenant.tenant.config;
2
3
import com.amran.dynamic.multitenant.mastertenant.config.DBContextHolder;
4
import com.amran.dynamic.multitenant.mastertenant.entity.MasterTenant;
5
import com.amran.dynamic.multitenant.mastertenant.repository.MasterTenantRepository;
6
import com.amran.dynamic.multitenant.util.DataSourceUtil;
7
import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10
import org.springframework.beans.factory.annotation.Autowired;
11
import org.springframework.context.ApplicationContext;
12
import org.springframework.context.annotation.Configuration;
13
import org.springframework.security.core.userdetails.UsernameNotFoundException;
14
15
import javax.sql.DataSource;
16
import java.util.List;
17
import java.util.Map;
18
import java.util.TreeMap;
19
20
/**
21
 * @author Md. Amran Hossain
22
 */
23
@Configuration
24
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
25
26
    private static final Logger LOG = LoggerFactory.getLogger(DataSourceBasedMultiTenantConnectionProviderImpl.class);
27
28
    private static final long serialVersionUID = 1L;
29
30
    private Map<String, DataSource> dataSourcesMtApp = new TreeMap<>();
31
32
    @Autowired
33
    private MasterTenantRepository masterTenantRepository;
34
35
    @Autowired
36
    ApplicationContext applicationContext;
37
38
    @Override
39
    protected DataSource selectAnyDataSource() {
40
        // This method is called more than once. So check if the data source map
41
        // is empty. If it is then rescan master_tenant table for all tenant
42
        if (dataSourcesMtApp.isEmpty()) {
43
            List<MasterTenant> masterTenants = masterTenantRepository.findAll();
44
            LOG.info("selectAnyDataSource() method call...Total tenants:" + masterTenants.size());
45
            for (MasterTenant masterTenant : masterTenants) {
46
                dataSourcesMtApp.put(masterTenant.getDbName(), DataSourceUtil.createAndConfigureDataSource(masterTenant));
47
            }
48
        }
49
        return this.dataSourcesMtApp.values().iterator().next();
50
    }
51
52
    @Override
53
    protected DataSource selectDataSource(String tenantIdentifier) {
54
        // If the requested tenant id is not present check for it in the master
55
        // database 'master_tenant' table
56
        tenantIdentifier = initializeTenantIfLost(tenantIdentifier);
57
        if (!this.dataSourcesMtApp.containsKey(tenantIdentifier)) {
58
            List<MasterTenant> masterTenants = masterTenantRepository.findAll();
59
            LOG.info("selectDataSource() method call...Tenant:" + tenantIdentifier + " Total tenants:" + masterTenants.size());
60
            for (MasterTenant masterTenant : masterTenants) {
61
                dataSourcesMtApp.put(masterTenant.getDbName(), DataSourceUtil.createAndConfigureDataSource(masterTenant));
62
            }
63
        }
64
        //check again if tenant exist in map after rescan master_db, if not, throw UsernameNotFoundException
65
        if (!this.dataSourcesMtApp.containsKey(tenantIdentifier)) {
66
            LOG.warn("Trying to get tenant:" + tenantIdentifier + " which was not found in master db after rescan");
67
            throw new UsernameNotFoundException(String.format("Tenant not found after rescan, " + " tenant=%s", tenantIdentifier));
68
        }
69
        return this.dataSourcesMtApp.get(tenantIdentifier);
70
    }
71
72
    private String initializeTenantIfLost(String tenantIdentifier) {
73
        if (tenantIdentifier != DBContextHolder.getCurrentDb()) {
74
            tenantIdentifier = DBContextHolder.getCurrentDb();
75
        }
76
        return tenantIdentifier;
77
    }
78
}
79



TenantDatabaseConfig.java 

Java
 




xxxxxxxxxx
1
101


 
1
package com.amran.dynamic.multitenant.tenant.config;
2
3
import org.hibernate.MultiTenancyStrategy;
4
import org.hibernate.cfg.Environment;
5
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
6
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
7
import org.springframework.beans.factory.annotation.Qualifier;
8
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
9
import org.springframework.context.annotation.Bean;
10
import org.springframework.context.annotation.ComponentScan;
11
import org.springframework.context.annotation.Configuration;
12
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
13
import org.springframework.orm.jpa.JpaTransactionManager;
14
import org.springframework.orm.jpa.JpaVendorAdapter;
15
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
16
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
17
import org.springframework.transaction.annotation.EnableTransactionManagement;
18
19
import javax.persistence.EntityManagerFactory;
20
import java.util.HashMap;
21
import java.util.Map;
22
23
/**
24
 * @author Md. Amran Hossain
25
 */
26
@Configuration
27
@EnableTransactionManagement
28
@ComponentScan(basePackages = { "com.amran.dynamic.multitenant.tenant.repository", "com.amran.dynamic.multitenant.tenant.entity" })
29
@EnableJpaRepositories(basePackages = {"com.amran.dynamic.multitenant.tenant.repository", "com.amran.dynamic.multitenant.tenant.service" },
30
        entityManagerFactoryRef = "tenantEntityManagerFactory",
31
        transactionManagerRef = "tenantTransactionManager")
32
public class TenantDatabaseConfig {
33
34
    @Bean(name = "tenantJpaVendorAdapter")
35
    public JpaVendorAdapter jpaVendorAdapter() {
36
        return new HibernateJpaVendorAdapter();
37
    }
38
39
    @Bean(name = "tenantTransactionManager")
40
    public JpaTransactionManager transactionManager(@Qualifier("tenantEntityManagerFactory") EntityManagerFactory tenantEntityManager) {
41
        JpaTransactionManager transactionManager = new JpaTransactionManager();
42
        transactionManager.setEntityManagerFactory(tenantEntityManager);
43
        return transactionManager;
44
    }
45
46
    /**
47
     * The multi tenant connection provider
48
     *
49
     * @return
50
     */
51
    @Bean(name = "datasourceBasedMultitenantConnectionProvider")
52
    @ConditionalOnBean(name = "masterEntityManagerFactory")
53
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
54
        // Autowires the multi connection provider
55
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
56
    }
57
58
    /**
59
     * The current tenant identifier resolver
60
     *
61
     * @return
62
     */
63
    @Bean(name = "currentTenantIdentifierResolver")
64
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
65
        return new CurrentTenantIdentifierResolverImpl();
66
    }
67
68
    /**
69
     * Creates the entity manager factory bean which is required to access the
70
     * JPA functionalities provided by the JPA persistence provider, i.e.
71
     * Hibernate in this case.
72
     *
73
     * @param connectionProvider
74
     * @param tenantResolver
75
     * @return
76
     */
77
    @Bean(name = "tenantEntityManagerFactory")
78
    @ConditionalOnBean(name = "datasourceBasedMultitenantConnectionProvider")
79
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
80
            @Qualifier("datasourceBasedMultitenantConnectionProvider")
81
                    MultiTenantConnectionProvider connectionProvider,
82
            @Qualifier("currentTenantIdentifierResolver")
83
                    CurrentTenantIdentifierResolver tenantResolver) {
84
        LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
85
        //All tenant related entities, repositories and service classes must be scanned
86
        emfBean.setPackagesToScan("com.amran.dynamic.multitenant");
87
        emfBean.setJpaVendorAdapter(jpaVendorAdapter());
88
        emfBean.setPersistenceUnitName("tenantdb-persistence-unit");
89
        Map<String, Object> properties = new HashMap<>();
90
        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
91
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider);
92
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantResolver);
93
        properties.put(Environment.DIALECT, "org.hibernate.dialect.MySQL5Dialect");
94
        properties.put(Environment.SHOW_SQL, true);
95
        properties.put(Environment.FORMAT_SQL, true);
96
        properties.put(Environment.HBM2DDL_AUTO, "none");
97
        emfBean.setJpaPropertyMap(properties);
98
        return emfBean;
99
    }
100
}
101



It seems like we're almost done. So we should move to the next step in the process.

8. Database Data checks:

Master Database data:
  • tbl_tenant_master

Master database data

Tenant Database (MySQL) Table Data:
  • tbl_user
  • tbl_product

tbl_user metadata

tbl_product metadata

Tenant Database (PostgreSQL) Tables Data:
  • tbl_user
  • tbl_product

tbl_user metadata

tbl_product metadata


9. Now, test that everything works as we expect using Postman:

Target MySQL:
User authentication test

User authorization test

Target PostgreSQL:

User authentication with Postman

User authorization with Postman

Conclusion

I hope, this tutorial will be helpful for any person or organization. I tried to show how to enable multi-tenancy in your Spring Boot application using Spring Security and JWT.

You can find source code in link: https://github.com/amran-bd/Dynamic-Multi-Tenancy-Using-Java-Spring-Boot-Security-JWT-Rest-API-MySQL-Postgresql-full-example.

That's all.

Topics:
hibernate ,java ,jpa ,jwt ,mysql ,postgresql ,rest api ,spring ,spring security

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}