MyBatis Plus 拦截器实现数据权限控制(完整版)

news/2024/11/8 16:43:56/

一、说明

变化:相比于之前写的数据权限拦截器,新增了白名单功能,通过注解的方式让哪些SQL不进行数据权限拦截,之前的文章地址

思路:通过MyBatisPlus的拦截器对每个要执行的SQL进行拦截,然后判断其是否为查询语句,如果是查询语句,则继续判断,其类或者方法上是否存在@DataScopeIgnore注解,如果存在则不做任何处理,反之对原始SQL进行解析,并获取该用户的角色,并获取其针对该接口设置的数据权限信息,改造原始的查询条件,以此来实现数据权限的控制

缺陷:目前@DataScopeIgnore注解只有作用于Mapper层才能生效,不过按理说应该是满足一般数据权限控制的要求

代码地址,其中包含一些暂时未使用的代码,是为了后续做单点登录准备的,所有涉及代码在下面已经全部提及

二、拦截器

package com.xx.permission.config;import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.xx.permission.entity.result.DataPermission;
import com.xx.permission.utils.ExpressionUtils;
import com.xx.permission.utils.UserUtils;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Map;/*** @author aqi* @describe 数据权限拦截器*/
@Slf4j
@Component
public class DataScopeInterceptor extends JsqlParserSupport implements InnerInterceptor {@Overridepublic void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);MappedStatement ms = mpSh.mappedStatement();SqlCommandType sct = ms.getSqlCommandType();if (sct == SqlCommandType.SELECT) {if (this.judgementDataScopeIgnore(ms)) {return;}PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();mpBs.sql(parserMulti(mpBs.sql(), null));}}/*** 查询*/@Overrideprotected void processSelect(Select select, int index, String sql, Object obj) {SelectBody selectBody = select.getSelectBody();PlainSelect plainSelect = (PlainSelect) selectBody;// 获取表名/别名(如果是关联查询是取第一个join左侧的表名/别名)String tableName = ExpressionUtils.getTableName(plainSelect);// 构建用户权限控制条件this.buildUserPermissionSql(plainSelect, sql, tableName);}/*** 构建用户权限控制条件* 思路:*      1、获取当前用户信息,再通过用户信息获取到角色对应的数据权限集合*      2、通过获取到的该用户所有的数据权限集合过滤出本次请求的数据权限*      3、将本次请求的数据权限拼接到已有的SQL中* @param plainSelect 用于解析SQL的类* @param tableName 表名/别名(join查询左侧表名)*/private void buildUserPermissionSql(PlainSelect plainSelect, String sql, String tableName) {// 获取当前用户接口数据权限(这里的数据都是模拟的,实际上可能得从缓存或者session中获取)Map<String, List<DataPermission>> userPermissionMap = UserUtils.getUserPermission();// 获取本次请求的uriString uri = UserUtils.getUri();// 获取本次请求uri对应的数据权限集合List<DataPermission> dataPermissions = userPermissionMap.get(uri);if (!CollectionUtils.isEmpty(dataPermissions)) {// 将多个条件合并在一起dataPermissions.forEach(permission -> {InExpression inExpression = ExpressionUtils.buildInSql(tableName, permission);ExpressionUtils.appendExpression(plainSelect, inExpression);});}log.info("[DataScopeInterceptor]请求uri:[{}],原始SQL:[{}]处理后SQL:[{}]", uri, sql, plainSelect);}/*** 判断类/方法上是否存在DataScopeIgnore注解* 存在的问题:这里只能获取到Mapper层中的一些东西,所以在别的地方加@DataScopeIgnore也不会生效* @param ms ms* @return 是否需要进行数据权限控制,true:不需要,false:需要*/private boolean judgementDataScopeIgnore(MappedStatement ms) {try {String id = ms.getId();String classPath = id.substring(0, id.lastIndexOf("."));Class<?> aClass = Class.forName(classPath);// 判断该类上是否存在DataScopeIgnore注解DataScopeIgnore declaredClassAnnotation = aClass.getDeclaredAnnotation(DataScopeIgnore.class);if (declaredClassAnnotation != null) {return true;}// 判断该方法上是否存在DataScopeIgnore注解String methodName = id.substring(id.lastIndexOf(".") + 1);Method method = aClass.getMethod(methodName);DataScopeIgnore declaredMethodAnnotation = method.getDeclaredAnnotation(DataScopeIgnore.class);return declaredMethodAnnotation != null;} catch (ClassNotFoundException | NoSuchMethodException e) {// 解析DataScopeIgnore出现异常,默认当作需要做数据权限控制return false;}}}

三、将拦截器加入MyBatisPlus拦截器

package com.xx.permission.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;/*** @author aqi* @since 2023/5/15 14:05*/
@Slf4j
@Configuration
@Component
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 初始化Mybatis Plus拦截器MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 自定义数据拦截器interceptor.addInnerInterceptor(new PaginationInnerInterceptor());interceptor.addInnerInterceptor(new DataScopeInterceptor());return interceptor;}
}

