Spring Boot + MyBatis-Plus 插件(数据权限实战)
Spring Boot + MyBatis-Plus 插件(数据权限实战)
一、数据权限概念
1.1 什么是数据权限?
数据权限(Data Permission)是系统根据用户角色、部门或其他业务规则动态控制数据访问范围的技术。其核心目标是确保用户仅能访问其权限范围内的数据,避免越权操作。常见场景包括:
- 多租户隔离:不同租户的数据物理或逻辑隔离。
- 部门数据管控:用户仅能查看本部门或下属部门的数据。
- 角色权限分级:根据角色级别限制数据可见性(如普通员工与经理的视图差异)。
二、MyBatis-Plus 数据权限实现原理
2.1 DataPermissionHandler 工作机制
DataPermissionHandler 是 MyBatis-Plus 提供的接口,用于在 SQL 解析阶段动态注入权限条件。其核心流程如下:
- SQL 解析:MyBatis-Plus 解析 Mapper 方法生成的 SQL 语句。
- 条件注入:调用 getSqlSegment 方法,将自定义条件拼接到原始 WHERE 子句。
- 执行优化:处理后的 SQL 传递至后续插件(如分页插件),生成最终执行语句。
三、完整实现步骤
3.1 环境准备
依赖配置
com.baomidou mybatis-plus-boot-starter 3.5.3.1 com.baomidou mybatis-plus-extension 3.5.3.1
3.2 自定义数据权限处理器
3.2.1自定义注解
package cn.sdlnrj.foundation.common.annotation; import java.lang.annotation.*; @Inherited @Target({ ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CustomDataPermission { /** * 是否进行数据权限 */ public boolean isRole() default false; /** * 权限字段 */ public String permissionField() default ""; /** * 权限方式(根据 用户、角色、部门等) */ public PermissionTypeEnum permissionType() default PermissionTypeEnum.USER; }
3.2.2数据权限获取的具体实现方法
@Getter @AllArgsConstructor public enum PermissionTypeEnum { USER("用户", 10) { /** * 获取当前用户创建的数据 * @param classAnnotation * @return */ @Override public Expression getPermission(CustomDataPermission classAnnotation) { //1.查询判断当前用户的角色,比如管理员角色不需要拼接数据权限 EqualsTo equalsTo = new EqualsTo(); equalsTo.setRightExpression(new StringValue("获取当前登录用户账号")); if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) { equalsTo.setLeftExpression(new Column(classAnnotation.permissionField())); } return equalsTo; } }, DEPT("部门", 20) { @Override public Expression getPermission(CustomDataPermission classAnnotation) { EqualsTo equalsTo = new EqualsTo(); //获取当前用户的部门权限 equalsTo.setRightExpression(new StringValue("获取当前用户的部门权限")); if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) { equalsTo.setLeftExpression(new Column(classAnnotation.permissionField())); } return equalsTo; } }, ROLE("角色", 30) { @Override public Expression getPermission(CustomDataPermission classAnnotation) { EqualsTo equalsTo = new EqualsTo(); equalsTo.setRightExpression(new StringValue("获取当前用户的角色权限")); if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) { equalsTo.setLeftExpression(new Column(classAnnotation.permissionField())); } return equalsTo; } }, POSITION("职务", 40) { @Override public Expression getPermission(CustomDataPermission classAnnotation) { EqualsTo equalsTo = new EqualsTo(); equalsTo.setRightExpression(new StringValue("获取当前用户的角色权限")); if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) { equalsTo.setLeftExpression(new Column(classAnnotation.permissionField())); } return equalsTo; } }; final String name; final Integer code; public abstract Expression getPermission(CustomDataPermission classAnnotation); public static Expression handle(CustomDataPermission classAnnotation) { PermissionTypeEnum[] values = PermissionTypeEnum.values(); for (PermissionTypeEnum emissionStatus : values) { if(emissionStatus.equals(classAnnotation.permissionType())){ return emissionStatus.getPermission(classAnnotation); } } return new EqualsTo(); } }
根据自己业务需求,可增加权限黑名单,也可增加白名单,这里不做白名单实例了。
data-permissions: blackList: -接口地址
@Configuration @ConfigurationProperties(prefix = "data-permissions") public class WhiteListConfig { private List blackList; public List getBlackList() { return blackList; } public void setBlackList(List blackList) { this.blackList= blackList; } public Boolean not(String url) { List collect = blackList.stream().filter(white -> url.contains(white)).collect(Collectors.toList()); if (CollectionUtils.isEmpty(collect)) { return false; } return true; } }
@Slf4j public class MyDataPermissionHandler implements DataPermissionHandler { /** * @param where 原SQL Where 条件表达式 * @param mappedStatementId Mapper接口方法ID * @return */ @SneakyThrows @Override public Expression getSqlSegment(Expression where, String mappedStatementId) { // 获取当前用户权限上下文(示例:部门ID和租户ID) UserContext user = UserContextHolder.getCurrentUser(); //如果调用mybatisplus查询单一的sql,则不进行数据权限拼接。 Class clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf("."))); if (StringUtils.isNotBlank(mappedStatementId)) { String substring = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1); if (substring.equals("selectById")) { return where; } } //判断访问的url是否在黑名单中 boolean isPreventAddress = SpringUtil.getBean(WhiteListConfig.class).not(WebUtil.getRequest().getRequestURI()); if (isPreventAddress) { return where; } //判断mapper上是否有注解,CustomDataPermission是用在mapper上,标识操作sql语句的别名,以及mapper是否需要走数据权限。 CustomDataPermission classAnnotation = clazz.getAnnotation(CustomDataPermission.class); if (Objects.isNull(classAnnotation)) { return where; } // 创建 AND 表达式 拼接Where 和 = 表达式 return new AndExpression(where, PermissionTypeEnum.handle(classAnnotation)); } }
3.3 插件配置与顺序控制
3.3.1. 多插件共存时的顺序规则
插件类型 推荐顺序 说明 数据权限插件 1 确保 WHERE 条件在所有分页、租户逻辑之前生效。 多租户插件 2 若同时使用多租户插件,应放在数据权限插件之后。 分页插件 3 最后执行,确保 LIMIT 和 COUNT 基于最终 SQL。 乐观锁插件 4 不影响 SQL 结构,顺序无严格要求。 完整配置示例:
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 1. 数据权限插件 interceptor.addInnerInterceptor(new DataPermissionInterceptor(...)); // 2. 多租户插件 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(...)); // 3. 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 4. 乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }
3.4 权限上下文管理
public class UserContextHolder { private static final TransmittableThreadLocal contextHolder = new TransmittableThreadLocal(); public static void setUserContext(UserContext user) { contextHolder.set(user); } public static UserContext getCurrentUser() { return contextHolder.get(); } public static void clear() { contextHolder.remove(); } } // 示例拦截器设置上下文 @Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { UserContext user = extractUser(request); UserContextHolder.setUserContext(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContextHolder.clear(); } }
四、常见问题与解决方案
4.1 分页总数不准确
问题原因:COUNT 查询未应用数据权限条件。
解决方案:
- 检查插件执行顺序,确保数据权限插件在分页插件之前。
- 手动指定 COUNT 查询:
@Select("SELECT * FROM user WHERE ${ew.customSqlSegment}") @Select("SELECT COUNT(*) FROM user WHERE ${ew.customSqlSegment}") IPage selectCustomPage(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper);
4.2 LIMIT 子句丢失
问题原因:分页插件未正确配置或执行顺序错误。
解决方案:
- 确保 PaginationInnerInterceptor 已添加到拦截器链。
- 验证插件顺序:数据权限插件 → 分页插件。
4.3 多表关联查询权限失效
问题原因:权限条件未正确附加到关联表。
解决方案:
- 在条件中指定表别名:
String condition = "u.dept_id = 2"; // u 为 user 表别名
- 使用 SQL 解析器动态处理别名。
(图片来源网络,侵删)(图片来源网络,侵删)
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。