不知道在座的亦菲、彦祖们有没有遇到这样的需求,要在某块业务加上数据权限控制,规定哪些人能访问,哪些人不能访问。像这样的数据权限的控制该怎么实现呢?可以使用基于Mybatis-Plus的插件机制来实现。话不多说,我们直接切入主题,开始实现
代码
数据库准备
-- 插入部门表
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门名称',
`code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门编码',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '部门描述',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `code`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- 插入用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '密码',
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '手机号',
`department_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- 插入用户数据
INSERT INTO `user` VALUES (1, 'testuser1', '123456', '13800138001', 1, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (2, 'testuser2', '123456', '13800138002', 2, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (3, 'testuser3', '123456', '13800138003', 3, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (4, 'tech_user1', '123456', '13800138004', 1, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (5, 'tech_user2', '123456', '13800138005', 1, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (6, 'sales_user1', '123456', '13800138006', 2, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (7, 'sales_user2', '123456', '13800138007', 2, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (8, 'hr_user1', '123456', '13800138008', 3, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (9, 'finance_user1', '123456', '13800138009', 4, '2026-01-19 20:48:31');
-- 插入部门数据
INSERT INTO `department` (`id`, `name`, `code`, `description`, `create_time`) VALUES
(1, '技术部', 'TECH', '技术研发部门', CURRENT_TIMESTAMP),
(2, '销售部', 'SALES', '销售市场部门', CURRENT_TIMESTAMP),
(3, '人力资源部', 'HR', '人力资源管理部门', CURRENT_TIMESTAMP),
(4, '财务部', 'FINANCE', '财务管理部门', CURRENT_TIMESTAMP);
在pom.xml引入依赖
<!--MyBatis-Plus扩展包(用于拦截器功能)-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- SQL解析器(用于数据权限拦截器) -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.6</version>
</dependency>
<!--Lombok(如果使用@Slf4j注解)-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
样例
//user对象
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String phone;
private Long departmentId;
private LocalDateTime createTime;
}
//mapper
@Mapper
public interface UserDao extends BaseMapper<User> {
@DataPermission //数据权限注解
@Select("select * from user")
List<User> selectUserList();
}
//service
public interface UserService extends IService<User> {
List<User> listUsersWithPermission();
}
//serviceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService {
@Autowired
private UserDao userDao;
@Override
public List<User> listUsersWithPermission() {
return userDao.selectUserList();
}
}
//controller
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 获取所有用户 - 应用数据权限过滤
* 只有当前用户所在部门的用户数据会被返回
*/
@GetMapping
public List<User> list() {
return userService.listUsersWithPermission();
}
}
数据权限注解
自定义注解-@DataPermission权限拦截器(核心)-DataPermissionInterceptorMybatis拦截器容器 -MybatisPlusInterceptorSQL解析器 -JSqlParser
//注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
/**
* 部门字段名
*/
String departmentField() default "department_id";
}
//权限拦截器
@Slf4j
@Component
public class DataPermissionInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 获取当前用户信息(这里需要从SecurityContext或其他地方获取当前登录用户)
Long currentUserDepartmentId = getCurrentUserDepartmentId();
if (currentUserDepartmentId == null) {
return; // 如果没有部门信息,不进行数据权限过滤
}
// 检查方法是否标注了@DataPermission注解
String methodName = ms.getId();
boolean hasDataPermission = hasDataPermissionAnnotation(methodName);
if (!hasDataPermission) {
return; // 如果没有注解,不进行数据权限过滤
}
// 解析SQL并添加部门过滤条件
String originalSql = boundSql.getSql();
try {
String modifiedSql = addDepartmentFilter(originalSql, currentUserDepartmentId);
// 使用反射修改BoundSql中的SQL
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, modifiedSql);
log.info("数据权限拦截器修改SQL: {}", modifiedSql);
} catch (Exception e) {
log.error("数据权限拦截器处理SQL失败", e);
}
}
/**
* 获取当前用户的部门ID
*/
private Long getCurrentUserDepartmentId() {
//模拟获取当前用户部门ID,实际应该从SecurityContext或ThreadLocal获取
return 1L;
}
private Boolean hasDataPermissionAnnotation(String methodName){
try {
//通过方法名解析类名和方法名
int lastDotIndex = methodName.lastIndexOf(".");
String className = methodName.substring(0, lastDotIndex);
String methodNameOnly = methodName.substring(lastDotIndex + 1);
Class<?> clazz = Class.forName(className);
Method method = clazz.getMethod(methodNameOnly);
return method.isAnnotationPresent(DataPermission.class);
}catch (Exception e){
log.warn("检查数据权限注解失败: {}", methodName, e);
return false;
}
}
/**
* 添加部门过了条件到SQL中
*/
private String addDepartmentFilter(String sql,Long departmentId) throws JSQLParserException{
Select select = (Select) CCJSqlParserUtil.parse(sql);
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect){
PlainSelect plainSelect = (PlainSelect) selectBody;
Expression where = plainSelect.getWhere();
Column departmentColumn = new Column("department_id");
LongValue departmentValue = new LongValue(departmentId);
EqualsTo departmentFilter = new EqualsTo(departmentColumn, departmentValue);
if (where ==null){
plainSelect.setWhere(departmentFilter);
}else {
AndExpression andExpression = new AndExpression(where,departmentFilter);
plainSelect.setWhere(andExpression);
}
}
return select.toString();
}
}
//Mybatis-plus的配置
@Component
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加数据权限插件
interceptor.addInnerInterceptor(new DataPermissionInterceptor());
return interceptor;
}
}
测试
@SpringBootTest
@Slf4j
class DemoApplicationTests {
@Autowired
private UserController userController;
@Test
void contextLoads() {
List<User> list = userController.list();
log.info("查询结果:{}", list);
}
}
输出:通过数据拦截器修改了SQL,所以人员只能看到本部门的数据
数据权限拦截器修改SQL: SELECT * FROM user WHERE department_id = 1
查询结果:[User(id=1, username=testuser1, password=123456, phone=13800138001, departmentId=1, createTime=2026-01-19T20:48:31), User(id=4, username=tech_user1, password=123456, phone=13800138004, departmentId=1, createTime=2026-01-19T20:48:31), User(id=5, username=tech_user2, password=123456, phone=13800138005, departmentId=1, createTime=2026-01-19T20:48:31)]
工作机制
- 当
DAO方法被标注了@DataPermission注解时 - 从当前用户上下文获取部门ID(目前示例中是硬编码1)
- 将
SELECT * FROM user改为SELECT * FROM user WHERE department_id = 1 - 只返回当前用户所在部门的数据
原理
Mybatis-Plus的插件机制是基于责任链模式和装饰器模式,是对Mybatis原生拦截器机制的增强封装
其插件的注册流程
Spring容器启动-> MybatisPlusConfig配置类->创建MybatisPlusInterceptor实例->调用addInnerInterceptor添加插件-> 插件被注册到内部列表(interceptors)中
注意:插件的执行顺序是,谁先注册到插件容器中,谁先执行
SQL执行的拦截流程
在描述拦截流程前,简单阐述下以下几个Mybatis底层的重要类
重要类
Mybatis底层允许拦截的类方法只有四个Executor、StatementHandler、ParameterHandler、ResultSetHandler。为啥时这四个,因为它们覆盖了SQL执行的全生命周期核心环节。
Executor调度执行StatementHandler 进行SQL构建,StatementHandler 设置SQL中占位符参数、ResultSetHandler封装执行SQL后返回的结果。所以我们只需要拦截以上类的方法,就能够拦截SQL执行流程了
Interceptor : 插件开发的接口,定义插件拦截逻辑和代理规则
InterceptorChain: 拦截器的【容量+责任链管理器】,负责管理所有拦截器并生成嵌套代理
Plugin: 动态代理的【生成器+处理器】,实现方法的拦截,插件的底层核心
流程解析
-
生成Mapper代理类:当调用我们文中
userDao.selectUserList的时候,mybatis会通过JDK的动态代理生成MybatisMapperProxy代理类进行处理 -
Mapper代理类执行:
MybatisMapperProxy代理类根据方法类型执行对应的逻辑,通过SqlSession执行selectList方法 -
生成
SqlSession执行,其调用过程:- 在执行
selectList方法前会生成SQLSessionProxy代理,代理会通过工厂类获取DefaultSqlSession实例 DefaultSqlSession的会从配置数据源中获取连接,并通过Configuration类创建Executor实例
- 在执行
-
Configuration类执行:interceptorChain.pluginAll方法,为生成当前动态代理对象,触发Plugin插件的plugin方法 -
循环嵌套代理(核心处理逻辑):InterceptorChain类内部维护插件容器,遍历所有注册在内的插件实例Interceptor,并调用插件中plugin方法为当前target(Executor)生成嵌套的动态代理。InterceptorChain是用责任链模式,核心逻辑://InterceptorChain类 public Object pluginAll(Object target) { Interceptor interceptor; for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) { interceptor = (Interceptor)var2.next(); //嵌套代理 } return target; } //MybatisPlusInterceptor类 public Object plugin(Object target) { return !(target instanceof Executor) && !(target instanceof StatementHandler) ? target : Plugin.wrap(target, this); } //原生Plugin代理类 public static Object wrap(Object target, Interceptor interceptor) { // 1. 解析拦截器的 @Signature 注解,获取拦截规则 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 2. 获取目标对象中匹配拦截规则的接口(必须是接口,JDK动态代理要求) Class<?> type = target.getClass(); // 3. 如果有匹配的接口,生成动态代理对象;否则返回原对象(无需代理) Class<?>[] interfaces = getAllInterfaces(type, signatureMap); return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target; }看到这会有同学想问什么是循环嵌套代理,循环嵌套代理是
循环: 检索所有注册到容器中的插件(如:
MybatisPlusInterceptor或自定义插件类)嵌套:代码
interceptor = (Interceptor) var2.next()而 Interceptor又执行Plugin.wrap(), 所以代码是target = Plugin.wrap(target, interceptor)在循环的过程中一层套一层代理:通过JDK的动态代理对target进行代理
所以使用循环嵌套代理是让所有的插件都可以拦截代理 target对象(
Excutor)方法,使得在执行target对象方法时可以使用代理的所有注册的插件功能如果不使用循环嵌套代理,
pluginAll方法的执行结果只会让最后一个插件代理生效 -
SqlSessionInterceptor执行调用:method.invoke -
在target目标对象被调用之前,通过
JDK动态代理的方式会执行到plugin插件的intercept方法public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass()); return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args); } catch (Exception var5) { throw ExceptionUtil.unwrapThrowable(var5); } } -
Plugin代理开始执行插件类的方法:MybatisMapperPlusInterceptor插件类内部维护了interceptors容器,遍历所有注册到容器的InnerInterceptor实例- 调用容器中的实例:就是我们实现的
DataPermissionInterceptor - 执行
beforeQuery方法:添加数据权限 - 执行其他插件的逻辑
-
开始执行其他可以被拦截的方法:
ParameterHandler、ResultSetHander、StatementHandler -
流程结束
总结
通过Mybatis-Plus的插件和Mybatis的拦截器机制,我们可以实现很多强大的功能,比如添加租户ID、分库分表、数据脱敏等等。