基于Mybatis 的SAAS应用多租户数据逻辑隔离

package com.opencloud.common.interceptor;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.Properties; /**
*多租户数据逻辑隔离
* auth:breka
* time:2019-11-01
* 通过sql拦截机制实现多租户之间的数据逻辑隔离,需要先对数据表添加tentant_id(Long)字段以及需要添加逻辑删除的is_delete(tinyint)字段
* 需要在mybatis-config.xml文件里添加此插件,并设置需要隔离的表与需要设置逻辑删除的表
* <plugin interceptor="com.tianque.saas.platform.filter.MybatisSaasInterceptor">
* <!--租户数据隔离表-->
* <property name="tentant_filte_tables" value="|uc_sys_organization|uc_sys_position|uc_sys_role|uc_sys_user|"/>
* <!--逻辑删除数据表-->
* <property name="is_deleted_tables" value="|uc_sys_organization|uc_sys_position|uc_sys_role|uc_sys_user|"/>
* </plugin>
*/
@Component
@Intercepts({
@Signature(
type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class
})
})
public class MybatisSaasInterceptor implements Interceptor { public static final String SAAS_SQL_SPACE = " ";
public static final String SAAS_SQL_SPACE_EQUES_SPACE = " = ";
public static final String SAAS_SQL_EQUS = "=";
public static final String SAAS_SQL_SPACE_DOT_SPACE = " , ";
public static final String SAAS_SQL_DOT = ",";
public static final String SAAS_SQL_GOT_N="\n";
public static final String SAAS_SQL_GOT_T="\t";
public static final String SAAS_SQL_COL="|";
public static final String SAAS_SQL_SELECT = "select";
public static final String SAAS_SQL_FROM = "from";
public static final String SAAS_SQL_JOIN = "join";
public static final String SAAS_SQL_WHERE = "where";
public static final String SAAS_SQL_WHERE_SPACE = "where ";
public static final String SAAS_SQL_RIGNT_SIGN = ")";
public static final String SAAS_SQL_SPACE_RIGNT_SIGN = " )";
public static final String SAAS_SQL_LEFT_SIGN = "(";
public static final String SAAS_SQL_LEFT_SIGN_SPACE = "( ";
public static final String SAAS_SQL_INSERT = "insert";
public static final String SAAS_SQL_UPDATE = "update";
public static final String SAAS_SQL_UPDATE_SPACE = "update ";
public static final String SAAS_SQL_CREATE_USER = "create_user";
public static final String SAAS_SQL_CREATE_USER_DOT = "create_user,";
public static final String SAAS_SQL_CREATE_DATE = "create_date";
public static final String SAAS_SQL_CREATE_DATE_DOT = "create_date,";
public static final String SAAS_SQL_UPDATE_USER = "update_user";
public static final String SAAS_SQL_UPDATE_DATE = "update_date";
public static final String SAAS_SQL_DELETE = "delete";
public static final String SAAS_SQL_TENTANT_ID = "tentant_id";
public static final String SAAS_SQL_TENTANT_ID_EQUS = "tentant_id=";
public static final String SAAS_SQL_SAPCE_TENTANT_ID_EQUS = " tentant_id=";
public static final String SAAS_SQL_TENTANT_ID_DOT = "tentant_id,";
public static final String SAAS_SQL_DOT_TENTANT_ID_EQUS = ".tentant_id=";
public static final String SAAS_SQL_SPACE_AND_SPACE_TENTANT_ID_EQUS = " and tentant_id=";
public static final String SAAS_SQL_DOT_IS_DELETED_EQUS_ZERO_AND_SPACE = ".is_deleted=0 and ";
public static final String SAAS_SQL_IS_DELETED_EQUS_ZERO_AND_SPACE = "is_deleted=0 and ";
public static final String SAAS_SQL_AS = "as";
public static final String SAAS_SQL_SPACE_SET_SPACE = " set ";
public static final String SAAS_SQL_VALUES = "values";
public static final String SAAS_SQL_SPACE_AND_SPANCE = " and ";
public static final String SAAS_SQL_UPDATE_USER_EQUS_TAG = "update_user='";
public static final String SAAS_SQL_UPDATE_DATE_EQUS_NOW_SIGN_DOT = "update_date=now(),";
public static final String SAAS_SQL_NOW_SIGN_DOT = "now(),";
public static final String SAAS_SQL_SEMEGENT_ONE="',update_date=now() where";
public static final String SAAS_SQL_SEMEGENT_TOW=" set is_deleted=1,update_user='";
public static final String SAAS_SQL_SQL="sql";
public static final String SAAS_SQL_DELEGATE="delegate.mappedStatement"; private static String tentant_filte_tables=""; //配置租户表
private static String is_deleted_tables=""; //配置逻辑删除表 @Override
public Object intercept(Invocation invocation) throws Throwable {
String userName="";//当前登入会话用户名
Long tentantId=0l;//当前登入会话租户Id //当前会议用户的租户id附值与用户名附值
// if(ThreadVariable.getSession()!=null&&ThreadVariable.getSession().getTentantId()>0)
// {
// userName=ThreadVariable.getSession().getUserName();
// tentantId=ThreadVariable.getSession().getTentantId();
// } StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
//先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(SAAS_SQL_DELEGATE);
//id为执行的mapper方法的全路径名,如com.uv.dao.UserMapper.insertUser
String id = mappedStatement.getId();
//sql语句类型 select、delete、insert、update
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
BoundSql boundSql = statementHandler.getBoundSql(); //获取到原始sql语句
String sql = boundSql.getSql();
//得到租户数据隔离后台的Sql
String newSql=this.getSaasSql(sql,tentantId,userName); //通过反射修改sql语句
Field field = boundSql.getClass().getDeclaredField(SAAS_SQL_SQL);
field.setAccessible(true);
field.set(boundSql, newSql);
return invocation.proceed();
} @Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
} } @Override
public void setProperties(Properties properties) {
//初始化租户数据隔离表与逻辑删除表
tentant_filte_tables = (String) properties.get("tentant_filte_tables");
is_deleted_tables = (String) properties.get("is_deleted_tables");
System.out.println("tentant_filte_tables:" + tentant_filte_tables + " is_deleted_tables:" + is_deleted_tables);
} public int tentant_filte_tables_indexof(String tableName)
{
return tentant_filte_tables.indexOf(SAAS_SQL_COL+tableName+SAAS_SQL_COL);
}
private int is_deleted_tables_indexof(String tableName)
{
return is_deleted_tables.indexOf(SAAS_SQL_COL+tableName+SAAS_SQL_COL);
}
private String getSaasSql(String sql,Long tentantId,String userName)
{
sql=sql.toLowerCase().trim();
sql=sql.replace(SAAS_SQL_GOT_N,SAAS_SQL_SPACE)
.replace(SAAS_SQL_GOT_T,SAAS_SQL_SPACE)
.replace(SAAS_SQL_EQUS,SAAS_SQL_SPACE_EQUES_SPACE)
.replace(SAAS_SQL_DOT,SAAS_SQL_SPACE_DOT_SPACE)
.replace(SAAS_SQL_LEFT_SIGN_SPACE,SAAS_SQL_LEFT_SIGN)
.replace(SAAS_SQL_SPACE_RIGNT_SIGN,SAAS_SQL_RIGNT_SIGN)
.replaceAll(" +",SAAS_SQL_SPACE); String newSql = sql;
String[] arrSql=sql.split(SAAS_SQL_SPACE);
String sqlCommandType =arrSql[0];
if(sqlCommandType.equals(SAAS_SQL_SELECT))
{
//region 查询SQL 租户与逻辑删除表替换
for(int i=1;i<arrSql.length;i++)
{
if(arrSql[i-1].equals(SAAS_SQL_FROM)||arrSql[i-1].equals(SAAS_SQL_JOIN))
{
//from where
String tableName=arrSql[i];
String smallName="";
if(tableName.indexOf(SAAS_SQL_LEFT_SIGN)==0)
continue; if(tentant_filte_tables_indexof(tableName)>-1||is_deleted_tables_indexof(tableName)>-1)
{
String strWhere="";
//region单表查询
if(arrSql[i+1].equals(SAAS_SQL_WHERE))
{
if(tentant_filte_tables.indexOf(tableName)>-1)
strWhere+=SAAS_SQL_TENTANT_ID_EQUS+tentantId+SAAS_SQL_SPACE_AND_SPANCE;
if(is_deleted_tables.indexOf(tableName)>-1)
strWhere+=SAAS_SQL_IS_DELETED_EQUS_ZERO_AND_SPACE;
arrSql[i+1]=SAAS_SQL_WHERE_SPACE+strWhere;
i++;
continue;
}
if(arrSql[i+2].equals(SAAS_SQL_WHERE))
{
smallName=arrSql[i+1];
if(tentant_filte_tables.indexOf(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_TENTANT_ID_EQUS+tentantId+SAAS_SQL_SPACE_AND_SPANCE;
if(is_deleted_tables.indexOf(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_IS_DELETED_EQUS_ZERO_AND_SPACE;
arrSql[i+2]=SAAS_SQL_WHERE_SPACE+strWhere;
i=i+2;
continue;
}
//endregion //region多表查询
if(arrSql[i+1].equals(SAAS_SQL_AS))
i=i+1;
smallName=arrSql[i+1];
if(tentant_filte_tables_indexof(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_TENTANT_ID_EQUS+tentantId+SAAS_SQL_SPACE_AND_SPANCE;
if(is_deleted_tables_indexof(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_IS_DELETED_EQUS_ZERO_AND_SPACE;
if(arrSql[i+2].equals(SAAS_SQL_DOT))
{
//region多表查询3表
i=i+3;
tableName=arrSql[i];
if(tentant_filte_tables_indexof(tableName)>-1||is_deleted_tables_indexof(tableName)>-1) {
if(arrSql[i+1].equals(SAAS_SQL_AS))
i=i+1;
smallName=arrSql[i+1];
if(tentant_filte_tables_indexof(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_TENTANT_ID_EQUS+tentantId+SAAS_SQL_SPACE_AND_SPANCE;
if(is_deleted_tables_indexof(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_IS_DELETED_EQUS_ZERO_AND_SPACE;
}
if(arrSql[i+2].equals(SAAS_SQL_DOT))
{
//多表查询3表
i=i+3;
tableName=arrSql[i];
if(tentant_filte_tables_indexof(tableName)>-1||is_deleted_tables_indexof(tableName)>-1) {
if(arrSql[i+1].equals(SAAS_SQL_AS))
i=i+1;
smallName=arrSql[i+1];
if(tentant_filte_tables_indexof(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_TENTANT_ID_EQUS+tentantId+SAAS_SQL_SPACE_AND_SPANCE;
if(is_deleted_tables_indexof(tableName)>-1)
strWhere+=smallName+SAAS_SQL_DOT_IS_DELETED_EQUS_ZERO_AND_SPACE;
}
}
//endregion
}
//endregion
for(int j=i;j<arrSql.length;j++)
{
if(arrSql[j].indexOf(SAAS_SQL_WHERE)==0)
{
arrSql[j]=SAAS_SQL_WHERE_SPACE+strWhere +SAAS_SQL_SPACE+arrSql[j].replace(SAAS_SQL_WHERE,SAAS_SQL_SPACE);
}
}
}
}
}
newSql= StringUtils.join(arrSql, SAAS_SQL_SPACE);
//endregion
}
else if(sqlCommandType.equals(SAAS_SQL_INSERT))
{
//region 新境SQL 租户表与添加人添加时间逻辑, 非查询插入
if(sql.indexOf(SAAS_SQL_SELECT)==-1) {
String strTags = "";
String strValues = "";
if (sql.indexOf(SAAS_SQL_CREATE_USER) == -1) {
//默认添加创建人
strTags += SAAS_SQL_CREATE_USER_DOT;
strValues += "'" + userName + "',";
}
if (sql.indexOf(SAAS_SQL_CREATE_DATE) == -1) {
//默认添加创建时间
strTags += SAAS_SQL_CREATE_DATE_DOT;
strValues += SAAS_SQL_NOW_SIGN_DOT;
}
String tableName = arrSql[2]; //当前表名
if (tentant_filte_tables_indexof(tableName) > -1) {
//是租户过滤表,当前没有添加租房Id插入则进行添加
if (sql.indexOf(SAAS_SQL_TENTANT_ID) == -1) {
strTags += SAAS_SQL_TENTANT_ID_DOT;
strValues += tentantId + SAAS_SQL_DOT;
}
}
arrSql[3] = SAAS_SQL_LEFT_SIGN + strTags + arrSql[3].replace(SAAS_SQL_LEFT_SIGN, "");
for (int i = 1; i < arrSql.length; i++) {
if (arrSql[i].indexOf(SAAS_SQL_LEFT_SIGN) == -1)
continue;
;
if (arrSql[i - 1].equals(SAAS_SQL_VALUES)) {
arrSql[i] = SAAS_SQL_LEFT_SIGN + strValues + arrSql[i].replace(SAAS_SQL_LEFT_SIGN, "");
}
//支持批量插入
if (arrSql[i - 1].equals(SAAS_SQL_DOT) && arrSql[i - 2].indexOf(SAAS_SQL_RIGNT_SIGN) > -1) {
arrSql[i] = SAAS_SQL_LEFT_SIGN + strValues + arrSql[i].replace(SAAS_SQL_LEFT_SIGN, "");
}
}
}
newSql=StringUtils.join(arrSql, SAAS_SQL_SPACE);
//endregion
}
else if(sqlCommandType.equals(SAAS_SQL_UPDATE))
{
//region 更新SQL,租户表与更新人、更新时间替换
String strUpdate=SAAS_SQL_SPACE;
if(sql.indexOf(SAAS_SQL_UPDATE_USER)==-1)
{
if(strUpdate.length()>1)
strUpdate+=SAAS_SQL_DOT;
strUpdate+=SAAS_SQL_UPDATE_USER_EQUS_TAG+userName+"'";//默认添加修改人
}
if(sql.indexOf(SAAS_SQL_UPDATE_DATE)==-1)
{
if(strUpdate.length()>1)
strUpdate+=SAAS_SQL_DOT;
strUpdate+=SAAS_SQL_UPDATE_DATE_EQUS_NOW_SIGN_DOT;//默认添加修改时间
}
if(strUpdate.length()>1)
{
int indexSet=sql.indexOf(SAAS_SQL_SPACE_SET_SPACE);
newSql=sql.replace(SAAS_SQL_SPACE_SET_SPACE,SAAS_SQL_SPACE_SET_SPACE+strUpdate);
} for(int i=1;i<arrSql.length;i++)
{
if(arrSql[i-1].equals(SAAS_SQL_UPDATE))
{
String tableName=arrSql[i]; //当前表名
if(tentant_filte_tables_indexof(tableName)>-1)
{
//是租户过滤表,更新条件中没有添加租户ID条件限制,则添加用户ID条件限制
if(sql.indexOf(SAAS_SQL_TENTANT_ID)==-1)
{
newSql+=SAAS_SQL_SPACE_AND_SPACE_TENTANT_ID_EQUS+tentantId;
break;
}
}
}
}
//endregion
}
else if(sqlCommandType.equals(SAAS_SQL_DELETE))
{
//region 删除SQL 租户表与逻辑删除表逻辑替换;
for(int i=1;i<arrSql.length;i++)
{
if(arrSql[i-1].equals(SAAS_SQL_FROM))
{
String tableName=arrSql[i]; //当前表名
if(is_deleted_tables_indexof(tableName)>-1)
{
//改成逻辑删除
newSql=SAAS_SQL_UPDATE_SPACE+tableName+SAAS_SQL_SEMEGENT_TOW+userName+SAAS_SQL_SEMEGENT_ONE;
if(tentant_filte_tables_indexof(tableName)>-1)
newSql+=SAAS_SQL_SAPCE_TENTANT_ID_EQUS+tentantId+SAAS_SQL_SPACE_AND_SPANCE;//租户表
int indexWhere=sql.indexOf(SAAS_SQL_WHERE)+5;
newSql+=sql.substring(indexWhere,sql.length());
break;
}
else
{
//租户物理删除表
if(tentant_filte_tables_indexof(tableName)>-1)
{
if(sql.indexOf(SAAS_SQL_TENTANT_ID)==-1)
{
newSql=sql+SAAS_SQL_SPACE_AND_SPACE_TENTANT_ID_EQUS+tentantId;//添加租户ID条件限制
break;
}
}
}
}
}
//endregion
}
newSql=newSql.replace(SAAS_SQL_SPACE_DOT_SPACE,SAAS_SQL_DOT)
.replace(SAAS_SQL_SPACE_EQUES_SPACE,SAAS_SQL_EQUS)
.replaceAll(" +",SAAS_SQL_SPACE);
System.out.println("---------当前租户:"+ tentantId +"---------"+newSql);
return newSql;
} }

SAAS多租户数据逻辑隔离的更多相关文章

  1. saas系统多租户数据隔离的实现(一)数据隔离方案

    0. 前言 前几天跟朋友聚会的时候,朋友说他们公司准备自己搞一套saas系统,以实现多个第三方平台的业务接入需求.聊完以后,实在手痒难耐,于是花了两天时间自己实现了两个saas系统多租户数据隔离实现方 ...

  2. 数据层的多租户浅谈(SAAS多租户数据库设计)

    在上一篇“浅析多租户在 Java 平台和某些 PaaS 上的实现”中我们谈到了应用层面的多租户架构,涉及到 PaaS.JVM.OS 等,与之相应的是数据层也有多租户的支持. 数据层的多租户综述 多租户 ...

  3. SaaS多租户模式数据存储方案

    云计算多租户几乎用于所有软件即服务 (Software as a Service, SaaS) 应用程序,因为计算资源是可伸缩的,而且这些资源的分配由实际使用决定.话虽如此,用户可以通过 Intern ...

  4. [转载]数据层的多租户浅谈(SAAS多租户数据库设计)

    原文:http://www.ibm.com/developerworks/cn/java/j-lo-dataMultitenant/index.html 在上一篇“浅析多租户在 Java 平台和某些 ...

  5. SaaS多租户模式数据存储方案比较

    云计算多租户几乎用于所有软件即服务 (Software as a Service, SaaS) 应用程序,因为计算资源是可伸缩的,而且这些资源的分配由实际使用决定.话虽如此,用户可以通过 Intern ...

  6. EEPlat PaaS中的多租户数据隔离模式

    EEPlat PaaS支持三种租户的数据隔离技术:Sparce Column.tenantId字段隔离.每一个租户独立数据库. 1)Sparce Column,和Salesforce Appforce ...

  7. JeeSite 4.x SAAS 多租户技术设计方案

    SaaS 是 Software-as-a-Service(软件即服务)的简称,从技术角度上可称之为 “多租户技术或称多重租赁技术”.它与 “按需软件.应用服务提供商.托管软件” 所具有相似的含义.它是 ...

  8. 实现saas多租户方案比较

    看到一篇比较多租户数据隔离方案的文章,总结挺不错.其实大部分内容在我前几年写的文章都有. 文章翻译自: https://blog.arkency.com/comparison-of-approache ...

  9. SAAS云平台搭建札记: (一) 浅论SAAS多租户自助云服务平台的产品、服务和订单

    最近在做一个多租户的云SAAS软件自助服务平台,途中遇到很多问题,我会将一些心得.体会逐渐分享出来,和大家一起探讨.这是本系列的第一篇文章. 大家知道,要做一个全自助服务的SAAS云平台是比较复杂的, ...

随机推荐

  1. linux运维、架构之路-redis

    一.redis介绍 Redis是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的Web应用程序. Redis属于非关系型数据库和Memcached类似,redis也是一个key- ...

  2. CF1051F The Shortest Statement Dijkstra + 性质分析

    动态询问连通图任意两点间最短路,单次询问. 显然,肯定有一些巧妙地性质(不然你就发明了新的最短路算法了233)有一点很奇怪:边数最多只比点数多 $20$ 个,那么就可以将这个图看作是一个生成树,上面连 ...

  3. POJ 1743 Musical Theme ( 后缀数组 && 最长不重叠相似子串 )

    题意 : 给 n 个数组成的串,求是否有多个“相似”且不重叠的子串的长度大于等于5,两个子串相似当且仅当长度相等且每一位的数字差都相等. 分析 :  根据题目对于 “ 相似 ” 串的定义,我们可以将原 ...

  4. [CSP-S模拟测试]:砍树(数学+模拟)

    题目传送门(内部题1) 输入格式 第一行两个整数$n$,$k$,代表树苗的数量和最大看书的总长度.第二行n个整数$a_i$,代表林先森希望每棵树苗的最终高度. 输出格式 一行一个整数,代表最大可能的d ...

  5. 关于Java协变性的思考

    简而言之,如果A IS-A B,那么A[] IS-A B[]. 举例:现在有类型Person.Employee和Student.Employee 是一个(IS-A) Person,Student是一个 ...

  6. mysqldump常用使用

    1:导出数据库sakila的表结构mysqldump -uroot -ppwd -d sakila > /data/tmp/sakila.sql; 2:导出数据库sakila下表employee ...

  7. vmware 虚拟 CPU 性能计数器事件与主机 CPU 不兼容。

    错误提示:虚拟 CPU 性能计数器事件与主机 CPU 不兼容. 点放弃 如果是暂时状态选关机, 丢失掉运行状态,即可保留硬盘内容重新开机

  8. 词频分析 评论标签 nltp APP-分析买家评论的评分-高频词:二维关系

    0-定评论结果:好评.差评,1星.4星,二元化为“积极.消极”,取一元的数据为样本 1-得到词频结果:如手机类的“积极样本”得到前10的高频词:运行(run running ran).内存(memor ...

  9. 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_09 序列化流_5_InvalidClassException异常_原理

    序列化一遍 反序列化再来一遍 age的修饰符改为public 直接反序列化就会抛出异常 序列化的时候会给Person.class添加序列号,serialVersionUID,.反序列化需要对比这个se ...

  10. 通过命令直接修改jar包中的静态文件

    1.先将要修改的jar包备份 copy xxx.jar xxx.jar_bak 2.建立一个新的目录便于后面的打包 mkdir jar_tmp 3.将包放到刚刚创建的目录里解压 mv xxx.jar ...