赞
踩
WEB类型软件产品,在Java(SpringBoot)+MybatisPlus架构场景下,本文针对下面两个问题,提供解决方案:
Demo源码仓库: java-test: java练习Demo项目 - Gitee.com
目前MybatisPlus官方文档中已有的插件功能:
各种插件的使用方法,网上资料也比较多,大家可自行百度。
另外,在MybatisPlus 3.x及以后的版本里,我们可以从源码里找到DataPermissionInterceptor数据权限处理器插件,虽然截止本文编写时(20230117),官网文档中还没有此插件的说明,但已经能百度到DataPermissionInterceptor拦截器的一些使用案例了。
个人感觉相比多租户拦截器TenantLineInnerInterceptor的用法,官方提供的数据权限拦截器DataPermissionInterceptor使用起来还是过于复杂,而且针对CRUD操作的鉴权功能也不够强大,因此参考多租户拦截器的实现原理,对数据权限拦截器进行了改造,后续有空了会将改造后的代码推荐给官方,看是否可以被采纳。见Demo源码仓库。
MybatisPlus的maven依赖:
- <dependencies>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <!-- 截止本文编写时,最新的MP版本 -->
- <version>3.5.3.1</version>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <version>8.0.29</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- <version>2.3.4.RELEASE</version>
- </dependency>
- </dependencies>

