当前位置:   article > 正文

mybatis-plus 基于 schema 多租户_mybatisplus schema

mybatisplus schema

 多租户实现分类
既然多租户设计的难点在于隔离用户数据,又同时共享资源。那么可以根据用户数据的物理分离程度来进行分类。

分为三类:数据库(DataBase)、模式(Schema)、数据表(Table)。

分离数据库
每一个租户分配一个数据库连接池,根据租户id获取对应的连接池

分离数据库类型


模式(Schema)
应用使用一个数据库连接池,切换不同的 Schema 。就可以切换不同租户。(可以简单理解为。java里面的包名称。不同的包名下,可以有相同类名的 class )

MySQL 不支持 Schema 使用数据库代替! SQL Server 和  PostgreSQL 支持 Schema、

分离模式类型

数据表(Table)
给每一个表结构,添加一个 tenant_id 字段,在 select、insert、update、delete 中都加上 tenant_id 的条件,此方式也是最简单,改动代码最少的。但是数据量大的时候,单表压力较大!

给表添加标识类型

以上内容截取自 Spring Boot JPA MySQL 多租户系统 Part1 - 基础实现


本次使用第二种方式 “模式(Schema)” 基于 spring-boot 2.6.14 和 mybatis-plus 3.5.2 以及 liquibase 4.17.2

liquibase 是用来管理 表结构变化的版本控制!重中之重!!!
因为 每创建一个租户,都需要创建表结构,所以必须要有版本来控制 表结构,
而我们的项目不可能表结构,一次创建永远不修改。
那么已存在的租户,表结构,也需要跟着修改!

  1. <dependency>
  2. <groupId>org.liquibase</groupId>
  3. <artifactId>liquibase-core</artifactId>
  4. <version>4.17.2</version>
  5. </dependency>

创建一个 DatabaseManager 用于管理,数据库的表结构!

  1. package com.xaaef.molly.core.tenant;
  2. import com.xaaef.molly.core.tenant.props.MultiTenantProperties;
  3. import javax.sql.DataSource;
  4. public interface DatabaseManager {
  5. default String getOldDbName(String url) {
  6. var startInx = url.lastIndexOf("?");
  7. var sub = url.substring(0, startInx);
  8. int startInx2 = sub.lastIndexOf("/") + 1;
  9. return sub.substring(startInx2);
  10. }
  11. /**
  12. * TODO 租户创建表结构
  13. *
  14. * @author WangChenChen
  15. * @date 2022/12/11 11:04
  16. */
  17. void createTable(String tenantId);
  18. /**
  19. * TODO 租户删除表结构
  20. *
  21. * @author WangChenChen
  22. * @date 2022/12/11 11:04
  23. */
  24. void deleteTable(String tenantId);
  25. /**
  26. * TODO 获取多租户信息
  27. *
  28. * @author WangChenChen
  29. * @date 2022/12/11 11:04
  30. */
  31. MultiTenantProperties getMultiTenantProperties();
  32. }

createTable() :  创建租户的时候,创建表结构

deleteTable() :  删除租户的时候,删除表结构

