依赖jar包

引入包 版本
jdk 1.8
spring boot 2.7.2
mybatis-plus-extension 3.5.3.1
mybatis-plus 3.5.3.1
mybatis-plus-boot-starter 3.5.3.1
spring-boot-starter-web 2.7.9

使用

添加依赖

1
2
3
4
5
<dependency>
<groupId>cn.allbs</groupId>
<artifactId>allbs-mybatis</artifactId>
<version>2.0.3</version>
</dependency>
1
implementation 'cn.allbs:allbs-mybatis:2.0.3'
1
implementation("cn.allbs:allbs-mybatis:2.0.3")

配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mybatis-plus:
mapper-locations: classpath*:mapper/*/*.xml
global-config:
banner: false
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
# 此处注释是为了使用该包中自定义打印的sql日志,如果放开会打印两次sql日志,只是格式不同
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-handlers-package: com.allbs.allbsjwt.config.handler
# 这边是为了打印自定义格式的sql日志,比如参数自动填充,结果按照类似表格输出。如果不需要或者生成环境此处设为false
show-sql: true
# 此处是为了审计自动填充,因为不同表中字段不一样所以逻辑删除、创建者、创建时间、更新者、更新时间字段自定义。有些系统创建者和更新者使用的是id则与本系统不兼容,此处默认的是插入spring security中的用户名
meta-custom:
del-flg: delFlag
create-name: createId
update-name: updateId
# 设置中文字符占其他字符的比例,取巧尽量让打印出来的格式工整些,比如某些字体占两个英文的宽度就设置为2
chine-rate: 1.5
# 是否开启权限过滤字段
data-pms: true

日志打印示例

image-20230327111534230

image-20230327111549175

审计字段自动插入

image-20230327111732400

权限过滤

开启

mybatis-plus.data-pms 设置为true,看上方配置示例最后一个配置

使用说明

实现DataPmsHandler后写详细的逻辑即可,比如:

@ScopeField是用于跟表关联的实体类上的注解,用于标记改表中权限过滤的字段是哪个,以下类举例:下文中DEFAULT_FILTER_FIELD默认的是ent_id指数据库表中以该字段作为区分,如果有张表突然设置的是unit_id而不是ent_id则在对应的实体上设置@ScopeField("unit_id")

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
import cn.allbs.allbsjwt.config.utils.SecurityUtils;
import cn.allbs.allbsjwt.config.vo.SysUser;
import cn.allbs.common.constant.StringPool;
import cn.allbs.mybatis.datascope.DataPmsHandler;
import cn.allbs.mybatis.datascope.ScopeField;
import cn.allbs.mybatis.utils.PluginUtils;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.stereotype.Component;

import java.util.Optional;
import java.util.Set;

/**
* 类 CustomPermissionHandler
*
* @author ChenQi
* @date 2023/3/28
*/
@Slf4j
@Component
public class CustomPermissionHandler implements DataPmsHandler {

private final static String DEFAULT_FILTER_FIELD = "ent_id";

/**
* 获取数据权限 SQL 片段
*
* @param where 待执行 SQL Where 条件表达式
* @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
* @return JSqlParser 条件表达式
*/
@Override
public Expression getSqlSegment(final Table table, Expression where, String mappedStatementId) {

SysUser sysUser = SecurityUtils.getUser();
// 如果非权限用户则不往下执行,执行原sql
if (sysUser == null) {
return where;
}
// 在有权限的情况下查询用户所关联的企业列表
Set<Long> permissionEntList = sysUser.getEntIdList();
// if (permissionEntList.size() == 0) {
// return where;
// }
TableInfo tableInfo = TableInfoHelper.getTableInfo(table.getName());
String fieldName = Optional.ofNullable(tableInfo.getEntityType().getAnnotation(ScopeField.class)).map(ScopeField::value).orElse(DEFAULT_FILTER_FIELD);
String finalFieldName = Optional.of(table).map(Table::getAlias).map(a -> a.getName() + StringPool.DOT + fieldName).orElse(fieldName);

if (permissionEntList.size() > 1) {
// 把集合转变为 JSQLParser需要的元素列表
InExpression inExpression = new InExpression(new Column(finalFieldName), PluginUtils.getItemList(permissionEntList));

// 组装sql
return where == null ? inExpression : new AndExpression(where, inExpression);
}
// 设置where
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(finalFieldName));
equalsTo.setRightExpression(new LongValue(permissionEntList.stream().findFirst().orElse(0L)));
return where == null ? equalsTo : new AndExpression(where, equalsTo);
}
}
效果

image-20230330102020985

image-20230330102109077

忽略权限拦截的方法
  • 自定义sql情况下忽略:

在对应的dao指定方法上添加注解@InterceptorIgnore

  • 使用mybatis plus 内置sdk的情况下忽略:

dao继承的BaseMapper修改为PmsMapper