本文后续的例子中,所用的数据库结构:
- /*
- Navicat Premium Data Transfer
- Source Server : mysql8
- Source Server Type : MySQL
- Source Server Version : 80027
- Source Host : localhost:3306
- Source Schema : wsp-test
- Target Server Type : MySQL
- Target Server Version : 80027
- File Encoding : 65001
- Date: 17/01/2023 16:26:17
- */
-
- SET NAMES utf8mb4;
- SET FOREIGN_KEY_CHECKS = 0;
-
- -- ----------------------------
- -- Table structure for wsp_org
- -- ----------------------------
- DROP TABLE IF EXISTS `wsp_org`;
- CREATE TABLE `wsp_org` (
- `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
- `create_time` timestamp NOT NULL COMMENT '创建时间',
- `update_time` timestamp NOT NULL COMMENT '更新时间',
- `org_name` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '部门名称',
- `org_address` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '部门地址',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic;
-
- -- ----------------------------
- -- Table structure for wsp_role
- -- ----------------------------
- DROP TABLE IF EXISTS `wsp_role`;
- CREATE TABLE `wsp_role` (
- `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
- `create_time` timestamp NOT NULL COMMENT '创建时间',
- `update_time` timestamp NOT NULL COMMENT '更新时间',
- `tenant_id` bigint NULL DEFAULT NULL COMMENT '租户id',
- `role_name` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
- `role_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色编码',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-
- -- ----------------------------
- -- Table structure for wsp_user
- -- ----------------------------
- DROP TABLE IF EXISTS `wsp_user`;
- CREATE TABLE `wsp_user` (
- `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
- `create_time` timestamp NOT NULL COMMENT '创建时间',
- `create_by` bigint unsigned NOT NULL COMMENT '创建人',
- `update_time` timestamp NOT NULL COMMENT '更新时间',
- `tenant_id` bigint NOT NULL COMMENT '租户id',
- `org_id` bigint NOT NULL COMMENT '部门id',
- `role_id` bigint NOT NULL COMMENT '角色id',
- `name` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '姓名',
- `age` int unsigned NOT NULL DEFAULT '0' COMMENT '年龄',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-
- SET FOREIGN_KEY_CHECKS = 1;

TenantLineInnerInterceptor是MybatisPlus中提供的多租户插件,其使用方法大致分为下面三步:
对夸租户的表设置白名单,忽略多租户拦截,这些配置可以放到配置文件中进行环境配置,例如:
- tenant:
- enable: true
- column: tenant_id
- filterTables:
- ignoreTables:
- - wsp_org
- ignoreLoginNames:
例如wsp_org表结构中,没有tenant_id多租户字段,那么多租户拦截器不拦截该表。
- import org.springframework.boot.context.properties.ConfigurationProperties;
- import java.util.List;
-
- /**
- * 多租户配置类
- *
- * @author wangshaopeng@talkweb.com.cn
- * @Date 2023-01-11
- */
- @Getter
- @Setter
- @ConfigurationProperties(prefix = "tenant")
- public class TenantProperties {
- /**
- * 是否开启多租户
- */
- private Boolean enable = true;
-
- /**
- * 租户id字段名
- */
- private String column = "tenant_id";
-
- /**
- * 需要进行租户id过滤的表名集合
- */
- private List<String> filterTables;
-
- /**
- * 需要忽略的多租户的表,此配置优先filterTables,若此配置为空则启用filterTables
- */
- private List<String> ignoreTables;
-
- /**
- * 需要排除租户过滤的登录用户名
- */
- private List<String> ignoreLoginNames;
- }

实现TenantLineHandler接口
- import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
- import com.sky.wsp.mybatis.plus.utils.SecurityContextHolder;
- import com.sky.wsp.mybatis.plus.config.properties.TenantProperties;
- import net.sf.jsqlparser.expression.Expression;
- import net.sf.jsqlparser.expression.LongValue;
-
- import java.util.List;
-
- /**
- * 多租户处理类
- *
- * @author wangshaopeng@talkweb.com.cn
- * @Date 2023-01-11
- */
- public class MultiTenantHandler implements TenantLineHandler {
-
- private final TenantProperties properties;
-
- public MultiTenantHandler(TenantProperties properties) {
- this.properties = properties;
- }
-
- /**
- * 获取租户 ID 值表达式,只支持单个 ID 值
- * <p>
- *
- * @return 租户 ID 值表达式
- */
- @Override
- public Expression getTenantId() {
- Long tenantId = SecurityContextHolder.getTenantId();
- return new LongValue(tenantId);
- }
-
- /**
- * 获取租户字段名
- * <p>
- * 默认字段名叫: tenant_id
- *
- * @return 租户字段名
- */
- @Override
- public String getTenantIdColumn() {
- return properties.getColumn();
- }
-
- /**
- * 根据表名判断是否忽略拼接多租户条件
- * <p>
- * 默认都要进行解析并拼接多租户条件
- *
- * @param tableName 表名
- * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
- */
- @Override
- public boolean ignoreTable(String tableName) {
-
- //忽略指定用户对租户的数据过滤
- List<String> ignoreLoginNames=properties.getIgnoreLoginNames();
- String loginName=SecurityContextHolder.getUsername();
- if(null!=ignoreLoginNames && ignoreLoginNames.contains(loginName)){
- return true;
- }
-
- //忽略指定表对租户数据的过滤
- List<String> ignoreTables = properties.getIgnoreTables();
- if (null != ignoreTables && ignoreTables.contains(tableName)) {
- return true;
- }
-
- return false;
- }
- }

- import com.baomidou.mybatisplus.annotation.DbType;
- import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
- import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
- import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
- import com.sky.wsp.mybatis.plus.config.properties.TenantProperties;
- import com.sky.wsp.mybatis.plus.handler.DBMetaObjectHandler;
- import com.sky.wsp.mybatis.plus.handler.MultiTenantHandler;
- import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
- import org.springframework.boot.context.properties.EnableConfigurationProperties;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- /**
- * MybatisPlus配置类
- *
- * @author wangshaopeng@talkweb.com.cn
- * @Date 2023-01-11
- */
- @Configuration(proxyBeanMethods = false)
- @EnableConfigurationProperties({
- TenantProperties.class,
- DataPermissionProperties.class
- })
- public class MybatisPlusConfig {
-
- /**
- * 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
- */
- private static final Long MAX_LIMIT = 1000L;
-
- @Bean
- public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties) {
- MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
- if (Boolean.TRUE.equals(tenantProperties.getEnable())) {
- // 启用多租户拦截
- interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MultiTenantHandler(tenantProperties)));
- }
- return interceptor;
- }
-
- @Bean
- @ConditionalOnMissingBean
- public MetaObjectHandler metaObjectHandler() {
- return new DBMetaObjectHandler();
- }
-
- }