实现类

  1. package com.xaaef.molly.core.tenant.schema;
  2. import com.xaaef.molly.core.tenant.DatabaseManager;
  3. import com.xaaef.molly.core.tenant.props.MultiTenantProperties;
  4. import liquibase.Liquibase;
  5. import liquibase.database.jvm.JdbcConnection;
  6. import liquibase.resource.ClassLoaderResourceAccessor;
  7. import lombok.AllArgsConstructor;
  8. import lombok.extern.slf4j.Slf4j;
  9. import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  10. import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
  11. import org.springframework.stereotype.Component;
  12. import javax.sql.DataSource;
  13. import java.sql.Connection;
  14. import java.sql.DriverManager;
  15. import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX;
  16. /**
  17. * 多租户 基于 模式(Schema)
  18. * 此方式,是 默认连接池,切换 租户的 schema,
  19. * 因为 mysql 数据库不支持 schema 所以只能使用 数据库 代替
  20. *
  21. * @author WangChenChen
  22. * @date 2022/12/11 11:29
  23. */
  24. @Slf4j
  25. @Component
  26. @AllArgsConstructor
  27. @ConditionalOnProperty(prefix = "multi.tenant", name = "db-style", havingValue = "Schema")
  28. public class SchemaDataSourceManager implements DatabaseManager {
  29. // 默认租户的数据源
  30. private final DataSource dataSource;
  31. private final MultiTenantProperties multiTenantProperties;
  32. private final DataSourceProperties dataSourceProperties;
  33. @Override
  34. public MultiTenantProperties getMultiTenantProperties() {
  35. return multiTenantProperties;
  36. }
  37. /**
  38. * 创建表
  39. *
  40. * @author WangChenChen
  41. * @date 2022/12/7 21:05
  42. */
  43. @Override
  44. public void createTable(String tenantId) {
  45. log.info("tenantId: {} create table ...", tenantId);
  46. try {
  47. // 判断 schema 是否存在。不存在就创建
  48. var conn = dataSource.getConnection();
  49. // 判断数据库是否存在!不存在就创建
  50. String tenantDbName = multiTenantProperties.getPrefix() + tenantId;
  51. String sql = String.format("CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;", tenantDbName);
  52. conn.createStatement().execute(sql);
  53. // 创建一次性的 jdbc 链接。只是用来生成表结构的。用完就关闭。
  54. var conn1 = new JdbcConnection(getTempConnection(tenantDbName));
  55. var changeLogPath = multiTenantProperties.getOtherChangeLog();
  56. // 使用 Liquibase 创建表结构
  57. if (multiTenantProperties.getOtherChangeLog().startsWith(CLASSPATH_URL_PREFIX)) {
  58. changeLogPath = multiTenantProperties.getOtherChangeLog().replaceFirst(CLASSPATH_URL_PREFIX, "");
  59. }
  60. var liquibase = new Liquibase(changeLogPath, new ClassLoaderResourceAccessor(), conn1);
  61. liquibase.update(tenantId);
  62. // 关闭链接
  63. conn1.close();
  64. } catch (Exception e) {
  65. e.printStackTrace();
  66. }
  67. }
  68. @Override
  69. public void deleteTable(String tenantId) {
  70. log.info("tenantId: {} delete table ...", tenantId);
  71. String tenantDbName = multiTenantProperties.getPrefix() + tenantId;
  72. String sql = String.format("DROP DATABASE %s ;", tenantDbName);
  73. try {
  74. var conn = getTempConnection(tenantDbName);
  75. conn.createStatement().execute(sql);
  76. conn.close();
  77. } catch (Exception e) {
  78. e.printStackTrace();
  79. }
  80. }
  81. /**
  82. * 创建 临时的 jdbc 连接。 用于生成表结构。用完就关闭
  83. *
  84. * @return
  85. * @author WangChenChen
  86. * @date 2022/12/8 12:44
  87. */
  88. private Connection getTempConnection(String tenantDbName) throws Exception {
  89. // 获取默认的数据名称
  90. var oldDbName = getOldDbName(dataSourceProperties.getUrl());
  91. // 替换连接池中的数据库名称
  92. var dataSourceUrl = dataSourceProperties.getUrl().replaceFirst(oldDbName, tenantDbName);
  93. //3.获取数据库连接对象
  94. return DriverManager.getConnection(dataSourceUrl,
  95. dataSourceProperties.getUsername(),
  96. dataSourceProperties.getPassword());
  97. }
  98. }

