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。
什么是多租户?
多租户是一种体系结构,其中软件应用程序的单个实例为多个客户提供服务。 每个客户称为租户。 可以赋予租户自定义应用程序某些部分的能力。
多租户应用程序是指租户(即公司中的用户)认为已经为他们创建并部署了该应用程序的地方。 实际上,有许多这样的租户,他们也使用相同的应用程序,但感觉它是专门为他们构建的。
动态多租户高级图:在这里,
客户端请求登录系统。
系统使用客户端ID与master数据库进行检查。
如果成功,则根据驱动程序类名称将当前数据库设置为上下文。
如果失败,则用户将收到消息"未授权"。
成功通过身份验证后,用户将获得JWT以进行下一次执行。
整个过程在以下工作流程中执行:
现在,让我们开始使用Spring Security和JWT逐步开发多租户应用程序。
1。 技术与项目结构:
Java 11。
Spring启动。
Spring安全。
SpringAOP。
Spring Data JPA。
冬眠。
智威汤逊
MySQL,PostgreSQL。
国际
您可以使用https://start.spring.io/快速入门。
项目结构:
2。 现在,创建一个主数据库和一个租户数据库。
主数据库:
在主数据库中,我们只有一个表(
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):
创建用于客户端登录身份验证的表(
创建另一个表(
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):
创建用于客户端登录身份验证的表(
创建另一个表(
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。 检查
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应用程序(
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:
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; } } |
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); } } |
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。 配置主数据库:
主数据源配置:
Web应用程序可以在
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(); } } |
创建另一个类
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; } } |
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
如果Hibernate无法解析要使用的租户标识符,它将使用方法
Hibernate提供了此接口的两种实现,具体取决于我们如何定义数据库连接:
使用Java中的数据源接口–我们将使用DataSourceBasedMultiTenantConnectionProviderImpl实现
使用Hibernate的ConnectionProvider接口–我们将使用AbstractMultiTenantConnectionProvider实现
CurrentTenantIdentifierResolver
Hibernate调用方法
模式方法在此策略中,我们将在同一物理数据库实例中使用不同的模式或用户。 当我们需要为应用程序提供最佳性能时,可以使用这种方法,并且会牺牲特殊的数据库功能,例如每个租户的备份。
数据库方法数据库多租户方法每个租户使用不同的物理数据库实例。 由于每个租户都是完全隔离的,因此当我们需要特殊的数据库功能时(例如每个租户的备份比我们需要的最佳性能更多),我们应该选择此策略。
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; } } |
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; } } |
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
tbl_user
tbl_product
tbl_user
tbl_product
9。 现在,使用邮递员测试一切是否按预期工作:
目标MySQL:
目标PostgreSQL :
结论
希望本教程对任何个人或组织都有帮助。 我试图展示如何使用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。
就这样。