使用Spring Security和JWT的动态多租户

Dynamic Multi-Tenancy Using Spring Security and JWTs

目的

我想要一个解决方案,其中通过将每个租户的数据库和所有用于身份验证和授权的用户信息(用户名,密码,客户端ID等)存储在各个租户数据库的用户表中来实现多租户。 这意味着我不仅需要多租户应用程序,而且需要像Spring Security保护的任何其他Web应用程序一样的安全应用程序。

我知道如何使用Spring Security来保护Web应用程序,以及如何使用Hibernate连接到数据库。 该要求进一步规定,属于租户的所有用户都必须存储在租户数据库中,而不是单独的数据库或中央数据库中。 这将允许每个租户完全隔离数据。

目标

  • 存档应用程序SaaS模型客户端明智的不同数据库。

  • 专注于Spring Security和JWT

  • 您可以将多个模式与单个数据库(例如MySQL)连接在一起-testdb,testdb2。

  • 您可以连接多个数据库,例如MySQL,PostgreSQL或Oracle。

  • 什么是多租户?

    多租户是一种体系结构,其中软件应用程序的单个实例为多个客户提供服务。 每个客户称为租户。 可以赋予租户自定义应用程序某些部分的能力。

    多租户应用程序是指租户(即公司中的用户)认为已经为他们创建并部署了该应用程序的地方。 实际上,有许多这样的租户,他们也使用相同的应用程序,但感觉它是专门为他们构建的。

    动态多租户高级图:Dynamic Multi-Tenant High-Level Diagram在这里,

  • 客户端请求登录系统。

  • 系统使用客户端ID与master数据库进行检查。

  • 如果成功,则根据驱动程序类名称将当前数据库设置为上下文。

  • 如果失败,则用户将收到消息"未授权"。

  • 成功通过身份验证后,用户将获得JWT以进行下一次执行。

  • 整个过程在以下工作流程中执行:

    Authentication workflow

    现在,让我们开始使用Spring Security和JWT逐步开发多租户应用程序。

    1。 技术与项目结构

  • Java 11。

  • Spring启动。

  • Spring安全。

  • SpringAOP。

  • Spring Data JPA。

  • 冬眠。

  • 智威汤逊

  • MySQL,PostgreSQL。

  • 国际

  • 您可以使用https://start.spring.io/快速入门。

    项目结构:

    Initial project structure

    2。 现在,创建一个主数据库和一个租户数据库。

    主数据库:

    在主数据库中,我们只有一个表(tbl_tenant_master),所有租户信息都存储在该表中。

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

    MySQL中的租户数据库(1):

    创建用于客户端登录身份验证的表(tbl_user)。

    创建另一个表(tbl_product)以使用JWT检索数据(用于授权检查)。

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

    DROP TABLE IF EXISTS `testdb`.`tbl_product`;
    CREATE TABLE  `testdb`.`tbl_product` (
      `product_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `product_name` varchar(50) NOT NULL,
      `quantity` int(10) unsigned NOT NULL DEFAULT '0',
      `size` varchar(3) NOT NULL,
      PRIMARY KEY (`product_id`)
    ) ENGINE=InnoDB;

    PostgreSQL中的租户数据库(2):

    创建用于客户端登录身份验证的表(tbl_user)。

    创建另一个表(tbl_product)以使用JWT检索数据(用于授权检查)。

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

    CREATE TABLE public.tbl_product
    (
        product_id integer NOT NULL,
        product_name character varying(50) COLLATE pg_catalog."default" NOT NULL,
        quantity integer NOT NULL DEFAULT 0,
        size character varying(3) COLLATE pg_catalog."default" NOT NULL,
        CONSTRAINT tbl_product_pkey PRIMARY KEY (product_id)
    )

    数据库创建和表创建完成!

    3。 检查pom.xml文件。

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

     <properties>
      <java.version>11</java.version>
     </properties>

     <dependencies>
      <dependency>
       <groupId>org.springframework.boot</groupId>
       spring-boot-starter-data-jpa</artifactId>
      </dependency>
      <dependency>
       <groupId>org.springframework.boot</groupId>
       spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
       <groupId>io.jsonwebtoken</groupId>
       jjwt</artifactId>
       <version>0.9.1</version>
      </dependency>
      <dependency>
       <groupId>org.springframework.boot</groupId>
       spring-boot-starter-web</artifactId>
      </dependency>

      <dependency>
       <groupId>org.springframework.boot</groupId>
       spring-boot-devtools</artifactId>
       <scope>runtime</scope>
       <optional>true</optional>
      </dependency>
      <dependency>
       <groupId>mysql</groupId>
       mysql-connector-java</artifactId>
       <scope>runtime</scope>
      </dependency>
      <dependency>
       <groupId>org.postgresql</groupId>
       postgresql</artifactId>
       <scope>runtime</scope>
      </dependency>
      <dependency>
       <groupId>joda-time</groupId>
       joda-time</artifactId>
       <version>2.10</version>
      </dependency>
      <dependency>
       <groupId>org.apache.commons</groupId>
       commons-lang3</artifactId>
      </dependency>
      <dependency>
       <groupId>org.springframework.boot</groupId>
       spring-boot-starter-tomcat</artifactId>
       <scope>provided</scope>
      </dependency>
      <dependency>
       <groupId>org.springframework.boot</groupId>
       spring-boot-starter-test</artifactId>
       <scope>test</scope>
       <exclusions>
        <exclusion>
         <groupId>org.junit.vintage</groupId>
         junit-vintage-engine</artifactId>
        </exclusion>
       </exclusions>
      </dependency>
      <dependency>
       <groupId>org.springframework.security</groupId>
       spring-security-test</artifactId>
       <scope>test</scope>
      </dependency>
     </dependencies>

     <build>
      <plugins>
       <plugin>
        <groupId>org.springframework.boot</groupId>
        spring-boot-maven-plugin</artifactId>
       </plugin>
      </plugins>
     </build>

    </project>

    4。 将master数据库或通用数据库配置到我们的Spring Boot应用程序(application.yml)中。

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

    5。 Spring Security和启用JWT:

    WebSecurityConfigurerAdapter允许用户为特定选择(在本例中为全部)请求配置基于Web的安全性。 它允许配置影响我们应用程序安全性的事物。 WebSecurityConfigurerAdapter是一个便捷类,允许对WebSecurityHttpSecurity进行自定义。

    WebSecurityConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    package com.amran.dynamic.multitenant.security;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;

    /**
     * @author Md. Amran Hossain
     */

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Autowired
        private JwtUserDetailsService jwtUserDetailsService;
        @Autowired
        private JwtAuthenticationEntryPoint unauthorizedHandler;

        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }

        @Autowired
        public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
        }

        @Bean
        public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
            return new JwtAuthenticationFilter();
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.cors().and().csrf().disable().
                    authorizeRequests()
                    .antMatchers("/api/auth/**").permitAll()
                    .antMatchers("/api/product/**").authenticated()
                    .and()
                    .exceptionHandling().AuthenticationEntryPoint(unauthorizedHandler).and()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
        }

    //    @Bean
    //    public PasswordEncoder passwordEncoder() {
    //        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    //        return encoder;
    //    }

        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }

        @Bean
        public FilterRegistrationBean platformCorsFilter() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

            CorsConfiguration configAutenticacao = new CorsConfiguration();
            configAutenticacao.setAllowCredentials(true);
            configAutenticacao.addAllowedOrigin("*");
            configAutenticacao.addAllowedHeader("Authorization");
            configAutenticacao.addAllowedHeader("Content-Type");
            configAutenticacao.addAllowedHeader("Accept");
            configAutenticacao.addAllowedMethod("POST");
            configAutenticacao.addAllowedMethod("GET");
            configAutenticacao.addAllowedMethod("DELETE");
            configAutenticacao.addAllowedMethod("PUT");
            configAutenticacao.addAllowedMethod("OPTIONS");
            configAutenticacao.setMaxAge(3600L);
            source.registerCorsConfiguration("/**", configAutenticacao);

            FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
            bean.setOrder(-110);
            return bean;
        }
    }

    OncePerRequestFilter类是一个过滤器基类,旨在保证在任何servlet容器上的每个请求分派单个执行。 从Servlet 3.0开始,过滤器可以作为在单独线程中发生的REQUEST或ASYNC调度的一部分来调用。

    JwtAuthenticationFilter.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    package com.amran.dynamic.multitenant.security;

    import com.amran.dynamic.multitenant.constant.JWTConstants;
    import com.amran.dynamic.multitenant.mastertenant.config.DBContextHolder;
    import com.amran.dynamic.multitenant.mastertenant.entity.MasterTenant;
    import com.amran.dynamic.multitenant.mastertenant.service.MasterTenantService;
    import com.amran.dynamic.multitenant.util.JwtTokenUtil;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.SignatureException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;

    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Arrays;

    /**
     * @author Md. Amran Hossain
     */

    @Component
    public class JwtAuthenticationFilter extends OncePerRequestFilter {

        @Autowired
        private JwtUserDetailsService jwtUserDetailsService;
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
        @Autowired
        MasterTenantService masterTenantService;

        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            String header = httpServletRequest.getHeader(JWTConstants.HEADER_STRING);
            String username = null;
            String audience = null; //tenantOrClientId
            String authToken = null;
            if (header != null && header.startsWith(JWTConstants.TOKEN_PREFIX)) {
                authToken = header.replace(JWTConstants.TOKEN_PREFIX,"");
                try {
                    username = jwtTokenUtil.getUsernameFromToken(authToken);
                    audience = jwtTokenUtil.getAudienceFromToken(authToken);
                    MasterTenant masterTenant = masterTenantService.findByClientId(Integer.valueOf(audience));
                    if(null == masterTenant){
                        logger.error("An error during getting tenant name");
                        throw new BadCredentialsException("Invalid tenant and user.");
                    }
                    DBContextHolder.setCurrentDb(masterTenant.getDbName());
                } catch (IllegalArgumentException ex) {
                    logger.error("An error during getting username from token", ex);
                } catch (ExpiredJwtException ex) {
                    logger.warn("The token is expired and not valid anymore", ex);
                } catch(SignatureException ex){
                    logger.error("Authentication Failed. Username or Password not valid.",ex);
                }
            } else {
                logger.warn("Couldn't find bearer string, will ignore the header");
            }
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    logger.info("authenticated user" + username +", setting security context");
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    }

    ExceptionTranslationFilter用于捕获任何Spring Security异常,以便可以返回HTTP错误响应,或者可以启动适当的AuthenticationEntryPoint。 如果用户请求安全的HTTP资源,但未通过身份验证,则将调用AuthenticationEntryPoint

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.amran.dynamic.multitenant.security;

    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;

    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.Serializable;

    /**
     * @author Md. Amran Hossain
     */

    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

        private static final long serialVersionUID = -7858869558953243875L;

        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized");
        }
    }

    6。 配置主数据库:

    主数据源配置:

    ThreadLocals用于维护与当前线程相关的某些上下文。 例如,当当前事务存储在ThreadLocal中时,您不需要通过每个方法调用将它作为参数传递,以防堆栈中有人需要访问它。

    Web应用程序可以在ThreadLocal中存储有关当前请求和会话的信息,以便该应用程序可以轻松访问它们。 在实现注入对象的自定义范围时可以使用ThreadLocals

    ThreadLocals是一种全局变量(尽管邪恶程度稍低,因为它们仅限于一个线程),因此在使用它们时应格外小心,以避免不必要的副作用和内存泄漏。

    DBContextHolder.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.amran.dynamic.multitenant.mastertenant.config;

    /**
     * @author Md. Amran Hossain
     * The context holder implementation is a container that stores the current context as a ThreadLocal reference.
     */

    public class DBContextHolder {

        private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

        public static void setCurrentDb(String dbType) {
            contextHolder.set(dbType);
        }

        public static String getCurrentDb() {
            return contextHolder.get();
        }

        public static void clear() {
            contextHolder.remove();
        }
    }

    创建另一个类MasterDatabaseConfigProperties.java。 它包含与连接相关的参数,如application.yml文件中所定义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    package com.amran.dynamic.multitenant.mastertenant.config;

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

    /**
     * @author Md. Amran Hossain
     */

    @Configuration
    @ConfigurationProperties("multitenancy.mtapp.master.datasource")
    public class MasterDatabaseConfigProperties {

        private String url;
        private String username;
        private String password;
        private String driverClassName;
        private long connectionTimeout;
        private int maxPoolSize;
        private long idleTimeout;
        private int minIdle;
        private String poolName;

        //Initialization of HikariCP.
        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("MasterDatabaseConfigProperties [url=");
            builder.append(url);
            builder.append(", username=");
            builder.append(username);
            builder.append(", password=");
            builder.append(password);
            builder.append(", driverClassName=");
            builder.append(driverClassName);
            builder.append(", connectionTimeout=");
            builder.append(connectionTimeout);
            builder.append(", maxPoolSize=");
            builder.append(maxPoolSize);
            builder.append(", idleTimeout=");
            builder.append(idleTimeout);
            builder.append(", minIdle=");
            builder.append(minIdle);
            builder.append(", poolName=");
            builder.append(poolName);
            builder.append("]");
            return builder.toString();
        }

        public String getUrl() {
            return url;
        }

        public MasterDatabaseConfigProperties setUrl(String url) {
            this.url = url;
            return this;
        }

        public String getUsername() {
            return username;
        }

        public MasterDatabaseConfigProperties setUsername(String username) {
            this.username = username;
            return this;
        }

        public String getPassword() {
            return password;
        }

        public MasterDatabaseConfigProperties setPassword(String password) {
            this.password = password;
            return this;
        }

        public String getDriverClassName() {
            return driverClassName;
        }

        public MasterDatabaseConfigProperties setDriverClassName(String driverClassName) {
            this.driverClassName = driverClassName;
            return this;
        }

        public long getConnectionTimeout() {
            return connectionTimeout;
        }

        public MasterDatabaseConfigProperties setConnectionTimeout(long connectionTimeout) {
            this.connectionTimeout = connectionTimeout;
            return this;
        }

        public int getMaxPoolSize() {
            return maxPoolSize;
        }

        public MasterDatabaseConfigProperties setMaxPoolSize(int maxPoolSize) {
            this.maxPoolSize = maxPoolSize;
            return this;
        }

        public long getIdleTimeout() {
            return idleTimeout;
        }

        public MasterDatabaseConfigProperties setIdleTimeout(long idleTimeout) {
            this.idleTimeout = idleTimeout;
            return this;
        }

        public int getMinIdle() {
            return minIdle;
        }

        public MasterDatabaseConfigProperties setMinIdle(int minIdle) {
            this.minIdle = minIdle;
            return this;
        }

        public String getPoolName() {
            return poolName;
        }

        public MasterDatabaseConfigProperties setPoolName(String poolName) {
            this.poolName = poolName;
            return this;
        }
    }

    @EnableTransactionManagement<tx:annotation-driven/>负责注册必要的Spring组件,这些组件可为注释驱动的事务管理提供支持,例如TransactionInterceptor和代理,或者注册基于AspectJ的建议,当JdbcFooRepository的< x3>方法被调用。

    MasterDatabaseConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    package com.amran.dynamic.multitenant.mastertenant.config;

    import com.amran.dynamic.multitenant.mastertenant.entity.MasterTenant;
    import com.amran.dynamic.multitenant.mastertenant.repository.MasterTenantRepository;
    import com.zaxxer.hikari.HikariDataSource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.orm.jpa.JpaTransactionManager;
    import org.springframework.orm.jpa.JpaVendorAdapter;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    import javax.persistence.EntityManagerFactory;
    import javax.sql.DataSource;
    import java.util.Properties;

    /**
     * @author Md. Amran Hossain
     */

    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(basePackages = {"com.amran.dynamic.multitenant.mastertenant.entity","com.amran.dynamic.multitenant.mastertenant.repository"},
            entityManagerFactoryRef ="masterEntityManagerFactory",
            transactionManagerRef ="masterTransactionManager")
    public class MasterDatabaseConfig {

        private static final Logger LOG = LoggerFactory.getLogger(MasterDatabaseConfig.class);

        @Autowired
        private MasterDatabaseConfigProperties masterDbProperties;

        //Create Master Data Source using master properties and also configure HikariCP
        @Bean(name ="masterDataSource")
        public DataSource masterDataSource() {
            HikariDataSource hikariDataSource = new HikariDataSource();
            hikariDataSource.setUsername(masterDbProperties.getUsername());
            hikariDataSource.setPassword(masterDbProperties.getPassword());
            hikariDataSource.setJdbcUrl(masterDbProperties.getUrl());
            hikariDataSource.setDriverClassName(masterDbProperties.getDriverClassName());
            hikariDataSource.setPoolName(masterDbProperties.getPoolName());
            // HikariCP settings
            hikariDataSource.setMaximumPoolSize(masterDbProperties.getMaxPoolSize());
            hikariDataSource.setMinimumIdle(masterDbProperties.getMinIdle());
            hikariDataSource.setConnectionTimeout(masterDbProperties.getConnectionTimeout());
            hikariDataSource.setIdleTimeout(masterDbProperties.getIdleTimeout());
            LOG.info("Setup of masterDataSource succeeded.");
            return hikariDataSource;
        }

        @Primary
        @Bean(name ="masterEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory() {
            LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
            // Set the master data source
            em.setDataSource(masterDataSource());
            // The master tenant entity and repository need to be scanned
            em.setPackagesToScan(new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()});
            // Setting a name for the persistence unit as Spring sets it as
            // 'default' if not defined
            em.setPersistenceUnitName("masterdb-persistence-unit");
            // Setting Hibernate as the JPA provider
            JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
            em.setJpaVendorAdapter(vendorAdapter);
            // Set the hibernate properties
            em.setJpaProperties(hibernateProperties());
            LOG.info("Setup of masterEntityManagerFactory succeeded.");
            return em;
        }

        @Bean(name ="masterTransactionManager")
        public JpaTransactionManager masterTransactionManager(@Qualifier("masterEntityManagerFactory") EntityManagerFactory emf) {
            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(emf);
            return transactionManager;
        }

        @Bean
        public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
            return new PersistenceExceptionTranslationPostProcessor();
        }

        //Hibernate configuration properties
        private Properties hibernateProperties() {
            Properties properties = new Properties();
            properties.put(org.hibernate.cfg.Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
            properties.put(org.hibernate.cfg.Environment.SHOW_SQL, true);
            properties.put(org.hibernate.cfg.Environment.FORMAT_SQL, true);
            properties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO,"none");
            return properties;
        }
    }

    7。 配置承租人数据库。

    在本节中,我们将努力了解Hibernate中的多租户。 Hibernate中有三种方法可以实现多租户:

  • 单独的架构-同一物理数据库实例中每个租户一个架构。

  • 单独的数据库-每个租户一个单独的物理数据库实例。

  • 分区(区分字符)数据-每个租户的数据均按区分符值进行分区。

  • 像往常一样,Hibernate将每种方法的实现抽象为复杂性,我们需要提供以下两个接口的实现:

  • MultiTenantConnectionProvider –提供每个租户的连接。

  • CurrentTenantIdentifierResolver –解析要使用的租户标识符。

  • MultiTenantConnectionProvider

    如果Hibernate无法解析要使用的租户标识符,它将使用方法getAnyConnection来建立连接。 否则,它将使用方法getConnection

    Hibernate提供了此接口的两种实现,具体取决于我们如何定义数据库连接

  • 使用Java中的数据源接口–我们将使用DataSourceBasedMultiTenantConnectionProviderImpl实现

  • 使用Hibernate的ConnectionProvider接口–我们将使用AbstractMultiTenantConnectionProvider实现

  • CurrentTenantIdentifierResolver

    Hibernate调用方法resolveCurrentTenantIdentifier以获取租户标识符。 如果我们希望Hibernate验证所有现有会话都属于同一租户标识符,则方法validateExistingCurrentSessions应该返回true。

    模式方法在此策略中,我们将在同一物理数据库实例中使用不同的模式或用户。 当我们需要为应用程序提供最佳性能时,可以使用这种方法,并且会牺牲特殊的数据库功能,例如每个租户的备份。

    数据库方法数据库多租户方法每个租户使用不同的物理数据库实例。 由于每个租户都是完全隔离的,因此当我们需要特殊的数据库功能时(例如每个租户的备份比我们需要的最佳性能更多),我们应该选择此策略。

    CurrentTenantIdentifierResolverImpl.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.amran.dynamic.multitenant.tenant.config;

    import com.amran.dynamic.multitenant.mastertenant.config.DBContextHolder;
    import org.apache.commons.lang3.StringUtils;
    import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

    /**
     * @author Md. Amran Hossain
     */

    public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

        private static final String DEFAULT_TENANT_ID ="client_tenant_1";

        @Override
        public String resolveCurrentTenantIdentifier() {
            String tenant = DBContextHolder.getCurrentDb();
            return StringUtils.isNotBlank(tenant) ? tenant : DEFAULT_TENANT_ID;
        }

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

    DataSourceBasedMultiTenantConnectionProviderImpl.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    package com.amran.dynamic.multitenant.tenant.config;

    import com.amran.dynamic.multitenant.mastertenant.config.DBContextHolder;
    import com.amran.dynamic.multitenant.mastertenant.entity.MasterTenant;
    import com.amran.dynamic.multitenant.mastertenant.repository.MasterTenantRepository;
    import com.amran.dynamic.multitenant.util.DataSourceUtil;
    import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;

    import javax.sql.DataSource;
    import java.util.List;
    import java.util.Map;
    import java.util.TreeMap;

    /**
     * @author Md. Amran Hossain
     */

    @Configuration
    public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

        private static final Logger LOG = LoggerFactory.getLogger(DataSourceBasedMultiTenantConnectionProviderImpl.class);

        private static final long serialVersionUID = 1L;

        private Map<String, DataSource> dataSourcesMtApp = new TreeMap<>();

        @Autowired
        private MasterTenantRepository masterTenantRepository;

        @Autowired
        ApplicationContext applicationContext;

        @Override
        protected DataSource selectAnyDataSource() {
            // This method is called more than once. So check if the data source map
            // is empty. If it is then rescan master_tenant table for all tenant
            if (dataSourcesMtApp.isEmpty()) {
                List<MasterTenant> masterTenants = masterTenantRepository.findAll();
                LOG.info("selectAnyDataSource() method call...Total tenants:" + masterTenants.size());
                for (MasterTenant masterTenant : masterTenants) {
                    dataSourcesMtApp.put(masterTenant.getDbName(), DataSourceUtil.createAndConfigureDataSource(masterTenant));
                }
            }
            return this.dataSourcesMtApp.values().iterator().next();
        }

        @Override
        protected DataSource selectDataSource(String tenantIdentifier) {
            // If the requested tenant id is not present check for it in the master
            // database 'master_tenant' table
            tenantIdentifier = initializeTenantIfLost(tenantIdentifier);
            if (!this.dataSourcesMtApp.containsKey(tenantIdentifier)) {
                List<MasterTenant> masterTenants = masterTenantRepository.findAll();
                LOG.info("selectDataSource() method call...Tenant:" + tenantIdentifier +" Total tenants:" + masterTenants.size());
                for (MasterTenant masterTenant : masterTenants) {
                    dataSourcesMtApp.put(masterTenant.getDbName(), DataSourceUtil.createAndConfigureDataSource(masterTenant));
                }
            }
            //check again if tenant exist in map after rescan master_db, if not, throw UsernameNotFoundException
            if (!this.dataSourcesMtApp.containsKey(tenantIdentifier)) {
                LOG.warn("Trying to get tenant:" + tenantIdentifier +" which was not found in master db after rescan");
                throw new UsernameNotFoundException(String.format("Tenant not found after rescan," +" tenant=%s", tenantIdentifier));
            }
            return this.dataSourcesMtApp.get(tenantIdentifier);
        }

        private String initializeTenantIfLost(String tenantIdentifier) {
            if (tenantIdentifier != DBContextHolder.getCurrentDb()) {
                tenantIdentifier = DBContextHolder.getCurrentDb();
            }
            return tenantIdentifier;
        }
    }

    TenantDatabaseConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    package com.amran.dynamic.multitenant.tenant.config;

    import org.hibernate.MultiTenancyStrategy;
    import org.hibernate.cfg.Environment;
    import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
    import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.orm.jpa.JpaTransactionManager;
    import org.springframework.orm.jpa.JpaVendorAdapter;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    import javax.persistence.EntityManagerFactory;
    import java.util.HashMap;
    import java.util.Map;

    /**
     * @author Md. Amran Hossain
     */

    @Configuration
    @EnableTransactionManagement
    @ComponentScan(basePackages = {"com.amran.dynamic.multitenant.tenant.repository","com.amran.dynamic.multitenant.tenant.entity" })
    @EnableJpaRepositories(basePackages = {"com.amran.dynamic.multitenant.tenant.repository","com.amran.dynamic.multitenant.tenant.service" },
            entityManagerFactoryRef ="tenantEntityManagerFactory",
            transactionManagerRef ="tenantTransactionManager")
    public class TenantDatabaseConfig {

        @Bean(name ="tenantJpaVendorAdapter")
        public JpaVendorAdapter jpaVendorAdapter() {
            return new HibernateJpaVendorAdapter();
        }

        @Bean(name ="tenantTransactionManager")
        public JpaTransactionManager transactionManager(@Qualifier("tenantEntityManagerFactory") EntityManagerFactory tenantEntityManager) {
            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(tenantEntityManager);
            return transactionManager;
        }

        /**
         * The multi tenant connection provider
         *
         * @return
         */

        @Bean(name ="datasourceBasedMultiTenantConnectionProvider")
        @ConditionalOnBean(name ="masterEntityManagerFactory")
        public MultiTenantConnectionProvider MultiTenantConnectionProvider() {
            // Autowires the multi connection provider
            return new DataSourceBasedMultiTenantConnectionProviderImpl();
        }

        /**
         * The current tenant identifier resolver
         *
         * @return
         */

        @Bean(name ="CurrentTenantIdentifierResolver")
        public CurrentTenantIdentifierResolver CurrentTenantIdentifierResolver() {
            return new CurrentTenantIdentifierResolverImpl();
        }

        /**
         * Creates the entity manager factory bean which is required to access the
         * JPA functionalities provided by the JPA persistence provider, i.e.
         * Hibernate in this case.
         *
         * @param connectionProvider
         * @param tenantResolver
         * @return
         */

        @Bean(name ="tenantEntityManagerFactory")
        @ConditionalOnBean(name ="datasourceBasedMultiTenantConnectionProvider")
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(
                @Qualifier("datasourceBasedMultiTenantConnectionProvider")
                        MultiTenantConnectionProvider connectionProvider,
                @Qualifier("CurrentTenantIdentifierResolver")
                        CurrentTenantIdentifierResolver tenantResolver) {
            LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
            //All tenant related entities, repositories and service classes must be scanned
            emfBean.setPackagesToScan("com.amran.dynamic.multitenant");
            emfBean.setJpaVendorAdapter(jpaVendorAdapter());
            emfBean.setPersistenceUnitName("tenantdb-persistence-unit");
            Map<String, Object> properties = new HashMap<>();
            properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
            properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider);
            properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantResolver);
            properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
            properties.put(Environment.SHOW_SQL, true);
            properties.put(Environment.FORMAT_SQL, true);
            properties.put(Environment.HBM2DDL_AUTO,"none");
            emfBean.setJpaPropertyMap(properties);
            return emfBean;
        }
    }

    看来我们快完成了。 因此,我们应该继续进行下一步。

    8。 数据库数据检查:

  • tbl_tenant_master

  • Master database data

  • tbl_user

  • tbl_product

  • tbl_user metadata

    tbl_product metadata

  • tbl_user

  • tbl_product

  • tbl_user metadata

    tbl_product metadata

    9。 现在,使用邮递员测试一切是否按预期工作:

    目标MySQL: User authentication test

    User authorization test

    目标PostgreSQL

    User authentication with Postman

    User authorization with Postman

    结论

    希望本教程对任何个人或组织都有帮助。 我试图展示如何使用Spring Security和JWT在Spring Boot应用程序中启用多租户。

    您可以在以下链接中找到源代码:https://github.com/amran-bd/Dynamic-Multi-Tenancy-Using-Java-Spring-Boot-Security-JWT-Rest-API-MySQL-Postgresql-full-example。

    就这样。