多租户的配置类。包括,默认的租户ID, 租户数据库的前缀。以及 生成表结构的 语句

  1. package com.xaaef.molly.core.tenant.props;
  2. import com.xaaef.molly.core.tenant.enums.DbStyle;
  3. import lombok.Getter;
  4. import lombok.Setter;
  5. import org.springframework.boot.context.properties.ConfigurationProperties;
  6. import org.springframework.stereotype.Component;
  7. /**
  8. * <p>
  9. * 多租户全局配置
  10. * </p>
  11. *
  12. * @author WangChenChen
  13. * @version 1.1
  14. * @date 2022/12/9 11:53
  15. */
  16. @Getter
  17. @Setter
  18. @Component
  19. @ConfigurationProperties(prefix = "multi.tenant")
  20. public class MultiTenantProperties {
  21. /**
  22. * 是否开启租户模式
  23. */
  24. private Boolean enable = true;
  25. /**
  26. * 是否开启租户模式
  27. */
  28. private Boolean enableProject = false;
  29. /**
  30. * 数据库名称前缀
  31. */
  32. private String prefix = "molly_";
  33. /**
  34. * 默认租户ID
  35. */
  36. private String defaultTenantId = "master";
  37. /**
  38. * 默认 项目ID
  39. */
  40. private String defaultProjectId = "master";
  41. /**
  42. * 多租户的类型。
  43. *
  44. * 一定要在配置文件里指定....
  45. */
  46. private DbStyle dbStyle;
  47. /**
  48. * 创建表结构
  49. */
  50. private Boolean createTable = Boolean.TRUE;
  51. /**
  52. * 其他 数据库 创建表结构的 Liquibase 文件地址
  53. */
  54. private String otherChangeLog = "classpath:db/changelog-other.xml";
  55. /**
  56. * 主 数据库 创建表结构的 Liquibase 文件地址
  57. */
  58. private String masterChangeLog = "classpath:db/changelog-master.xml";
  59. }
  1. package com.xaaef.molly.core.tenant.util;
  2. import org.apache.commons.lang3.StringUtils;
  3. import org.springframework.core.NamedInheritableThreadLocal;
  4. import org.springframework.web.context.request.RequestContextHolder;
  5. import org.springframework.web.context.request.ServletRequestAttributes;
  6. import java.util.Objects;
  7. import static com.xaaef.molly.core.tenant.consts.MbpConst.X_PROJECT_ID;
  8. import static com.xaaef.molly.core.tenant.consts.MbpConst.X_TENANT_ID;
  9. /**
  10. * <p>
  11. * </p>
  12. *
  13. * @author WangChenChen
  14. * @version 1.1
  15. * @date 2022/11/25 11:14
  16. */
  17. public class TenantUtils {
  18. private final static ThreadLocal<String> TENANT_ID_THREAD_LOCAL = new NamedInheritableThreadLocal<>("TENANT_ID_THREAD_LOCAL");
  19. private final static ThreadLocal<String> PROJECT_ID_THREAD_LOCAL = new NamedInheritableThreadLocal<>("PROJECT_ID_THREAD_LOCAL");
  20. /**
  21. * 获取 租户ID
  22. */
  23. public static String getTenantId() {
  24. if (StringUtils.isNotBlank(TENANT_ID_THREAD_LOCAL.get())) {
  25. return TENANT_ID_THREAD_LOCAL.get();
  26. }
  27. var attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  28. if (Objects.isNull(attributes)) {
  29. return null;
  30. }
  31. var request = attributes.getRequest();
  32. return request.getHeader(X_TENANT_ID);
  33. }
  34. /**
  35. * 设置 租户ID
  36. */
  37. public static void setTenantId(String tenantId) {
  38. if (StringUtils.isNotBlank(tenantId)) {
  39. TENANT_ID_THREAD_LOCAL.set(tenantId);
  40. } else {
  41. TENANT_ID_THREAD_LOCAL.remove();
  42. }
  43. }
  44. /**
  45. * 获取 项目ID
  46. */
  47. public static String getProjectId() {
  48. if (StringUtils.isNotBlank(PROJECT_ID_THREAD_LOCAL.get())) {
  49. return PROJECT_ID_THREAD_LOCAL.get();
  50. }
  51. var attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  52. if (Objects.isNull(attributes)) {
  53. return null;
  54. }
  55. var request = attributes.getRequest();
  56. return request.getHeader(X_PROJECT_ID);
  57. }
  58. /**
  59. * 设置 项目ID
  60. */
  61. public static void setProjectId(String tenantId) {
  62. if (StringUtils.isNotBlank(tenantId)) {
  63. PROJECT_ID_THREAD_LOCAL.set(tenantId);
  64. } else {
  65. PROJECT_ID_THREAD_LOCAL.remove();
  66. }
  67. }
  68. }