针对MybatisPlus提供的API、自定义Mapper中的statement(不清楚statement概念的同学可自行百度)我都进行了测试,均可正常拦截,下面附上一些拦截前后SQL对比的例子:
例1:使用MybatisPlus的insert方法,添加数据时会自动初始化tenant_id列
处理前 | 处理后 |
---|---|
INSERT INTO wsp_user ( create_time, create_by, update_time, org_id, role_id, NAME, age ) VALUES ( ?, ?, ?, ?, ?, ?, ? ) | INSERT INTO wsp_user ( create_time, create_by, update_time, org_id, role_id, NAME, age, tenant_id ) VALUES (?, ?, ?, ?, ?, ?, ?, 1) |
例2:使用MybatisPlus的selectById方法,添加数据时会自动初始化tenant_id列
处理前 | 处理后 |
---|---|
SELECT id, create_time, update_time, tenant_id, org_id, role_id, NAME, age FROM wsp_user WHERE id =? | SELECT id, create_time, update_time, tenant_id, org_id, role_id, NAME, age FROM wsp_user WHERE id = ? AND tenant_id = 1 |
例3:使用自定义Mapper的statement方法
处理前 | 处理后 |
---|---|
SELECT id, create_time, update_time, tenant_id, org_id, NAME, age FROM wsp_user AS USER WHERE USER.id = ? | SELECT id, create_time, update_time, tenant_id, org_id, NAME, age FROM wsp_user AS USER WHERE USER.id = ? AND USER.tenant_id = 1 |
例4:使用自定义Mapper的statement方法,进行多表关联查询
处理前 | 处理后 |
---|---|
SELECT USER .tenant_id, USER.org_id, org.org_name, org.org_address, USER.role_id, role.role_name, role.role_code, USER.id AS user_id, USER.NAME AS user_name, USER.age AS user_age FROM wsp_user AS USER LEFT JOIN wsp_org AS org ON USER.org_id = org.id LEFT JOIN wsp_role AS role ON USER.role_id = role.id WHERE USER.id = ? | SELECT USER .tenant_id, USER.org_id, org.org_name, org.org_address, USER.role_id, role.role_name, role.role_code, USER.id AS user_id, USER.NAME AS user_name, USER.age AS user_age FROM wsp_user AS USER LEFT JOIN wsp_org AS org ON USER.org_id = org.id LEFT JOIN wsp_role AS role ON USER.role_id = role.id AND role.tenant_id = 1 WHERE USER.id = ? AND USER.tenant_id = 1 |
由于官方提供的数据权限拦截器DataPermissionInterceptor,只能自己拼装SQL来实现数据鉴权,拼装SQL操作比较困难,因此参考多租户拦截器,对数据权限拦截器进行了改造,简化了使用难度,见Demo源码仓库:
注意:数据权限的id外键,在新建数据时,是无法通过拦截器进行初始化的,因为一个账号的数据权限,通常会包含多个部门,那新建数据时,到底是属于哪个部门下的数据,不好判断,因此由用户自己(开发人员)在业务代码中对数据权限id进行初始化。
类似多租户插件,数据权限插件使用方法也大致分为下面三步:
对夸部门共享的表设置白名单,忽略多数据权限拦截,这些配置可以放到配置文件中进行环境配置,例如:
- data:
- permission:
- enable: true
- # 创建人的标记列
- dataCreatorColumn: create_by
- # 部门数据权限的标记列
- dataPermissionIdColumn: org_id
- filterTables:
- ignoreTables:
- # 不进行数据鉴权拦截的表
- - wsp_org
- - wsp_role
- ignoreLoginNames:
例如wsp_org、wsp_role表结构中,没有org_id部门外键,那么数据权限拦截器不拦截该表。
- import org.springframework.boot.context.properties.ConfigurationProperties;
-
- import java.util.List;
-
-
- /**
- * 数据权限配置类
- *
- * @author wangshaopeng@talkweb.com.cn
- * @Date 2023-01-11
- */
- @Getter
- @Setter
- @ConfigurationProperties(prefix = "data.permission")
- public class DataPermissionProperties {
- /**
- * 是否开启数据权限拦截
- */
- private Boolean enable = true;
-
- /**
- * 数据创建人字段名
- */
- private String dataCreatorColumn = "creator";
-
- /**
- * 数据权限id字段名
- */
- private String dataPermissionIdColumn = "permission_id";
-
- /**
- * 需要进行数据权限id过滤的表名集合
- */
- private List<String> filterTables;
-
- /**
- * 需要忽略的多数据权限的表,此配置优先filterTables,若此配置为空则启用filterTables
- */
- private List<String> ignoreTables;
-
- /**
- * 需要排除数据权限过滤的登录用户名
- */
- private List<String> ignoreLoginNames;
- }