四、创建自定义白名单注解

package com.xx.permission.config;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author aqi* @describe 不进行数据权限控制* @since 2023/6/8 14:44*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScopeIgnore {}

五、JsqlParser工具类

package com.xx.permission.utils;import com.xx.permission.entity.result.DataPermission;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;import java.util.Objects;
import java.util.stream.Collectors;/*** @author aqi* @since 2023/5/17 10:16* @describe JSqlParser工具类,用于通过API的方式操作SQL语句*/
public class ExpressionUtils {private final static String LONG_TYPE = "long";private final static String STRING_TYPE = "string";/*** 构建in sql* @param tableName 表名* @param permission 字段权限* @return InExpression*/public static InExpression buildInSql(String tableName, DataPermission permission) {// 把集合转变为JSQLParser需要的元素列表ItemsList itemsList = ExpressionUtils.handleFieldType(permission);// 创建IN表达式对象,传入列名及IN范围列表return new InExpression(new Column(tableName + "." + permission.getField()), itemsList);}/*** 构建查询语句之前,判断字段类型* @param permission 字段权限*/private static ItemsList handleFieldType(DataPermission permission) {String fieldType = permission.getFieldType();if (Objects.equals(fieldType, LONG_TYPE)) {return new ExpressionList(permission.getValue().stream().map(LongValue::new).collect(Collectors.toList()));} else {return new ExpressionList(permission.getValue().stream().map(StringValue::new).collect(Collectors.toList()));}}/*** 构建eq sql* @param columnName 字段名称* @param value 字段值* @return EqualsTo*/public static EqualsTo buildEq(String columnName, String value) {return new EqualsTo(new Column(columnName), new StringValue(value));}/*** 获取表名/别名* @param plainSelect plainSelect* @return 表名/别名*/public static String getTableName(PlainSelect plainSelect) {// 获取别名Table table= (Table) plainSelect.getFromItem();Alias alias = table.getAlias();return null == alias ? table.getName() : alias.getName();}/*** 将2个where条件拼接到一起* @param plainSelect plainSelect* @param appendExpression 待拼接条件*/public static void appendExpression(PlainSelect plainSelect, Expression appendExpression) {Expression where = plainSelect.getWhere() == null ? appendExpression : new AndExpression(plainSelect.getWhere(), appendExpression);plainSelect.setWhere(where);}
}

六、用户信息工具类

目前是简单的用户工具类,按照实际情况需要调整

package com.xx.permission.utils;import com.xx.permission.config.CacheData;
import com.xx.permission.entity.result.DataPermission;
import com.xx.permission.entity.result.RoleDataPermission;
import com.xx.permission.entity.result.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** @author aqi* @since 2023/5/17 14:20*/
@Slf4j
public class UserUtils {public static UserDTO currentUser;/*** 获取当前用户信息* @return 用户信息*/public static UserDTO getCurrentUserInfo() {return currentUser;}/*** 模拟登录之后往session或者redis中存储用户信息的过程* @param user 用户信息*/public static void setCurrentUser(UserDTO user) {UserUtils.currentUser = user;}/*** 获取用户权限信息* @return 用户权限信息*/public static Map<String, List<DataPermission>> getUserPermission() {// 获取当前用户信息UserDTO currentUserInfo = UserUtils.getCurrentUserInfo();// 获取当前用户角色下的所有接口数据权限集合List<RoleDataPermission> roleDataPermissionList = CacheData.ROLE_DATA_PERMISSION_MAP.get(currentUserInfo.getRoleId());// key:uri(接口地址),value:DataPermission(数据权限)return roleDataPermissionList.stream().collect(Collectors.toMap(RoleDataPermission::getUri, RoleDataPermission::getDataPermissionList));}/*** 获取本次请求的uri* @return uri*/public static String getUri() {// 获取此次请求的uriString uri = "";RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();if (null != requestAttributes) {HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();uri = request.getRequestURI();}return uri;}
}

七、初始化用户权限缓存

缓存

package com.xx.permission.config;import com.xx.permission.entity.result.RoleDataPermission;import java.util.List;
import java.util.Map;/*** @author xiaxing* @describe* @since 2023/5/18 15:06*/
public class CacheData {/*** 角色权限信息*/public static List<RoleDataPermission> ROLE_DATA_PERMISSION_LIST;/*** 角色权限信息(key:角色ID,value:角色权限信息)*/public static Map<Long, List<RoleDataPermission>> ROLE_DATA_PERMISSION_MAP;}

初始化初始化角色数据权限

package com.xx.permission.config;import com.alibaba.fastjson.JSONArray;
import com.xx.permission.entity.result.DataPermission;
import com.xx.permission.entity.result.RoleDataPermission;
import com.xx.permission.service.SysRoleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;/*** @author aqi* @since 2023/5/18 14:48* @describe 初始化配置数据*/
@Slf4j
@Component
public class InitConfig {@Resourceprivate SysRoleService sysRoleService;@PostConstructpublic void init() {this.initRoleDataPermission();}/*** 初始化角色数据权限*/private void initRoleDataPermission() {log.info("初始化角色数据权限...");List<RoleDataPermission> roleDataPermission = sysRoleService.getRoleDataPermission();for (RoleDataPermission dataPermission : roleDataPermission) {List<DataPermission> dataPermissions = JSONArray.parseArray(dataPermission.getDataPermission(), DataPermission.class);dataPermission.setDataPermissionList(dataPermissions);}CacheData.ROLE_DATA_PERMISSION_LIST = roleDataPermission;CacheData.ROLE_DATA_PERMISSION_MAP = CacheData.ROLE_DATA_PERMISSION_LIST.stream().collect(Collectors.groupingBy(RoleDataPermission::getRoleId));}}

八、其他代码生成器生成代码

8.1、角色权限

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.xx.permission.mapper.SysRoleMapper"><select id="getRoleDataPermission" resultType="com.xx.permission.entity.result.RoleDataPermission">SELECTsys_role.id roleId,sys_role.role_name roleName,sys_role.DESCRIBE roleDescribe,sys_role_api_permission.data_permission dataPermission,sys_api.uriFROMsys_roleINNER JOIN sys_role_api_permission ON sys_role.id = sys_role_api_permission.role_idINNER JOIN sys_api ON sys_api.id = sys_role_api_permission.api_id</select></mapper>

这里在方法上加上了@DataScopeIgnore注解,避免获取角色权限时受到数据权限拦截的影响

package com.xx.permission.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xx.permission.config.DataScopeIgnore;
import com.xx.permission.entity.SysRole;
import com.xx.permission.entity.result.RoleDataPermission;
import org.apache.ibatis.annotations.Mapper;import java.util.List;/*** <p>* 角色表 Mapper 接口* </p>** @author aqi* @since 2023-05-18*/
@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {@DataScopeIgnoreList<RoleDataPermission> getRoleDataPermission();}

实现类

package com.xx.permission.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xx.permission.entity.SysRole;
import com.xx.permission.entity.result.RoleDataPermission;
import com.xx.permission.mapper.SysRoleMapper;
import com.xx.permission.service.SysRoleService;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;/*** <p>* 角色表 服务实现类* </p>** @author aqi* @since 2023-05-18*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {@ResourceSysRoleMapper sysRoleMapper;@Overridepublic List<RoleDataPermission> getRoleDataPermission() {return sysRoleMapper.getRoleDataPermission();}
}

实体类

package com.xx.permission.entity.result;import lombok.Data;import java.util.List;/*** @author xiaxing* @describe 角色权限* @since 2023/5/18 14:59*/
@Data
public class RoleDataPermission {/*** 角色ID*/private Long roleId;/*** 角色名称*/private String roleName;/*** 角色描述*/private String roleDescribe;private String dataPermission;/*** 角色权限集合*/private List<DataPermission> dataPermissionList;/*** 接口地址*/private String uri;
}
package com.xx.permission.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;import lombok.Data;
import lombok.Getter;
import lombok.Setter;/*** <p>* 角色表* </p>** @author aqi* @since 2023-05-18*/
@Data
@TableName("sys_role")
public class SysRole implements Serializable {private static final long serialVersionUID = 1L;/*** 角色ID*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 角色名称*/private String roleName;/*** 角色说明*/private String describe;
}

8.2、表结构

这里的表结构只有最基础的字段

8.2.1、角色表

在这里插入图片描述

在这里插入图片描述

8.2.1、接口表

在这里插入图片描述

在这里插入图片描述

8.2.1、角色接口权限关联表

这里的数据权限存在了JSON中,只是为了少建表,其中field表示字段名称,value表示字段的值,fieldType表示字段的数据类型(目前只写了Long和String的支持)

在这里插入图片描述

在这里插入图片描述

九、测试

package com.xx.permission.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xx.permission.entity.result.UserDTO;
import com.xx.permission.service.TOrderService;
import com.xx.permission.utils.UserUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** <p>* 订单表 前端控制器* </p>** @author aqi* @since 2023-06-08*/
@RestController
@RequestMapping("/order")
public class TOrderController {@Resourceprivate TOrderService tOrderService;@GetMapping("/list")public void test() {UserDTO userDTO = new UserDTO();userDTO.setRoleId(2L);UserUtils.setCurrentUser(userDTO);tOrderService.page(new Page<>(1, 10));}}

效果

2023-06-09 09:47:12.349  INFO 33248 --- [nio-8080-exec-2] c.x.p.config.DataScopeInterceptor        : [DataScopeInterceptor]请求uri:[/order/list],原始SQL:[SELECT COUNT(*) AS total FROM t_order]处理后SQL:[SELECT COUNT(*) AS total FROM t_order WHERE t_order.area IN (2) AND t_order.type IN ('1')]
2023-06-09 09:47:12.349 DEBUG 33248 --- [nio-8080-exec-2] c.x.p.m.TOrderMapper.selectPage_mpCount  : ==>  Preparing: SELECT COUNT(*) AS total FROM t_order WHERE t_order.area IN (2) AND t_order.type IN ('1')
2023-06-09 09:47:12.349 DEBUG 33248 --- [nio-8080-exec-2] c.x.p.m.TOrderMapper.selectPage_mpCount  : ==> Parameters: 
2023-06-09 09:47:12.356 DEBUG 33248 --- [nio-8080-exec-2] c.x.p.m.TOrderMapper.selectPage_mpCount  : <==      Total: 1
2023-06-09 09:47:12.364  INFO 33248 --- [nio-8080-exec-2] c.x.p.config.DataScopeInterceptor        : [DataScopeInterceptor]请求uri:[/order/list],原始SQL:[SELECT  id,number,type,area  FROM t_order LIMIT ?]处理后SQL:[SELECT id, number, type, area FROM t_order WHERE t_order.area IN (2) AND t_order.type IN ('1') LIMIT ?]
2023-06-09 09:47:12.364 DEBUG 33248 --- [nio-8080-exec-2] c.x.p.mapper.TOrderMapper.selectPage     : ==>  Preparing: SELECT id, number, type, area FROM t_order WHERE t_order.area IN (2) AND t_order.type IN ('1') LIMIT ?
2023-06-09 09:47:12.365 DEBUG 33248 --- [nio-8080-exec-2] c.x.p.mapper.TOrderMapper.selectPage     : ==> Parameters: 10(Long)
2023-06-09 09:47:12.377 DEBUG 33248 --- [nio-8080-exec-2] c.x.p.mapper.TOrderMapper.selectPage     : <==      Total: 1

http://www.ppmy.cn/news/292648.html

相关文章

ETC人车关系查询-ETC人车关系查询api接口

接口地址&#xff1a; https://登录后显示/api/189/363(支持:http/https)) 接口页面&#xff1a;https://www.wapi.cn/api_detail/189/363.html 网站地址&#xff1a;https://www.wapi.cn 接口简介&#xff1a;核验指定人员/企业是否是指定车辆的 ETC 开户人、车辆所有人或 E…

移植蓝牙芯片后,PCM 无声音问题记录

背景:投影仪项目上的蓝牙模组本地已经验证ok,送到客户那里发现HFP打电话没声音。 1. 客户平台是3566,android 11的环境, 该环境下其他的模组是可以的 2. 在3566上安装QQ, 波通VOIP电话后, 无阴影, 3. 通过示波器接收pcm 无波形输出, 问题分析查证 1.查看HCI log ,…

word如何转化为pdf格式?分享四个方法给大家!

在工作和学习中&#xff0c;经常需要对文档进行转换&#xff0c;其中将Word文档转换为PDF是最常见的格式转换之一。下面介绍几种常用的转换方法&#xff0c;包括使用记灵在线工具。 方法一&#xff1a;使用Word软件直接转换 如果你使用的是电脑上的Word软件&#xff0c;可以直…

芯片行业FAE岗位

在半导体市场推广和销售这个行业里&#xff0c;主要有以下几个相关的工程师职位&#xff1a; 销售工程师Sales Engineer现场应用工程师Field Application Engineer应用/系统工程师Application/System Engineer市场工程师Marketing Engineer 芯片行业的岗位介绍 一、Sales&am…

银行家算法:避免死锁的资源分配算法[cpp]

银行家算法&#xff1a;避免死锁的资源分配算法[cpp] 介绍 在多进程环境中&#xff0c;资源的合理分配和管理对系统的正常运行至关重要。然而&#xff0c;不当的资源分配可能会导致死锁&#xff0c;即进程无法继续执行并永久阻塞。为了避免死锁的发生&#xff0c;银行家算法应…

iphone手机 ios系统 无法更新app 跳转到AppStore 显示 打开

出现场景: 长期未更新的app应用, 当出现新功能想要体验, 去苹果应用商店发现 原本该出现"更新"按钮的地方显示的是 "打开" 解决方案: 设置->通用->iphone存储空间 重点来了, 找到要想更新的应用, 点击 接下来会出现 "卸载APP", "…

mac导出iphone手机上的ipa包

0、前提是手机越狱了 1、下载安装爱思助手 2、手机连接mac本&#xff0c;打开爱思助手 3、在文件管理——程序&#xff08;用户&#xff09;——【要导出的应用】 4、点击进入文件夹&#xff0c;导出.app后缀的文件夹 5、然后新建一个名为Payload的文件夹&#xff0c;然后把…

iphone12启用来电小窗口(设置方法)

iphone12如何设置启用来电小窗口?在全新的iphone12手机系统中。官方有新增了一些全新的功能。来电小窗口就是全新增加的一个功能。开启这个功能之后手机来电就会以小窗口的形式呈现在手机的顶部上。这样就节省空间不会影响其他操作了。那么要如何开启这个来电小窗口功能呢?下…