image-20230330104127313

  • 指定表所有数据都不经过过滤

    在对应的dao上添加注解@InterceptorIgnore

新增、修改、批量新增、批量修改时的越权判断

实现DataPmsHandler中的updateParameterinsertParameter

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package cn.allbs.allbsjwt.config.datascope.mapper;

import cn.allbs.allbsjwt.config.exception.UserOverreachException;
import cn.allbs.allbsjwt.config.utils.SecurityUtils;
import cn.allbs.allbsjwt.config.vo.SysUser;
import cn.allbs.common.constant.StringPool;
import cn.allbs.mybatis.datascope.DataPmsHandler;
import cn.allbs.mybatis.datascope.ScopeField;
import cn.allbs.mybatis.utils.PluginUtils;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
* 类 CustomPermissionHandler
*
* @author ChenQi
* @date 2023/3/28
*/
@Slf4j
@Component
public class CustomPermissionHandler implements DataPmsHandler {

private final static String DEFAULT_FILTER_FIELD = "ent_id";

/**
* 获取数据权限 SQL 片段
*
* @param where 待执行 SQL Where 条件表达式
* @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
* @return JSqlParser 条件表达式
*/
@Override
public Expression getSqlSegment(final Table table, Expression where, String mappedStatementId) {

// 在有权限的情况下查询用户所关联的企业列表
SysUser sysUser = SecurityUtils.getUser();
// 如果非权限用户则不往下执行,执行原sql
if (sysUser == null) {
return where;
}
Set<Long> permissionEntList = sysUser.getEntIdList();
// if (permissionEntList.size() == 0) {
// return where;
// }
TableInfo tableInfo = TableInfoHelper.getTableInfo(table.getName());
String fieldName = tableInfo.getFieldList().stream()
.filter(a -> a.getField().getAnnotation(ScopeField.class) != null)
.map(a -> a.getField().getAnnotation(ScopeField.class).value())
.findFirst()
.orElse(DEFAULT_FILTER_FIELD);
Alias fromItemAlias = table.getAlias();
String finalFieldName = Optional.ofNullable(fromItemAlias).map(a -> a.getName() + StringPool.DOT + fieldName).orElse(fieldName);

if (permissionEntList.size() > 1) {
// 把集合转变为 JSQLParser需要的元素列表
InExpression inExpression = new InExpression(new Column(finalFieldName), PluginUtils.getItemList(permissionEntList));

// 组装sql
return where == null ? inExpression : new AndExpression(where, inExpression);
}
// 设置where
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(finalFieldName));
equalsTo.setRightExpression(new LongValue(permissionEntList.stream().findFirst().orElse(0L)));
return where == null ? equalsTo : new AndExpression(where, equalsTo);
}

@Override
public void updateParameter(Update updateStmt, MappedStatement mappedStatement, BoundSql boundSql) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(updateStmt.getTable().getName());
parameterHandler(tableInfo.getFieldList(), boundSql);
}

@Override
public void insertParameter(Insert insertStmt, BoundSql boundSql) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(insertStmt.getTable().getName());
parameterHandler(tableInfo.getFieldList(), boundSql);
}

private void parameterHandler(List<TableFieldInfo> fieldList, BoundSql boundSql) {
// 过滤数据
SysUser sysUser = SecurityUtils.getUser();
// 如果当前用户是超级管理员,不处理,根据自己系统实际情况去判断
if (sysUser.getId() == 1L) {
return;
}
// 获取当前用户所具备的ent_id
Set<Long> permissionEntList = sysUser.getEntIdList();

// 获取当前表中需要权限过滤的字段名称
String fieldName = fieldList.stream()
.filter(a -> a.getField().getAnnotation(ScopeField.class) != null)
.map(a -> a.getField().getAnnotation(ScopeField.class).value())
.findFirst()
.orElse(DEFAULT_FILTER_FIELD);

MetaObject metaObject = SystemMetaObject.forObject(boundSql.getParameterObject());

for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
String propertyName = parameterMapping.getProperty();
if (propertyName.startsWith("ew.paramNameValuePairs")) {
continue;
}
String[] arr = propertyName.split("\\.");
String propertyNameTrim = arr[arr.length - 1].replace("_", "").toUpperCase();
if (fieldName.replaceAll("[._\\-$]", "").toUpperCase().equals(propertyNameTrim)) {
if (!Optional.ofNullable(metaObject.getValue(propertyName)).isPresent()) {
return;
}
long currentEntId = Long.parseLong(metaObject.getValue(propertyName).toString());
// 判断是否在权限范围内
if (permissionEntList.contains(currentEntId)) {
metaObject.setValue(propertyName, currentEntId);
} else {
// 我这边是直接抛出异常,所有sql语句会直接回滚可以选择其他办法如: 使用当前用户的ent_id 替换插入值 or 直接忽略当前插入sql但不抛出异常
throw new UserOverreachException();
}
}

}
}
}

源码

github