实现MyDataPermissionHandler接口,(这个接口也是参考多租户的接口新建的)
- import com.sky.wsp.mybatis.plus.config.properties.DataPermissionProperties;
- import com.sky.wsp.mybatis.plus.plugins.handler.MyDataPermissionHandler;
- import com.sky.wsp.mybatis.plus.utils.SecurityContextHolder;
-
- import java.util.List;
-
- /**
- * 基于用户组织机构(Org)的数据权限处理类
- *
- * @author wangshaopeng@talkweb.com.cn
- * @Date 2023-01-11
- */
- public class OrgDataPermissionHandler implements MyDataPermissionHandler {
-
- private final DataPermissionProperties properties;
-
- public OrgDataPermissionHandler(DataPermissionProperties properties) {
- this.properties = properties;
- }
-
- @Override
- public Long getDataCreator() {
- // user_id作为creator
- return SecurityContextHolder.getUserId();
- }
-
- @Override
- public String getDataCreatorColumn() {
- // user_id作为creator
- return properties.getDataCreatorColumn();
- }
-
- @Override
- public List<Long> getDataPermissionIdSet() {
- // org_id作为数据权限
- return SecurityContextHolder.getOrgIds();
- }
-
- @Override
- public String getDataPermissionIdColumn() {
- // org_id作为数据权限
- return properties.getDataPermissionIdColumn();
- }
-
- @Override
- public boolean ignoreTable(String tableName) {
-
- //忽略指定用户对数据权限的过滤
- List<String> ignoreLoginNames=properties.getIgnoreLoginNames();
- String loginName=SecurityContextHolder.getUsername();
- if(null!=ignoreLoginNames && ignoreLoginNames.contains(loginName)){
- return true;
- }
-
- //忽略指定表对数据权限的过滤
- List<String> ignoreTables = properties.getIgnoreTables();
- if (null != ignoreTables && ignoreTables.contains(tableName)) {
- return true;
- }
- return false;
- }
- }

MyDataPermissionInterceptor就是参考多租户拦截器实现的数据权限拦截器,核心逻辑都在这个类里。
- import com.baomidou.mybatisplus.annotation.DbType;
- import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
- import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
- import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
- import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
- import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
- import com.sky.wsp.mybatis.plus.config.properties.DataPermissionProperties;
- import com.sky.wsp.mybatis.plus.config.properties.TenantProperties;
- import com.sky.wsp.mybatis.plus.handler.DBMetaObjectHandler;
- import com.sky.wsp.mybatis.plus.handler.MultiTenantHandler;
- import com.sky.wsp.mybatis.plus.handler.OrgDataPermissionHandler;
- import com.sky.wsp.mybatis.plus.plugins.inner.MyDataPermissionInterceptor;
- import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
- import org.springframework.boot.context.properties.EnableConfigurationProperties;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- /**
- * MybatisPlus配置类
- *
- * @author wangshaopeng@talkweb.com.cn
- * @Date 2023-01-11
- */
- @Configuration(proxyBeanMethods = false)
- @EnableConfigurationProperties({
- TenantProperties.class,
- DataPermissionProperties.class
- })
- public class MybatisPlusConfig {
-
- /**
- * 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
- */
- private static final Long MAX_LIMIT = 1000L;
-
- @Bean
- public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties, DataPermissionProperties dataPermissionProperties) {
- MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
- if (Boolean.TRUE.equals(tenantProperties.getEnable())) {
- // 启用多租户拦截
- interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MultiTenantHandler(tenantProperties)));
- }
- if (Boolean.TRUE.equals(dataPermissionProperties.getEnable())) {
- // 启用数据权限拦截
- interceptor.addInnerInterceptor(new MyDataPermissionInterceptor(new OrgDataPermissionHandler(dataPermissionProperties)));
- }
- // 分页拦截
- PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
- paginationInterceptor.setMaxLimit(MAX_LIMIT);
- interceptor.addInnerInterceptor(paginationInterceptor);
- // 乐观锁拦截
- interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
- return interceptor;
- }
-
- @Bean
- @ConditionalOnMissingBean
- public MetaObjectHandler metaObjectHandler() {
- return new DBMetaObjectHandler();
- }
-
- }

针对MybatisPlus提供的API、自定义Mapper中的statement(不清楚statement概念的同学可自行百度)我都进行了测试,均可正常拦截,下面附上一些拦截前后SQL对比的例子:
处理前 | 处理后 |
---|---|
SELECT USER .tenant_id, USER.org_id, org.org_name, org.org_address, USER.role_id, role.role_name, role.role_code, USER.id AS user_id, USER.NAME AS user_name, USER.age AS user_age FROM wsp_user AS USER LEFT JOIN wsp_org AS org ON USER.org_id = org.id LEFT JOIN wsp_role AS role ON USER.role_id = role.id WHERE USER.id = ? | SELECT USER .tenant_id, USER.org_id, org.org_name, org.org_address, USER.role_id, role.role_name, role.role_code, USER.id AS user_id, USER.NAME AS user_name, USER.age AS user_age FROM wsp_user AS USER LEFT JOIN wsp_org AS org ON USER.org_id = org.id LEFT JOIN wsp_role AS role ON USER.role_id = role.id AND role.tenant_id = 1 WHERE USER.id = ? AND USER.tenant_id = 1 AND ( create_by = 1 OR USER.org_id IN ( 4, 5, 6 )) |
其他的增删改查的例子,同多租户拦截器,这里就不赘述了。
在一些场景下,无需多租户拦截、无需数据鉴权拦截,或者对于一些超级管理员使用的接口,希望夸租户查询、免数据鉴权时,可以通过下面几种方式实现忽略拦截:
- /**
- * 使用@InterceptorIgnore注解,忽略多租户拦截 <br/>
- * 注解@InterceptorIgnore可以用在Mapper类上,也可以用在方法上
- *
- * @param id
- * @return
- */
- @InterceptorIgnore(tenantLine = "true")
- UserOrgVO myFindByIdNoTenant(@Param(value = "id") Long id);
- tenant:
- enable: true
- column: tenant_id
- filterTables:
- ignoreTables:
- # 不进行多租户拦截的表
- - wsp_org
- ignoreLoginNames:
- # 这里配置了ID,需要使用username的,可在MultiTenantHandler中自己实现判断逻辑
- - 99
- - 98
-
- data:
- permission:
- enable: true
- dataCreatorColumn: create_by
- dataPermissionIdColumn: org_id
- filterTables:
- ignoreTables:
- # 不进行数据鉴权拦截的表
- - wsp_org
- - wsp_role
- ignoreLoginNames:
- # 这里配置了ID,需要使用username的,可在OrgDataPermissionHandler中自己实现判断逻辑
- - 99
- - 98

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。