spring mvc 拦截器,从请求中获取,租户ID

  1. package com.xaaef.molly.core.tenant;
  2. import cn.hutool.core.util.StrUtil;
  3. import com.xaaef.molly.common.util.JsonResult;
  4. import com.xaaef.molly.common.util.ServletUtils;
  5. import com.xaaef.molly.core.auth.jwt.JwtSecurityUtils;
  6. import com.xaaef.molly.core.tenant.service.MultiTenantManager;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import com.xaaef.molly.core.tenant.util.TenantUtils;
  10. import lombok.AllArgsConstructor;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.apache.commons.lang3.StringUtils;
  13. import org.jetbrains.annotations.NotNull;
  14. import org.springframework.stereotype.Component;
  15. import org.springframework.web.servlet.HandlerInterceptor;
  16. import static com.xaaef.molly.core.tenant.consts.MbpConst.X_TENANT_ID;
  17. /**
  18. * <p>
  19. * </p>
  20. *
  21. * @author WangChenChen
  22. * @version 1.1
  23. * @date 2022/11/15 11:41
  24. */
  25. @Slf4j
  26. @Component
  27. @AllArgsConstructor
  28. public class TenantIdInterceptor implements HandlerInterceptor {
  29. private final MultiTenantManager tenantManager;
  30. @Override
  31. public boolean preHandle(HttpServletRequest request,
  32. @NotNull HttpServletResponse response,
  33. @NotNull Object handler) throws Exception {
  34. /*
  35. * 从请求头中获取 如:
  36. * GET https://www.baidu.com/hello
  37. * x-tenant-id=master
  38. * */
  39. var tenantId = request.getHeader(X_TENANT_ID);
  40. if (StringUtils.isEmpty(tenantId)) {
  41. /*
  42. * 从URL地址中获取 如:
  43. * GET https://www.baidu.com/hello?x-tenant-id=master
  44. * */
  45. tenantId = request.getParameter(X_TENANT_ID);
  46. }
  47. if (StringUtils.isEmpty(tenantId)) {
  48. // 判断当前此请求,是否已经登录。
  49. if (JwtSecurityUtils.isAuthenticated()) {
  50. // 判断登录的用户类型。
  51. // 系统用户: 必须添加 租户ID.
  52. // 租户用户: 租户ID 在登录的时候,已经确定了
  53. if (JwtSecurityUtils.isMasterUser()) {
  54. return writeError(response);
  55. } else {
  56. TenantUtils.setTenantId(JwtSecurityUtils.getTenantId());
  57. tenantId = JwtSecurityUtils.getTenantId();
  58. }
  59. } else {
  60. return writeError(response);
  61. }
  62. }
  63. // 校验租户,是否存在系统中
  64. if (!tenantManager.existById(tenantId)) {
  65. var err = StrUtil.format("租户ID {} 不存在!", tenantId);
  66. ServletUtils.renderError(response, JsonResult.fail(err));
  67. return false;
  68. }
  69. return HandlerInterceptor.super.preHandle(request, response, handler);
  70. }
  71. private static boolean writeError(HttpServletResponse response) {
  72. var err = StrUtil.format("请求头或者URL参数中必须添加 {}", X_TENANT_ID);
  73. ServletUtils.renderError(response, JsonResult.fail(err));
  74. return false;
  75. }
  76. }

