VOOZH about

URL: https://dzone.com/articles/dynamic-multi-tenancy-using-java-spring-boot-sprin

โ‡ฑ Developing a Multi-Tenancy Application With Spring Security


Related

  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Developing a Multi-Tenancy Application With Spring Security and JWTs

Developing a Multi-Tenancy Application With Spring Security and JWTs

This article guides you through the process of creating a multi-tenancy application following a Software as a Service (SaaS) model, where each client has a dedicated database.

Likes
Comment
Save
70.7K Views

Join the DZone community and get the full member experience.

Join For Free

This article addresses the need for a robust solution for implementing multi-tenancy in web applications while ensuring security. By adopting a database-per-tenant approach and storing user information within each tenant's database, both multi-tenancy and stringent security measures can be achieved seamlessly using Spring Security.

The main goal of this tutorial is to demonstrate the creation of a multi-tenancy application following a Software as a Service (SaaS) model, where each client has a dedicated database. We'll focus on integrating Spring Security and JWT for authentication and authorization. Whether you're connecting multiple schemas within a single database (e.g., MySQL) or multiple databases (e.g., MySQL, PostgreSQL, or Oracle), this tutorial will guide you through the process.


Mastering Java & Spring Framework Essentials Bundle*

*Affiliate link. See Terms of Use.

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.

  • 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:

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

How to Develop a Multi-Tenancy Application With Spring Security and JWT

1. Set up the project

Here are all the technologies that will play a role within our application:

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

You can set up your project quickly by using https://start.spring.io/.

Following the steps outlined in the Spring site should the resulting project structure:

2. 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 stored 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

Your pom file should look like this:

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 

We can configure the master database or common database into our Spring Boot application via the application.yml file as follows:

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. Enable Spring security and JWT

WebSecurityConfigurerAdapterallows 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. WebSecurityConfigurerAdapteris 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 AuthenticationEntryPointcan be launched. The AuthenticationEntryPointwill 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 the 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.ymlfile.

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 the 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. Perform database data checks

Master Database data:
  • tbl_tenant_master
Tenant Database (MySQL) Table Data:
  • tbl_user
  • tbl_product
Tenant Database (PostgreSQL) Tables Data:
  • tbl_user
  • tbl_product


9. Test that everything works as we expect using Postman

Target MySQL:

Target PostgreSQL:

Conclusion

This tutorial has provided a comprehensive guide for developing a multi-tenancy application with Spring Security and JWTs. By leveraging a database-per-tenant architecture and securely managing user credentials within each tenant's database, we've ensured both data isolation and robust security measures.

Throughout this tutorial, we've emphasized the importance of maintaining the integrity of each tenant's data while implementing authentication and authorization mechanisms using Spring Security and JWTs. By following the steps outlined here, you're equipped to build scalable and secure multi-tenant applications that adhere to industry standards and best practices.

I hope this tutorial will be helpful for any person or organization. 

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

Spring Framework Spring Security Database JWT (JSON Web Token) Web application

Opinions expressed by DZone contributors are their own.

Related

  • Enterprise RIA With Spring 3, Flex 4 and GraniteDS
  • How to Use JWT Securely
  • Authentication With Remote LDAP Server in Spring Web MVC
  • Prototype for a Java Database Application With REST and Security

Partner Resources

ร—

Comments

The likes didn't load as expected. Please refresh the page and try again.

Let's be friends: