公司是做saas应用的,意味着会有多个客户购买我们的应用后,我们会通过系统的超级管理员账号在后台给客户生成一个租户管理员的账号,一个账号对应一个id。 多个客户之间数据是不共享的,只能查到自己所在公司下的数据,来达到数据隔离的目的。
当时项目架构搭建的时候使用了Mybatis-plus代替Myabtis,便使用了mp提供的多租户拦截器进行数据隔离。
1.引入mp的jar包
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus-boot-starter.version}
jsqlparser
com.github.jsqlparser
`
2.配置application.yml,把我们的一些自定义的租户配置配好
#多租户的字段
jxc.tenant.column=tenant_id
#超管账号(超管不进行数据隔离,允许查看所有数据,多个账号可以使用,号隔开)
jxc.tenant.admin=admin
#租户表(目前没有使用到这个配置)
jxc.tenant.tables=jsh_depothead,jsh_depotitem,jsh_material,jsh_materialcategory
#系统表,也就是一些不需要进行多租户隔离的表,比如共享菜单,共享的中间表等。
jxc.tenant.sys-tables=databasechangelog,databasechangeloglock,\
jsh_tenant,jsh_functions,tbl_sequence,jsh_userbusiness,jsh_app,dual,jsh_systemconfig
#自定义sql,这些特定的sql也不进行租户隔离,比如新增用户时校验用户名是否存在等。
jxc.tenant.sql-filter=com.jsh.erp.datasource.mappers.UserMapperEx.selectByConditionForIsExists,\
com.jsh.erp.datasource.mappers.UserMapperEx.selectUserForLogin,\
com.jsh.erp.datasource.mappers.MaterialPropertyMapperEx.initialMaterialProperty
3.进行多租户的java配置(这里有四个配置类,为了方便我贴在一起),顺便提一下,这里我做是否登录验证是通过redis取的,这里要根据自己项目的架构进行调整
@Getter
@Setter
@ConfigurationProperties(prefix = "jxc.tenant")
public class JxcTenantProperties {
/**
* 多租户字段名称
*/
private String column;
/**
* 最高权限人员登陆名称
*/
private List<String> admin = new ArrayList<>();
/**
* 多租户数据表
*/
private List<String> tables = new ArrayList<>();
/**
* 多租户系统数据表
*/
private List<String> sysTables = new ArrayList<>();
/**
* 需要过滤的sql
*/
private List<String> sqlFilter = new ArrayList<>();
}
@Slf4j
@AllArgsConstructor
public class JxcTenantHandler implements TenantHandler {
private final JxcTenantProperties properties;
/**
* 获取租户ID
*
* @return 租户ID
*/
@Override
public Expression getTenantId() {
// 获取租户ID
Map<String, Object> userMap = WebUtil.transformationToken();
if (userMap != null) {
return new LongValue((String) userMap.get(CommonConstant.REQ_HEADER_TENANT_ID));
}
return null;
}
/**
* 获取租户字段名称
*
* @return 租户字段名称
*/
@Override
public String getTenantIdColumn() {
return properties.getColumn();
}
/**
* 过滤租户表
*
* @param tableName 表名
* @return 是否进行过滤
*/
@Override
public boolean doTableFilter(String tableName) {
Map<String, Object> userMap = WebUtil.transformationToken();
//没有登陆不用拦截
if (userMap == null) {
return true;
}
//管理员不用配置
String loginName = (String) userMap.get(CommonConstant.REQ_HEADER_LOGIN_NAME);
if (properties.getAdmin().contains(loginName)) {
return true;
}
// 这里是租户相关的表
// boolean isTenantTable = properties.getTables().size() > 0 && properties.getTables().contains(tableName);
// boolean b = (isTenantTable || contains) && userMap.get(CommonConstant.REQ_HEADER_TENANT_ID) !=null;
//不是系统表和有租户ID的拦截
boolean contains = properties.getSysTables().contains(tableName);
boolean aNull = Objects.isNull(userMap.get(CommonConstant.REQ_HEADER_TENANT_ID));
return (contains || aNull);
}
}
@AllArgsConstructor
public class JxcTenantSqlParserFilter implements ISqlParserFilter {
private final JxcTenantProperties properties;
@Override
public boolean doFilter(MetaObject metaObject) {
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
// 过滤自定义查询此时无租户信息约束出现
//if ("com.jsh.erp.datasource.mappers.DepotHeadMapperEx.getBuildOnlyNumber".equals(ms.getId())) {
// return true;
//}
//此sql不作租户SQL拦截
return properties.getSqlFilter().contains(ms.getId());
}
}
@Configuration
@AllArgsConstructor
@AutoConfigureBefore(MybatisConfiguration.class)
@EnableConfigurationProperties(JxcTenantProperties.class)
public class JxcTenantConfiguration {
/**
* 多租户配置类
*/
private final JxcTenantProperties properties;
/**
* 自定义租户处理器
*
* @return TenantHandler
*/
@Bean
@ConditionalOnMissingBean(TenantHandler.class)
public TenantHandler bladeTenantHandler() {
return new JxcTenantHandler(properties);
}
/**
* 分页插件
*
* @param tenantHandler 自定义租户处理器
* @return PaginationInterceptor
*/
@Bean
public PaginationInterceptor paginationInterceptor(TenantHandler tenantHandler) {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
TenantSqlParser tenantSqlParser = new TenantSqlParser();
tenantSqlParser.setTenantHandler(tenantHandler);
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
paginationInterceptor.setSqlParserFilter(new JxcTenantSqlParserFilter(properties));
return paginationInterceptor;
}
}
4.配置好之后就可以让mp去拦截sql,进行sql拼接了。
mp会在改、删 的操作时,将tenant_id到where条件后,tenant_id的值 需要你用自己的逻辑去取。
新增的时候会根据当前的tenant_id,自动拼接然后insert到数据库中。
查询的时候会将tenant_id拼接到where条件后面。