下面就是 核心中的核心了 mybatis-puls 拦截器

  1. package com.xaaef.molly.core.tenant.schema;
  2. import cn.hutool.core.collection.CollectionUtil;
  3. import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
  4. import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
  5. import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
  6. import com.xaaef.molly.core.auth.jwt.JwtSecurityUtils;
  7. import com.xaaef.molly.core.tenant.props.MultiTenantProperties;
  8. import com.xaaef.molly.core.tenant.util.TenantUtils;
  9. import lombok.AllArgsConstructor;
  10. import lombok.extern.slf4j.Slf4j;
  11. import net.sf.jsqlparser.JSQLParserException;
  12. import net.sf.jsqlparser.parser.CCJSqlParserUtil;
  13. import net.sf.jsqlparser.statement.Statements;
  14. import net.sf.jsqlparser.util.TablesNamesFinder;
  15. import org.apache.ibatis.executor.statement.StatementHandler;
  16. import org.springframework.stereotype.Component;
  17. import java.sql.Connection;
  18. import java.sql.SQLException;
  19. import java.util.Collection;
  20. import java.util.Optional;
  21. import java.util.Set;
  22. import java.util.stream.Collectors;
  23. import static com.xaaef.molly.core.tenant.consts.MbpConst.TENANT_IGNORE_TABLES;
  24. /**
  25. * <p>
  26. * </p>
  27. *
  28. * @author WangChenChen
  29. * @version 1.1
  30. * @date 2022/12/14 12:40
  31. */
  32. @Slf4j
  33. @Component
  34. @AllArgsConstructor
  35. public class SchemaInterceptor extends JsqlParserSupport implements InnerInterceptor {
  36. private final MultiTenantProperties props;
  37. @Override
  38. public void beforePrepare(StatementHandler sh, Connection conn, Integer transactionTimeout) {
  39. var mpBoundSql = PluginUtils.mpBoundSql(sh.getBoundSql());
  40. // 获取当前 sql 语句中的。表名称
  41. Set<String> tableName = getTableListName(mpBoundSql.sql());
  42. // 判断 表名称 是否需要过滤,即: 使用 公共库,而不是 租户 库。
  43. if (ignoreTable(tableName)) {
  44. // 切换数据库
  45. switchSchema(conn, props.getDefaultTenantId());
  46. } else {
  47. // 切换数据库
  48. switchSchema(conn, getCurrentTenantId());
  49. }
  50. InnerInterceptor.super.beforePrepare(sh, conn, transactionTimeout);
  51. }
  52. private String getCurrentTenantId() {
  53. // 判断当前此请求,是否已经登录。
  54. if (JwtSecurityUtils.isAuthenticated()) {
  55. // 判断登录的用户类型。
  56. // 系统用户: 可以操作任何一个 租户 的数据库。
  57. // 租户用户: 只能操作 所在租户 的数据库
  58. if (JwtSecurityUtils.isMasterUser()) {
  59. return TenantUtils.getTenantId();
  60. } else {
  61. return JwtSecurityUtils.getTenantId();
  62. }
  63. }
  64. return Optional.ofNullable(TenantUtils.getTenantId())
  65. .orElse(props.getDefaultTenantId());
  66. }
  67. private void switchSchema(Connection conn, String schema) {
  68. // PostgreSQL 和 SQL Server 可以使用 schema
  69. // conn.setSchema(schema);
  70. // 切换数据库
  71. String sql = String.format("use %s%s", props.getPrefix(), schema);
  72. try {
  73. conn.createStatement().execute(sql);
  74. } catch (SQLException e) {
  75. throw new RuntimeException(e);
  76. }
  77. }
  78. private final static TablesNamesFinder TABLES_NAMES_FINDER = new TablesNamesFinder();
  79. /**
  80. * 解析 sql 获取全部的 表名称
  81. */
  82. private static Set<String> getTableListName(String sql) {
  83. Statements statements = null;
  84. try {
  85. statements = CCJSqlParserUtil.parseStatements(sql);
  86. } catch (JSQLParserException e) {
  87. e.printStackTrace();
  88. return Set.of();
  89. }
  90. return statements.getStatements()
  91. .stream()
  92. .map(TABLES_NAMES_FINDER::getTableList)
  93. .flatMap(Collection::stream)
  94. .collect(Collectors.toSet());
  95. }
  96. /**
  97. * 过滤 公共的表。
  98. */
  99. private static boolean ignoreTable(Set<String> tableName) {
  100. return CollectionUtil.containsAny(TENANT_IGNORE_TABLES, tableName);
  101. }
  102. }

主要方法   

beforePrepare()  : 在 sql 语句执行前的  前置处理。根据PluginUtils.mpBoundSql(sh.getBoundSql());    获取当前执行的 sql 语句。

getTableListName() : 根据 sql 语句,获取要执行的表名称。然后根据表名,判断此表,是公共表,还是租户表。如:sys_config 这种表,肯定是所有租户公用的。而 user,role,之类,肯定是每个租户独有的!

switchSchema() : 根据数据库名称,切换数据库,因为 mysql 不支持 Schema 。所有只能用数据库替代。

getCurrentTenantId() : 获取当前登录的用户所在的租户ID。 当然也要判断用户类型。

如果是:系统用户,那么此用户就可以操作 所有的租户数据库。也就是说,可以随便切换到其他租户的数据库。

如果是:租户用户,那么此用户只能操作 “默认数据库” 和 “租户所属的数据库”,默认数据库中存放了一些公共数据,如:sys_config 。 

