文章目录
- 学习链接
- mybatisplus数据权限插件学习初探
- 前言
- 案例
- 建表
- 用户表
- 订单表
- 环境准备
- User
- UserMapper
- UserMapper.xml
- Orders
- OrdersMapper
- OrdersMapper.xml
- 配置
- UserTypeEnum
- UserContextHolder
- CustomizeDataPermissionHandler
- MybatisPlusConfig
- 测试
- 测试类
- boss
- deptManager
- clerk
- 动态表名更换插件
- 案例
- 代码
- MyBatisplusConfig
- AccountController
- AccountMapper
- AccountMapper.xml
- 测试
- 防止全表更新与删除插件
- 案例
- 代码
- MyBatisplusConfig
- AccountController
- AccountMapper
- AccountMapper.xml
学习链接
Mybatis-Plus入门系列(3)- MybatisPlus之数据权限插件DataPermissionInterceptor
jsqlparser学习 - 自己收藏的链接
Mysql递归查询子级(父子级结构)&从子级ID查询所有父级(及扩展知识) - 自己的链接
Mysql带层级(父子级)的递归查询案例 - 自己的链接
mybatisplus数据权限插件学习初探
前言
对于系统中的不同用户,对于同一接口,可能都有权限访问此接口,但是由于用户各自的权限大小,看到的数据不一样(数据权限
)。
就比如:有一张用户表,每个用户只能属于某一个部门,每个用户都有自己的用户类型(用户类型有老板、部门经理、普通职工),每个员工的订单记录在订单表中,订单属于创建这个订单的用户,订单也属于这个用户的部门。
- 对于老板来说,能看到所有的订单
- 对于部门经理来说,能够看到自己部门(可能还有子部门,这里暂不考虑)的订单
- 对于普通职工来说,只能看到自己的订单
如果对于订单表的查询有多个地方,或者又不仅仅是订单表需要按上面的规则来作数据权限控制,那么在service层的每个地方几乎都要来上面的用户类型判断,然后再写不同的sql来做对应的查询。
所以如果能在dao层能够根据当前用户类型,自动的拼接对应的条件,那样是比较方便的,下面演示的案例仅作为自己入门学习案例记录,对于复杂的sql,需要深入学习下Jsqlparser,参考一些开源项目的做法。
值得思考的问题:
- 如何进行数据权限的划分?按部门划分是一种方案,但是部门之间有可能会存在上下级,有些人可能属于多个部门(同时存在上下级部门)。有没有其它的划分方式?
- 复杂sql的处理?
案例
建表
用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`type` int(1) DEFAULT NULL COMMENT '用户类型',
`tenant_id` bigint(20) NOT NULL COMMENT '租户ID',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`dept_id` int(11) DEFAULT NULL COMMENT '所属部门id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (1, 0, 1, 'mp', NULL);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (2, 1, 1, 'Jack', 1);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (3, 1, 1, 'Sandy', 2);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (4, 2, 1, 'Billie', 1);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (5, 2, 1, 'Sally', 1);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (6, 2, 1, 'Kevin', 2);
订单表
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`orders_no` varchar(10) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`dept_id` int(11) DEFAULT NULL,
`tenant_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `test`.`orders` (`id`, `orders_no`, `user_id`, `dept_id`, `tenant_id`) VALUES (1, '001', 4, 1, 1);
INSERT INTO `test`.`orders` (`id`, `orders_no`, `user_id`, `dept_id`, `tenant_id`) VALUES (2, '002', 5, 1, 1);
INSERT INTO `test`.`orders` (`id`, `orders_no`, `user_id`, `dept_id`, `tenant_id`) VALUES (3, '003', 6, 2, 1);
环境准备
User
@Data
@Accessors(chain = true)
public class User {
/**
* 租户 ID
*/
private Long tenantId;
@TableId(type = IdType.AUTO)
private Long id;
private Integer type;
private Integer deptId;
private String name;
}
UserMapper
public interface UserMapper extends BaseMapper<User> {
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baomidou.mybatisplus.samples.dataPerm.mapper.UserMapper">
</mapper>
Orders
@Data
@Accessors(chain = true)
public class Orders {
private String id;
private String ordersNo;
private Integer userId;
private String deptId;
}
OrdersMapper
public interface OrdersMapper extends BaseMapper<Orders> {
List<Orders> selectOrdersList();
}
OrdersMapper.xml
这里起初并没有where条件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baomidou.mybatisplus.samples.dataPerm.mapper.OrdersMapper">
<select id="selectOrdersList" resultType="com.baomidou.mybatisplus.samples.dataPerm.entity.Orders">
SELECT * FROM orders
</select>
</mapper>
配置
UserTypeEnum
@Getter
public enum UserTypeEnum {
BOSS(0,"老板"),
DEPT_MANAGER(1,"部门经理"),
CLERK(1,"普通职员"),
DEFAULT(1,"普通职员"),
;
Integer type;
String desc;
UserTypeEnum(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
public static UserTypeEnum type(Integer type) {
for (UserTypeEnum value : UserTypeEnum.values()) {
if (Objects.equals(type, value.type)) {
return value;
}
}
return DEFAULT;
}
}
UserContextHolder
public class UserContextHolder {
private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();
public static void bindUser(User user) {
USER_THREAD_LOCAL.set(user);
}
public static void unBindUser() {
USER_THREAD_LOCAL.remove();
}
public static User getUser() {
return USER_THREAD_LOCAL.get();
}
}
CustomizeDataPermissionHandler
@Component
public class CustomizeDataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
User user = UserContextHolder.getUser();
if (user == null) {
return where;
}
UserTypeEnum type = UserTypeEnum.type(user.getType());
if (UserTypeEnum.BOSS == type) {
return where;
} else if (UserTypeEnum.DEPT_MANAGER == type) {
// 部门下可能存在子部门(需要查询当前部门的所有子部门(包括当前部门),可以使用sql递归), 这里暂时就认为只有1个
StringValue deptIdStringValue = new StringValue(String.valueOf(user.getDeptId()));
ExpressionList expressionList = new ExpressionList(deptIdStringValue);
InExpression inExpression = new InExpression(new Column("orders.dept_id"), expressionList);
if (where == null) {
// 如果原来没有where条件, 就添加一个where条件
return inExpression;
} else {
return new AndExpression(where, inExpression);
}
} else {
EqualsTo equalsTo = new EqualsTo(new Column("orders.user_id"), new StringValue(String.valueOf(user.getUserId())));
if (where == null) {
// 如果原来没有where条件, 就添加一个where条件
return equalsTo;
} else {
return new AndExpression(where, equalsTo);
}
}
}
}
MybatisPlusConfig
@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.dataPerm.mapper")
public class MybatisPlusConfig {
@Autowired
private CustomizeDataPermissionHandler dataPermissionHandler;
/**
* 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataPermissionInterceptor(dataPermissionHandler));
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
User user = UserContextHolder.getUser();
if (user != null) {
return new LongValue(user.getTenantId());
} else {
return null;
}
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
return Objects.equals("user", tableName);
}
}));
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDialect(DialectFactory.getDialect(DbType.MYSQL));
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// interceptor.addInnerInterceptor();
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
// @Bean
// public ConfigurationCustomizer configurationCustomizer() {
// return configuration -> configuration.setUseDeprecatedExecutor(false);
// }
}
测试
测试类
@Slf4j
@SpringBootTest(classes = TenantApplication.class)
public class TenantTest {
@Resource
private UserMapper userMapper;
@Resource
private OrdersMapper ordersMapper;
@Test
public void test001() {
// User user = userMapper.selectById(1);
// User user = userMapper.selectById(2);
User user = userMapper.selectById(4);
UserContextHolder.bindUser(user);
List<Orders> orders = ordersMapper.selectOrdersList();
for (Orders order : orders) {
log.info("{}", order);
}
Page<Orders> ordersPage = new Page<>(1, 1);
ordersMapper.selectPage(ordersPage, null);
log.info("total:{},pages:{},data:{}",ordersPage.getTotal(),ordersPage.getPages(), ordersPage.getRecords());
UserContextHolder.unBindUser();
}
}
boss
老板可以看到所有的数据。可以看到下面,仅拼接了租户id
SELECT id, tenant_id, type, dept_id, name FROM user WHERE id = 1
SELECT * FROM orders WHERE orders.tenant_id = 1
SELECT COUNT(*) AS total FROM orders WHERE orders.tenant_id = 1
SELECT id, orders_no, user_id, dept_id FROM orders WHERE orders.tenant_id = 1 LIMIT 1
deptManager
部门经理可以看到本部门及子部门的数据。可以看到下面拼接了in的条件
SELECT id, tenant_id, type, dept_id, name FROM user WHERE id = 2
SELECT * FROM orders WHERE orders.dept_id IN ('1') AND orders.tenant_id = 1
SELECT COUNT(*) AS total FROM orders WHERE orders.dept_id IN ('1') AND orders.tenant_id = 1
SELECT id, orders_no, user_id, dept_id FROM orders WHERE orders.dept_id IN ('1') AND orders.tenant_id = 1 LIMIT 1
clerk
普通职员只能看到自己的数据。可以看到拼接的条件使用user_id
SELECT id, tenant_id, type, dept_id, name FROM user WHERE id = 4
SELECT * FROM orders WHERE orders.user_id = '4' AND orders.tenant_id = 1
SELECT COUNT(*) AS total FROM orders WHERE orders.user_id = '4' AND orders.tenant_id = 1
SELECT id, orders_no, user_id, dept_id FROM orders WHERE orders.user_id = '4' AND orders.tenant_id = 1 LIMIT 1
动态表名更换插件
这个插件比较简单,就是在sql中遇到表名,会把这个表名传递给TableNameHandler处理器,然后在TableNameHandler处理器的dynamicTableName方法中获取到表名,然后返回一个新的表名替换掉sql中的原表名。
案例
代码
MyBatisplusConfig
@Slf4j
@Configuration
public class MyBatisplusConfig {
@Bean
public MybatisPlusInterceptor interceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameInnerInterceptor.setTableNameHandler(new TableNameHandler() {
@Override
public String dynamicTableName(String sql, String tableName) {
log.info("sql: {}", sql);
log.info("tableName: {}", tableName);
return tableName + "_1";
}
});
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
AccountController
@RestController
@RequestMapping("/account")
public class AccountController {
@RequestMapping("getAccounts")
public List<Account> getAccounts() {
// 调用mybatisplus的方法
return accountService.list();
}
@RequestMapping("getAccounts2")
public List<Account> getAccounts2() {
// 调用自己写的mapper方法
return accountMapper.find();
}
}
AccountMapper
public interface AccountMapper extends BaseMapper<Account> {
List<Account> find();
}
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzhua.mapper.AccountMapper">
<select id="find" resultMap="BaseResultMap">
SELECT * FROM account a INNER JOIN user u on a.user_id = u.id
</select>
</mapper>
测试
访问:http://localhost:8080/account/getAccounts
,可以看到表名替换了
访问:http://localhost:8080/account/getAccounts2
,可以看到2个表名都会经过TableNameHandler
防止全表更新与删除插件
这个插件可以防止全表更新与删除插件,就是如果不带where条件DML的sql会抛出异常,但是有的时候,就是需要全表删除呢?可以在mapper方法上使用@InterceptorIgnore(blockAttack = “true”)注解标注此方法即可。
案例
代码
MyBatisplusConfig
直接加就完了
@Slf4j
@Configuration
public class MyBatisplusConfig {
@Bean
public MybatisPlusInterceptor interceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}
AccountController
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("deleteAll")
public Object deleteAll() {
accountMapper.deleteAll();
return "删除成功";
}
@GetMapping("updateAll")
public Object updateAll() {
LambdaUpdateWrapper<Account> updateWrapper = new LambdaUpdateWrapper<Account>().set(Account::getNickName, "005");
accountMapper.update(null, updateWrapper);
return "修改成功";
}
}
AccountMapper
public interface AccountMapper extends BaseMapper<Account> {
@InterceptorIgnore(blockAttack = "true") // true表示不启用插件
void deleteAll();
}
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzhua.mapper.AccountMapper">
<delete id="deleteAll">
delete from account
</delete>
</mapper>