mybatisplus数据权限插件学习初探 & 动态表名更换插件 &防止全表更新与删除插件

文章目录

  • 学习链接
  • 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

从零搭建开发脚手架 基于Mybatis-Plus的数据权限实现

jsqlparser学习 - 自己收藏的链接

Mysql递归查询子级(父子级结构)&从子级ID查询所有父级(及扩展知识) - 自己的链接
Mysql带层级(父子级)的递归查询案例 - 自己的链接

mybatisplus数据权限插件学习初探

前言

对于系统中的不同用户,对于同一接口,可能都有权限访问此接口,但是由于用户各自的权限大小,看到的数据不一样(数据权限)。

就比如:有一张用户表,每个用户只能属于某一个部门,每个用户都有自己的用户类型(用户类型有老板、部门经理、普通职工),每个员工的订单记录在订单表中,订单属于创建这个订单的用户,订单也属于这个用户的部门。

  • 对于老板来说,能看到所有的订单
  • 对于部门经理来说,能够看到自己部门(可能还有子部门,这里暂不考虑)的订单
  • 对于普通职工来说,只能看到自己的订单

如果对于订单表的查询有多个地方,或者又不仅仅是订单表需要按上面的规则来作数据权限控制,那么在service层的每个地方几乎都要来上面的用户类型判断,然后再写不同的sql来做对应的查询。

所以如果能在dao层能够根据当前用户类型,自动的拼接对应的条件,那样是比较方便的,下面演示的案例仅作为自己入门学习案例记录,对于复杂的sql,需要深入学习下Jsqlparser,参考一些开源项目的做法。

值得思考的问题:

  • 如何进行数据权限的划分?按部门划分是一种方案,但是部门之间有可能会存在上下级,有些人可能属于多个部门(同时存在上下级部门)。有没有其它的划分方式?
  • 复杂sql的处理?

案例

建表

mybatisplus数据权限插件学习初探 & 动态表名更换插件 &防止全表更新与删除插件_第1张图片

用户表

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


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条件


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


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


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>

你可能感兴趣的:(#,mybatis,学习,mybatis,java)