这个判断很简单, 就是租户id ,是不是默认的租户id、

  1. package com.xaaef.molly.core.tenant;
  2. import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
  3. import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
  4. import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  5. import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
  6. import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
  7. import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
  8. import com.xaaef.molly.common.util.JsonUtils;
  9. import com.xaaef.molly.core.auth.jwt.JwtSecurityUtils;
  10. import com.xaaef.molly.core.tenant.props.MultiTenantProperties;
  11. import com.xaaef.molly.core.tenant.schema.SchemaInterceptor;
  12. import lombok.AllArgsConstructor;
  13. import lombok.extern.slf4j.Slf4j;
  14. import org.apache.ibatis.reflection.MetaObject;
  15. import org.springframework.context.annotation.Bean;
  16. import org.springframework.context.annotation.Configuration;
  17. import org.springframework.stereotype.Component;
  18. import org.springframework.transaction.annotation.EnableTransactionManagement;
  19. import java.time.LocalDateTime;
  20. import static com.xaaef.molly.core.tenant.consts.MbpConst.*;
  21. /**
  22. * <p>
  23. * </p>
  24. *
  25. * @author Wang Chen Chen
  26. * @version 1.0
  27. * @date 2021/7/8 9:21
  28. */
  29. @Slf4j
  30. @Configuration
  31. @AllArgsConstructor
  32. @EnableTransactionManagement
  33. public class MybatisPlusConfig {
  34. private final MultiTenantProperties tenantProperties;
  35. /**
  36. * 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
  37. */
  38. private static final Long MAX_LIMIT = 100L;
  39. /**
  40. * 新的分页插件,一缓和二缓遵循mybatis的规则,
  41. * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
  42. * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
  43. */
  44. @Bean
  45. public MybatisPlusInterceptor paginationInterceptor() {
  46. // 设置 ObjectMapper
  47. JacksonTypeHandler.setObjectMapper(JsonUtils.getMapper());
  48. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  49. // 是否启用租户
  50. if (tenantProperties.getEnable()) {
  51. var schemaInterceptor = new SchemaInterceptor(tenantProperties);
  52. interceptor.addInnerInterceptor(schemaInterceptor);
  53. }
  54. //分页插件: PaginationInnerInterceptor
  55. PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
  56. paginationInnerInterceptor.setMaxLimit(MAX_LIMIT);
  57. //防止全表更新与删除插件: BlockAttackInnerInterceptor
  58. BlockAttackInnerInterceptor blockAttackInnerInterceptor = new BlockAttackInnerInterceptor();
  59. interceptor.addInnerInterceptor(paginationInnerInterceptor);
  60. interceptor.addInnerInterceptor(blockAttackInnerInterceptor);
  61. return interceptor;
  62. }
  63. }

到这里已经整合完成了。大概流程就是这样

1. web浏览器发送请求,请求头中携带 x-tenant-id 参数,用于区分是哪个租户

2.spring mvc 拦截器,拦截到租户id。保存到 ThreadLocal 中。

3.执行自己的业务

4.mybatis-plus 拦截器,拦截到业务中的 sql 语句,获取到 表名称 。判断 是否为公共表,

如果是:公共表 切换到 默认数据库,

如果是:租户的表,就再次判断当前登录的用户类型,

        如果是:系统用户,获取 ThreadLocal 中的租户id。切换到对应的数据库

        如果是:租户用户,直接切换到 所属的租户。租户用户,只能操作自己的库

系统用户登录

### 获取验证码
###  http://localhost:18891/auth/captcha/codes?codeKey=5jXzuwcoUzbtnHNh
GET {{baseUrl}}/auth/captcha/codes?codeKey=5jXzuwcoUzbtnHNh
x-tenant-id: master


### [master]密码模式登录
POST {{baseUrl}}/auth/login
Content-Type: application/json
x-tenant-id: master
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0

{
  "username": "admin",
  "password": "123456",
  "codeKey": "5jXzuwcoUzbtnHNh",
  "codeText": "uj57i"
}

> {%
client.global.set("tokenValue", response.body.data.access_token);
client.global.set("refreshToken", response.body.data.refresh_token);
%}
 

租户用户登录

### 获取验证码
###  http://localhost:18891/auth/captcha/codes?codeKey=5jXzuwcoUzbtnHNh
GET {{baseUrl}}/auth/captcha/codes?codeKey=applezrgegbtnHNrefh
x-tenant-id: apple


### [master]密码模式登录
POST {{baseUrl}}/auth/login
Content-Type: application/json
x-tenant-id: apple
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0

{
  "username": "apple",
  "password": "apple",
  "codeKey": "applezrgegbtnHNrefh",
  "codeText": "9xwbm"
}

> {%
client.global.set("tokenValue", response.body.data.access_token);
client.global.set("refreshToken", response.body.data.refresh_token);
%}

执行获取 默认租户的 角色列表

 执行获取 google租户的 角色列表

完整代码 Gitee

完整代码 Github



 

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号