目录
承前
概念概述
权限概述
数据权限要素
操作控制
规则定义
方案选型
方案一:子查询:
方案二:手动添加Sql控制
方案三:借助视图
方案四:Sql解析器
比对选型
条件视图实现
Mybatis访问
添加配置
DB侧
性能问题
Sql解析器实现
支持场景
资源定义
操作控制
DruidSql解析器逻辑
Join
Union
Where
DruidSqlParser
Mybatis拦截器代码
总结
承前
如果一个表根据tenantid区分不同公司的数据,那么就需要在写Sql脚本的时候添加一个where tenantid = ** 的条件,用来区分。如下面表:
tenantid | id | name | sex | age |
1001 | 1 | 张三1 | 男 | 18 |
1001 | 2 | 张三2 | 男 | 19 |
1001 | 3 | 张三3 | 男 | 20 |
1001 | 4 | 张三4 | 男 | 21 |
1001 | 5 | 张三5 | 男 | 22 |
1002 | 6 | 张三6 | 男 | 23 |
1002 | 7 | 张三7 | 男 | 24 |
1002 | 8 | 张三8 | 男 | 25 |
1002 | 9 | 张三9 | 男 | 26 |
1002 | 10 | 张三10 | 男 | 27 |
概念概述
权限概述
权限就是某个人对某资源是否可以进行某种操作。
权限分为功能权限和数据权限。
功能权限:表象就是:角色,权限,菜单,登录,认证
数据权限:指对系统用户进行数据资源可见性的控制,比如:某部门的HR只能看到某部门的员工
数据权限要素
从当前登录用户的角度来说,数据权限的定义可以解释为:`当前登录的用户只能看到该用户权限范围内的数据资源`。由此可以分析出数据权限控制中几个关键要素:
1. 主体,即当前登录用户。领导、角色等概念可翻译为当前登录用户是否是领导,是否拥有某角色。
2. 数据资源。即受管控系统数据。
3. 条件规则。即当前登录用户对于某特定的数据资源适用的条件。
主体:主体是人,实际使用呢,中间会添加一个角色。 当然也可以直接关联到人。
资源:数据资源包括DB中的数据,也包括File。如果File已经在DB中映射记录,那么全部可以理解为DB中的数据。
资源的定义:DB中的表很多,需要从业务的角度分析,控制的资源的类型,以及从哪些维度控制,权限值是啥。比如:按照部门控制员工的访问。员工就是资源类型,部门就是控制的一个维度,可以理解为表中的一个字段,这个字段的取值需要是固定的,或者是相对固定的,这个固定的字段值,就是权限值。
条件规则:即用户(角色) 可以访问的 资源+维度+权限值 的映射关系
操作控制
按说操作是功能权限,但为了防止访问的横向扩展。
比如:我是A部门的HR,具有A部分的员工CRUD权限。 URI是可以通过的。 insert的时候 我传的 部门ID 是B。
为了控制这种情况,需要对insert,update,delete 的条件和数值进行校验,融合select进行的控制,可以分为以下三种情况:
1. 只控制条件:数据查询和删除权限 Select、Delete
- 将具有的权限添加到where条件中
2. 只控制数值:数据新增权限 insert
- 控制values中对应的字段,比如部门字段值是否是A
3. 上述两种的融合,即控制条件也控制值,数据更新权限 update
- 控制update的条件,是为了防止更新了其他部门的数据
- 控制values中的值,是为了防止将A部门更新成B部门
规则定义
1. 对逻辑关系的扩展:上述只能and场景,也可以添加一些or场景
2. 对权限值的扩展:比如我可以访问性李的员工, 权限值: 李*
当然 规则定义完成后,需要在实现的时候兼容即可。
方案选型
数据权限 的控制 就是从Sql的角度来控制,控制执行的Sql中是权限内的。
方案一:子查询:
有控制的就替换为子查询
Mybatis拦截器,拦截访问的Sql
框架角度解决,可以解决手写Sql不足
不足:子查询会创建零时表,在数据量大时DB会直接挂掉
优点:不会对业务代码进行入侵;规则可以灵活;动态增加资源类型和维度而不需要修改代码。
方案二:手动添加Sql控制
实现简单
改动点多,数据权限扩展 影响很大
方案三:借助视图
1.动态视图,动态创建视图
2.条件视图,实现创建的视图条件值是一个函数,视图使用MERGE模式(默认算法,不创建中间表),不足:权限值只能有一个,如果新增条件,需要更新视图;好处是:可以从DB权限进行控制,只给视图的权限。
A:视图使用函数传值,函数通过存储过程传值
B:存储过程传值可以放在Mybatis的拦截器中做
3.视图&中间表,函数只可以返回一个值,没法做到in场景,可以创建一个中间表,根视图进行join 进行控制
方案四:Sql解析器
Mybatis拦截器,拦截访问的Sql
使用Sql解析器解析Sql语句,通过规则的比对,添加On,where条件,或者 校验insert 和update 的值的合法性
不足:多了一次解析Sql的逻辑;编码复杂;
优点:不会对业务代码进行入侵;规则可以灵活;动态增加资源类型和维度而不需要修改代码。
比对选型
1. 如果可以接受中间表,可以选方案1
2. 如果团队技术能力一般,业务也比较简单,控制的地方不多,也没变更的需求,方案2
3. 如果业务固定,数据库控制严格,比如说要给第三方直接看数据库,这种方案也可以考虑,建议用条件视图;动态创建视图权限太大是个问题;借助中间表,高并发是问题。
4. 如果并发量不高,业务复杂,且需要灵活配置,适应需求的频繁变更,团队技术能力还可以,方案4
条件视图实现
Mybatis访问
<mapper namespace="com.glodon.gcms.admin.dao.BusinessUDMapper">
<select id="list1" resultType="com.glodon.gcms.admin.dbsupportor.entity.Table1"
parameterType="int">
call test_sp(#{tenant});
SELECT * from test_view;
</select>
</mapper>
添加配置
jdbc:mysql://192.168.118.162:3306/test_wq?museUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
必须:在mybatis同一个方法中执行多个SQL语句实现方法:
1、修改数据库连接参数加上allowMultiQueries=true,如:
hikariConfig.security.jdbcUrl=jdbc:mysql://xx.xx.xx:3306/xxxxx?characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&allowMultiQueries=true
2、直接写多条语句,用“;”隔开即可
<delete id="delete" parameterType="upc.cbs.HtxxlrEntity">
delete from PC_CBS_CONTRACT where contract_id = #{contract_id};
delete from PC_CBS_UPLOAD_FILES where contract_id = #{contract_id} and filetype='合同附件';
delete from PC_CBS_CONTRACT_TEAM where contract_id = #{contract_id};
</delete>
mysql需要修改数据连接添加allowMultiQueries=true属性;每个sql语句用“;”分开
DB侧
-- 创建存储过程
-- 作用:给变量赋值
drop procedure if exists test_sp;
create procedure test_sp(
in tenantcode int(11)
)
BEGIN
set @tenantcode = tenantcode;
-- SELECT * from test_view3;
END;
-- 创建方法
-- 作为视图的变量
create FUNCTION test_f()
returns int(11)
BEGIN
return @tenantcode;
END;
-- 创建视图
-- 使用函数作为变量
create view test_view
AS
SELECT * from table1 where tenant = test_f();
-- 调用过程
set autocommit=0;
START TRANSACTION;
call test_sp(22);
SELECT * from test_view;
COMMIT;
-- 调用过程
call test_sp(22);
SELECT * from test_view;
性能问题
MySQL在处理视图时有两种算法,分别称为MERGE和TEMPTABLE。
在执行"CREATE VIEW"语句时可以指定使用哪种算法。不显现指定的话,Mysql默认使用Merge算法。
MERGE,将视图sql合并到主查询sql中,重新构成新sql进行查询。网上有个大侠说的挺好“有点类似于C语言中的宏展开”
TEMPTABLE, 就是将视图单作临时表来处理。
Sql解析器实现
支持场景
支持场景:
1. ([销售地区] [等于] [上海]) or ([销售地区] [等于] [北京])
2. 不支持 ([销售地区] [等于] [上海]) or ([货物类型] [等于] [3C]) -- 看看这种场景是否需要 支持
资源定义
数据权限包括:资源定义 + 操作控制
数据定义:
数据结构:<资源类型,<维度,取值>>
示例:<table1,<name,11>>
含义:只允许操作 table1中name=11的字段
操作控制
操作控制:
1. 查询权限: 如果查table1,只能查name=11的数据;
- 涉及Select和Delete
- 会添加where条件,如果select有join,会添加On条件
2. 添加权限:如果给table1添加数据,name值只能是11
- 涉及:insert
- 操作:会检查value中对应的值,如果name不是11,则抛出异常;
3. 更新权限: 如果要更新table1表,只能更新name=11的数据,且values中name对应的值只能是11
- 涉及:update
- 相当于是select+insert并集,即检查values中数据的合法性;又添加where条件。
DruidSql解析器逻辑
Join
给jsonsql语句添加ON 条件
示例:
FROM
table1 t1
LEFT JOIN table2 t2 ON t1.tenant = t2.tenant
LEFT JOIN table3 t3 ON t2.tenant = t3.tenant
数据结构:
SQLJoinTableSource
/ \
SQLJoinTableSource SQLExprTableSource(table3)
/ \
SQLExprTableSource(table3) SQLExprTableSource(table2)
Union
如果是union查询曾需要处理左右两棵树,每棵树都是一个SQLSelectQuery节点
叶子节点是简单的SQLSelectQueryBlock查询节点
处理完成后,需要处理where条件,同样where条件 也是一棵树,也可能包含子查询,子查询又是一个SQLSelectQuery
Where
数据结构:
处理对象:SQLBinaryOpExpr,SQLInSubQueryExpr
处理方式:
1. SQLBinaryOpExpr,左右子树遍历进去处理,如果有SQLInSubQueryExpr则处理,其他的类型不做处理,即:type=11 或者 添加上去的 tenant in (11,22) 也不做处理
2. 如果类型是SQLInSubQueryExpr,则是继续处理Select语句一样处理。
DruidSqlParser
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLCharExpr;
import com.alibaba.druid.sql.ast.expr.SQLIdentifierExpr;
import com.alibaba.druid.sql.ast.statement.SQLExprTableSource;
import com.alibaba.druid.sql.ast.statement.SQLInsertStatement.ValuesClause;
import com.alibaba.druid.sql.ast.statement.SQLJoinTableSource;
import com.alibaba.druid.sql.ast.statement.SQLSelect;
import com.alibaba.druid.sql.ast.statement.SQLSelectQueryBlock;
import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;
import com.alibaba.druid.sql.ast.statement.SQLTableSource;
import com.alibaba.druid.sql.ast.statement.SQLUpdateSetItem;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlDeleteStatement;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlInsertStatement;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlUpdateStatement;
import com.alibaba.druid.sql.parser.SQLParserUtils;
import com.alibaba.druid.sql.parser.SQLStatementParser;
import com.alibaba.druid.util.JdbcUtils;
/**
* 阿里巴巴的Druid Sql 解析器
* 用来控制数据权限
* 数据权限包括:资源定义 + 操作控制
* 数据定义:
* 数据结构:<资源类型,<维度,取值>>
* 示例:<table1,<name,11>>
* 含义:只允许操作 table1中name=11的字段
* 操作控制:
* 1. 查询权限: 如果查table1,只能查name=11的数据;
* - 涉及Select和Delete
* - 会添加where条件,如果select有join,会添加On条件
* 2. 添加权限:如果给table1添加数据,name值只能是11
* - 涉及:insert
* - 操作:会检查value中对应的值,如果name不是11,则抛出异常;
* 3. 更新权限: 如果要更新table1表,只能更新name=11的数据,且values中name对应的值只能是11
* - 涉及:update
* - 相当于是select+insert并集,即检查values中数据的合法性;又添加where条件。
*
* 案例:
-- ----------------1.update----------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[222, 33]] --
-- 维度[type] 权限值[33] --
-- 维度[tenant] 权限值[11] --
-- 原始脚本:
update `table1` SET `remark` = 222 where id = 1;
-- 结果脚本:
UPDATE `table1`
SET `remark` = 222
WHERE id = 1
AND (remark IN ('222', 33)
AND type = '33'
AND tenant = '11');
-- --------------------------------------------------------------------
-- --------2.单表查询------------------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[222, 33]] --
-- 维度[type] 权限值[33] --
-- 维度[tenant] 权限值[11] --
-- 原始脚本:
SELECT * from table1 where type = 111;
-- 结果脚本:
SELECT *
FROM table1
WHERE type = 111
AND (remark IN ('222', 33)
AND type = '33'
AND tenant = '11');
-- --------------------------------------------------------------------
-- --------3.多表查询------------------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[222, 33]] --
-- 维度[type] 权限值[33] --
-- 维度[tenant] 权限值[11] --
-- 原始脚本:
SELECT
t1.`name`,
t2.tenant,
t3.type
FROM
table1 t1
LEFT JOIN table2 t2 ON t1.tenant = t2.tenant
LEFT JOIN table3 t3 ON t2.tenant = t3.tenant
WHERE
t1.tenant = 11
ORDER BY
t1.type
LIMIT 0,
1;
-- 结果脚本:
SELECT
t1.`name`,
t2.tenant,
t3.type
FROM
table1 t1
LEFT JOIN table2 t2 ON t1.tenant = t2.tenant
AND (
t1.remark IN ('222', 33)
AND t1.type = '33'
AND t1.tenant = '11'
)
LEFT JOIN table3 t3 ON t2.tenant = t3.tenant
AND t3.id = '33'
WHERE
t1.tenant = 11
ORDER BY
t1.type
LIMIT 0,
1;
-- --------------------------------------------------------------------
-- --------4.删除------------------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[222, 33]] --
-- 维度[type] 权限值[33] --
-- 维度[tenant] 权限值[11] --
-- 原始脚本:
DELETE from table1 where tenant =1;
-- 结果脚本:
DELETE FROM table1
WHERE tenant = 1
AND (remark IN ('222', 33)
AND type = '33'
AND tenant = '11');
-- --------------------------------------------------------------------
-- -------5.有错误的insert-------------------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[222, 33]] --
-- 维度[type] 权限值[33] --
-- 维度[tenant] 权限值[11] --
-- 原始脚本:
INSERT INTO `table1` ( `name`, `remark`, `tenant`, `type`)VALUES ('55', '55', '55', '555'),('66', '66', '66', '666') ;
-- 结果脚本:
INSERT INTO `table1` (`name`, `remark`, `tenant`, `type`)
VALUES ('55', '55', '55', '555'),
('66', '66', '66', '666');
-- 错误信息:
第 [0] 行, 列:remark,值只允许是:[222, 33],实际值是:55
第 [1] 行, 列:remark,值只允许是:[222, 33],实际值是:66
第 [0] 行, 列:tenant,值只允许是:11,实际值是:55
第 [1] 行, 列:tenant,值只允许是:11,实际值是:66
第 [0] 行, 列:type,值只允许是:33,实际值是:555
第 [1] 行, 列:type,值只允许是:33,实际值是:666
-- --------------------------------------------------------------------
-- --------6.部分报错Insert------------------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[55, 44]] --
-- 原始脚本:
INSERT INTO `table1` ( `name`, `remark`, `tenant`, `type`)VALUES ('55', '55', '55', '555'),('66', '66', '66', '666') ;
-- 结果脚本:
INSERT INTO `table1` (`name`, `remark`, `tenant`, `type`)
VALUES ('55', '55', '55', '555'),
('66', '66', '66', '666');
-- 错误信息:
第 [1] 行, 列:remark,值只允许是:[55, 44],实际值是:66
-- --------------------------------------------------------------------
-- --------7.无报错insert------------------------------------------------
-- 权限设置
-- 资源[table3] --
-- 维度[id] 权限值[33] --
-- 资源[table1] --
-- 维度[remark] 权限值[[55, 66]] --
-- 原始脚本:
INSERT INTO `table1` ( `name`, `remark`, `tenant`, `type`)VALUES ('55', '55', '55', '555'),('66', '66', '66', '666') ;
-- 结果脚本:
INSERT INTO `table1` (`name`, `remark`, `tenant`, `type`)
VALUES ('55', '55', '55', '555'),
('66', '66', '66', '666');
-- 错误信息:
-- --------------------------------------------------------------------
*
*
*/
public class DruidSqlParser {
public static void main(String[] args) throws Exception {
String sql = "SELECT * from table1 where type = 111";
// String sql = "SELECT t1.`name`,t2.tenant from table1 t1 LEFT JOIN table2 t2 On t1.tenant = t2.tenant where t1.tenant = 11 order by t1.type limit 0,1";
// String sql = "SELECT t1.`name`,t2.tenant,t3.type from table1 t1 LEFT JOIN table2 t2 On t1.tenant = t2.tenant LEFT JOIN table3 t3 On t2.tenant = t3.tenant where t1.tenant = 11 order by t1.type limit 0,1 ";
// String sql = "DELETE from table1 where tenant =1";
// String sql = "INSERT INTO `table1` ( `name`, `remark`, `tenant`, `type`)VALUES ('55', '55', '55', '555'),('66', '66', '66', '666') ";
// String sql = "update `table1` SET `remark` = 222 where id = 1";
/**
* 数据结构:<资源类型,<维度,取值>>
* 取值类型:int,string,list
*/
Map<String, Map<String, Object>> privateMap = new HashMap<String, Map<String, Object>>();
Map<String, Object> columnMap1 = new HashMap<String, Object>();
// columnMap1.put("tenant", "11");
// columnMap1.put("type", "33");
// columnMap1.put("tenant", 11);
columnMap1.put("remark", Arrays.asList("55","66"));
privateMap.put("table1", columnMap1);
/*Map<String, Object> columnMap2 = new HashMap<String, Object>();
columnMap2.put("type", "22");
privateMap.put("table2", columnMap2);*/
Map<String, Object> columnMap3 = new HashMap<String, Object>();
columnMap3.put("id", "33");
privateMap.put("table3", columnMap3);
String temp = null;
// 编译Sql,类似是Mysql
SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, JdbcUtils.MYSQL);
List<SQLStatement> stmtList = parser.parseStatementList();
SQLStatement stmt = stmtList.get(0);
// 是update语句
if (stmt instanceof MySqlUpdateStatement) {
MySqlUpdateStatement updateStatement = (MySqlUpdateStatement) stmt;
SQLExprTableSource tableSource = (SQLExprTableSource) updateStatement.getTableSource();
SQLExpr where = updateStatement.getWhere();
List<SQLUpdateSetItem> items = updateStatement.getItems();
String alias = tableSource.getAlias();
SQLIdentifierExpr expr = (SQLIdentifierExpr) tableSource.getExpr();
// 获取表名
String tableName = cleanSqlName(expr.getName());
// 判断该表是否控制
if(privateMap.containsKey(tableName)){
//获取该资源的各维度数据
Map<String, Object> columnMap = privateMap.get(tableName);
// 缓存错误信息
StringBuffer sb = new StringBuffer();
// 遍历 set 的各个键值对,处理
for(SQLUpdateSetItem item : items){
// 获取列名
String columnName = cleanSqlName(item.getColumn().toString());
// 判断是否控制该列
if(columnMap.containsKey(columnName)){
// 获取值,如果是null,则不进行控制
SQLExpr valueExpr = item.getValue();
if(valueExpr == null){
continue;
}
// 获取该列允许的权限值
Object privateValue = columnMap.get(columnName);
String value = valueExpr.toString();
// 如果权限值是 数字或者 字符,则直接== 判断
if(privateValue instanceof Number || privateValue instanceof String){
if(!(privateValue+"").equals(value)){
sb.append("\n").append(String.format("列:%s,值只允许是:%s,实际值是:%s", columnName,privateValue,value));
}
}else if (privateValue instanceof List){
//如果 权限值 是一个范围,就需要遍历这个List,看是否允许
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) privateValue;
// 如果将要插入的值不是 允许的值,则添加报错信息
if(!list.contains(value)){
// 如果不包含报错
sb.append("\n").append(String.format("列:%s,值只允许是:%s,实际值是:%s", columnName,list,value));
}
}
}
}
// 如果报错信息不为空,则直接抛出异常
// if(sb.toString().length() > 0){
// throw new Exception(sb.toString());
// }
temp = sb.toString();
}
// 构建where条件
where = volidatePrivate(privateMap, where, alias, tableName);
// 设置where
updateStatement.setWhere(where);
}
/**
* insert 语句
* 示例脚本:INSERT INTO `table1` ( `name`, `remark`, `tenant`, `type`)
* VALUES ('55', '55', '55', '555'),('66', '66', '66', '666')
*/
if (stmt instanceof MySqlInsertStatement) {
MySqlInsertStatement insertStatement = (MySqlInsertStatement) stmt;
SQLExprTableSource tableSource = (SQLExprTableSource) insertStatement.getTableSource();
SQLIdentifierExpr expr = (SQLIdentifierExpr) tableSource.getExpr();
// `table1`,看看 是不是要清理
String tableName = cleanSqlName(expr.getName());
// 判断是否控制资源
if(privateMap.containsKey(tableName)){
// 获取各维度值
Map<String, Object> columnMap = privateMap.get(tableName);
// 获取列名字
List<SQLExpr> columns = insertStatement.getColumns();
// 获取值列表,存在一次insert多条的场景
List<ValuesClause> valuesList = insertStatement.getValuesList();
// 错误信息缓存
StringBuffer sb = new StringBuffer();
// 遍历 ( `name`, `remark`, `tenant`, `type`)
for (int i = 0 ; i < columns.size() ;i++) {
// 获取列,名称 `name`,也需要清理吧
String columnName = cleanSqlName(((SQLIdentifierExpr) columns.get(i)).getName());
// 判断是否对改列有 控制
if(columnMap.containsKey(columnName)){
Object privateValue = columnMap.get(columnName);
// 遍历 列值,看是否是允许的值
for (int j = 0 ; j < valuesList.size() ;j++) {
// insert中的一行 ('55', '55', '55', '555')
List<SQLExpr> values = valuesList.get(j).getValues();
// 根据列的下标,获取对应的值
String value = ((SQLCharExpr) values.get(i)).getText();
// 如果是number和string直接判断相等与否;如果是列表,择需要看是否包含
if(privateValue instanceof Number || privateValue instanceof String){
// 如果跟设置的不相同
if(!(privateValue+"").equals(value)){
sb.append("\n").append(String.format("第 [%s] 行, 列:%s,值只允许是:%s,实际值是:%s", j,columnName,privateValue,value));
}
}else if (privateValue instanceof List){
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) privateValue;
// 如果不包含报错
if(!list.contains(value)){
sb.append("\n").append(String.format("第 [%s] 行, 列:%s,值只允许是:%s,实际值是:%s", j,columnName,list,value));
}
}
}
}
}
// 如果错误信息不为空,直接抛出异常
// if(sb.toString().length() > 0){
// throw new Exception(sb.toString());
// }
temp = sb.toString();
}
}
// 删除语句
if (stmt instanceof MySqlDeleteStatement) {
MySqlDeleteStatement delStatement = (MySqlDeleteStatement) stmt;
SQLExpr where = delStatement.getWhere();
SQLExprTableSource tableSource = (SQLExprTableSource) delStatement.getTableSource();
String alias = tableSource.getAlias();
SQLIdentifierExpr expr = (SQLIdentifierExpr) tableSource.getExpr();
// 获取表名,并清理`
String tableName = cleanSqlName(expr.getName());
// 构建where条件
where = volidatePrivate(privateMap, where, alias, tableName);
// 设置where
delStatement.setWhere(where);
}
// 查询语句
if (stmt instanceof SQLSelectStatement) {
SQLSelectStatement selectStmt = (SQLSelectStatement) stmt;
SQLSelect sqlselect = selectStmt.getSelect();
SQLSelectQueryBlock query = (SQLSelectQueryBlock) sqlselect.getQuery();
SQLTableSource from = query.getFrom();
// json语句,这是是棵树
if(from instanceof SQLJoinTableSource){
// 获取sql原始数据
SQLJoinTableSource sqlTableSource = (SQLJoinTableSource) from;
List<String> sqlList = new ArrayList<String>();
// 递归添加ON条件
addJoinSqlCondition(privateMap,sqlList,sqlTableSource);
}
// 简单Sql
if(from instanceof SQLExprTableSource){
// 获取sql原始数据
SQLExprTableSource sqlExprTableSource = (SQLExprTableSource) from;
SQLExpr expr = sqlExprTableSource.getExpr();
SQLIdentifierExpr sqlIdentifierExpr = (SQLIdentifierExpr) expr;
String tableName = cleanSqlName(sqlIdentifierExpr.getName());
String alias = sqlExprTableSource.getAlias();
SQLExpr where = query.getWhere();
// 构建where条件
where = volidatePrivate(privateMap, where, alias, tableName);
// 设置where条件
query.setWhere(where);
}
}
// 打印查询语句
System.out.println("-- --------------------------------------------------------------------");
System.out.println("-- 权限设置");
Set<String> keySet = privateMap.keySet();
for(String key : keySet){
System.out.println(String.format("-- 资源[%s] --", key));
Map<String, Object> map = privateMap.get(key);
Set<String> keySet2 = map.keySet();
for(String key2 : keySet2){
Object object = map.get(key2);
System.out.println(String.format("-- 维度[%s] 权限值[%s] --",key2, object.toString()));
}
}
System.out.println("");
System.out.println("-- 原始脚本:");
System.out.println(sql+";");
System.out.println("");
System.out.println("-- 结果脚本:");
System.out.println(stmt.toString()+";");
System.out.println("-- 错误信息:");
System.out.println(temp);
System.out.println("-- --------------------------------------------------------------------");
System.out.println("");
}
private static String cleanSqlName(String val){
if(val == null){
return val;
}
return val.replace("`", "");
}
private static SQLExpr volidatePrivate(Map<String, Map<String, Object>> privateMap,SQLExpr where, String alias,String tableName) {
if(privateMap.containsKey(tableName)){
Map<String, Object> columnMap = privateMap.get(tableName);
if(columnMap != null && columnMap.size() > 0){
Set<String> keys = columnMap.keySet();
List<String> sqlList = new ArrayList<String>();
for(String key:keys){
key = cleanSqlName(key);
// 如果别名存在就用别名
String key1 = alias !=null && alias.length() > 0 ? alias.concat(".").concat(key) : key;
// 拼接条件对象
sqlList.add(getSqlByClass1(key1,columnMap.get(key)));
}
// 添加条件
String sql1 = listToSql(sqlList);
// 清空list
sqlList.clear();
// 构建条件对象
SQLExpr sqlExpr = SQLParserUtils.createExprParser(sql1, JdbcUtils.MYSQL).expr();
// 修改where表达式
if (where == null) {
where = sqlExpr;
} else {
where = new SQLBinaryOpExpr(where, SQLBinaryOperator.BooleanAnd,sqlExpr );
}
}
}
return where;
}
/**
* 给jsonsql语句添加ON 条件
* 示例:
* FROM
* table1 t1
* LEFT JOIN table2 t2 ON t1.tenant = t2.tenant
* LEFT JOIN table3 t3 ON t2.tenant = t3.tenant
* 数据结构:
* SQLJoinTableSource
* / \
* SQLJoinTableSource SQLExprTableSource(table3)
* / \
* SQLExprTableSource(table3) SQLExprTableSource(table2)
*
* @param privateMap 数据权限Map
* @param sqlList 添加的Sql条件列表
* @param ts 数据源对象
*/
private static void addJoinSqlCondition(Map<String, Map<String, Object>> privateMap,List<String> sqlList,SQLTableSource ts) {
if(ts == null){
return ;
}
// 判断是否我 符合节点
if(ts instanceof SQLJoinTableSource){
// 转换类型
SQLJoinTableSource sqlTableSource = (SQLJoinTableSource) ts;
// 检查左边
addJoinSqlCondition(privateMap,sqlList,sqlTableSource.getLeft());
// 检查右边
addJoinSqlCondition(privateMap,sqlList,sqlTableSource.getRight());
// 添加条件
String sql = listToSql(sqlList);
// 清空list
sqlList.clear();
if(sql != null && sql.length() > 0){
SQLExpr condition = sqlTableSource.getCondition();
SQLExpr constraintsExpr = SQLParserUtils.createExprParser(sql, JdbcUtils.MYSQL).expr();
condition = new SQLBinaryOpExpr(condition, SQLBinaryOperator.BooleanAnd, constraintsExpr);
sqlTableSource.setCondition(condition);
}
}
// 判断是否是 叶子节点
if(ts instanceof SQLExprTableSource){
// 获取别名
String leftTableAlias = ts.getAlias();
SQLExprTableSource sqlExprTableSource = (SQLExprTableSource) ts;
SQLIdentifierExpr sqlIdentifierExpr = (SQLIdentifierExpr) sqlExprTableSource.getExpr();
// 获取表名
String tableName = cleanSqlName(sqlIdentifierExpr.getName());
// 判断是否控制该资源
if(privateMap.containsKey(tableName)){
// 获取维度数据
Map<String, Object> columnMap = privateMap.get(tableName);
if(columnMap != null && columnMap.size() > 0){
// 遍历维度数据
Set<String> keys = columnMap.keySet();
for(String key : keys){
// 如果别名存在就用别名
String key1 = leftTableAlias !=null && leftTableAlias.length() > 0 ? leftTableAlias.concat(".").concat(key) : key;
// 拼接条件对象
sqlList.add(getSqlByClass1(key1,columnMap.get(key)));
}
}
}
}
}
/**
* 将条件的list转换为Sql
* @param sqlList
* @return
*/
private static String listToSql(List<String> sqlList) {
StringBuffer sb = new StringBuffer();
if(sqlList.size() > 0){
String sql1 = sqlList.get(0);
sb.append(sql1);
for(int i = 1; i < sqlList.size() ; i++){
sb.append(" and").append(sqlList.get(i));
}
}
return sb.toString();
}
/**
* 根据类型拼接sql 条件
* @param key1
* @param value
* @return
*/
private static String getSqlByClass1(String key1, Object value) {
StringBuffer sb = new StringBuffer();
sb.append(" ").append(key1).append(" ");
if(value instanceof Number){
sb.append(" = ").append(value);
}else if(value instanceof String){
sb.append(" = '").append(value+"'");
}else if (value instanceof List){
sb.append(" in (");
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
Iterator<Object> it = list.iterator();
while(it.hasNext()){
Object next = it.next();
if(next instanceof Number){
sb.append(next);
}else if(next instanceof String){
sb.append("'").append(next).append("'");
}
if(it.hasNext()){
sb.append(",");
}
}
sb.append(")");
}
return sb.toString();
}
}
Mybatis拦截器代码
import java.sql.Connection;
import java.util.Properties;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.springframework.stereotype.Component;
/**
* mybatis分表拦截器 -- 水平切分
*/
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
@Component
public class TableShardInterceptor implements Interceptor {
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("ThreadID:"+Thread.currentThread().getId());
System.out.println("Cache:"+BusinessServiceController.Cache.get());
if (invocation.getTarget() instanceof RoutingStatementHandler) {
try {
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
// MetaObject是mybatis里面提供的一个工具类,类似反射的效果
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");//获取sql语句
String originSql = boundSql.getSql();
System.out.println("originSql1:"+originSql);
if(originSql.contains("table1")){
originSql = originSql.replaceAll("table1","test_view");
originSql = "call test_sp("+BusinessServiceController.Cache.get()+");" + originSql;
}
System.out.println("originSql2:"+originSql);
// originSql = originSql.replaceAll("test_view", "table1");
// 把新语句设置回去
metaStatementHandler.setValue("delegate.boundSql.sql", originSql);
} catch (Exception e) {
// ignore 任何一个地方有异常都去执行原始操作 -- invocation.proceed()
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
return (target instanceof RoutingStatementHandler) ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
}
}
总结
……