DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Enterprise RIA With Spring 3, Flex 4 and GraniteDS
  • How to Use JWT Securely
  • Authentication With Remote LDAP Server in Spring Web MVC
  • Authorization Code Grant Flow With Spring Security OAuth 2.0

Trending

  • 5 Subtle Indicators Your Development Environment Is Under Siege
  • A Developer's Guide to Mastering Agentic AI: From Theory to Practice
  • How to Convert XLS to XLSX in Java
  • Measuring the Impact of AI on Software Engineering Productivity
  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.

By 
Md Amran Hossain user avatar
Md Amran Hossain
DZone Core CORE ·
Updated by 
Rob Gravelle user avatar
Rob Gravelle
·
Updated Apr. 04, 24 · Tutorial
Likes (22)
Comment
Save
Tweet
Share
66.4K 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.Dynamic Multi-Tenant High-Level Diagram

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

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:

Initial 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

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

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

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
  • Authorization Code Grant Flow With Spring Security OAuth 2.0

Partner Resources

×

Comments
Oops! Something Went Wrong

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

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!