MP自定义拦截器实现数据权限

角色权限划分

编号名称描述
1管理员全部数据权限
2普通角色自定义数据权限
3部门权限部门权限
4部门及以下数据权限部门及以下数据权限
5本人数据本人数据

大致思路

1、模仿 PageHelper.startPage 用来声明哪些操作需要做范围限制

2、定义 Mybatis plus 自定义拦截器 Interceptor,用于每次拦截查询 sql语句,附带数据范围权限sql条件

3、因为使用了 PageHelper.startPage 分页插件的使用,先计算总数,怎么在这之前拦截,需要拦截多次

4、考虑到 Mybatis拦截器能够拦截SQL执行的整个过程,因为我们可以考虑SQL执行之前,对SQL进行重写,从而达到数据行权限的目的。

部分代码实现步骤

权限工具类

public class SecurityUtils {

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    protected static final ThreadLocal<String> threadLocal = new ThreadLocal();

    /**
     * 设置权限标识
     */
    public static void startDataScope(){
        threadLocal.set(SecurityConstants.DATA_SCOPE);
    }
    /**
     * 获取权限标识
     */
    public static String getDataScope(){
        return threadLocal.get();
    }
    /**
     * 清除权限标识
     */
    public static void cleanDataScope(){
        threadLocal.remove();
    }
    
}

权限常量

/**
 * 权限相关通用常量
 * 
 */
public class SecurityConstants {
    /**
     * 数据权限过滤关键字
     */
    public static final String DATA_SCOPE = "dataScope";
}

自定义MybatisPlus拦截器

/**
 * @description 数据权限插件
 */
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
@Slf4j
public class MybatisDataPermissionIntercept implements Interceptor {
    CCJSqlParserManager parserManager = new CCJSqlParserManager();

    /**
     * 全部数据权限
     */
    public static final String DATA_SCOPE_ALL = "1";

    /**
     * 自定数据权限
     */
    public static final String DATA_SCOPE_CUSTOM = "2";

    /**
     * 部门数据权限
     */
    public static final String DATA_SCOPE_DEPT = "3";

    /**
     * 部门及以下数据权限
     */
    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";

    /**
     * 仅本人数据权限
     */
    public static final String DATA_SCOPE_SELF = "5";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            //TODO 自己要进行的各种处理
            String sql = boundSql.getSql();
            log.info("原始SQL: {}", sql);
            //判断线程内是否有权限信息
            String dataScope = SecurityUtils.getDataScope();
            if (SecurityConstants.DATA_SCOPE.equals(dataScope)){
                // 增强sql
                Select select = (Select) parserManager.parse(new StringReader(sql));
                SelectBody selectBody = select.getSelectBody();
                if (selectBody instanceof PlainSelect) {
                    this.setWhere((PlainSelect) selectBody);
                } else if (selectBody instanceof SetOperationList) {
                    SetOperationList setOperationList = (SetOperationList) selectBody;
                    List<SelectBody> selectBodyList = setOperationList.getSelects();
                    selectBodyList.forEach((s) -> {
                        this.setWhere((PlainSelect) s);
                    });
                }
                String dataPermissionSql = select.toString();
                log.info("增强SQL: {}", dataPermissionSql);
                BoundSql dataPermissionBoundSql = new BoundSql(ms.getConfiguration(), dataPermissionSql, boundSql.getParameterMappings(), parameter);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, dataPermissionBoundSql);
            }
            //注:下面的方法可以根据自己的逻辑调用多次,在分页插件中,count 和 page 各调用了一次
            return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } finally {
            //清除线程中权限参数
            SecurityUtils.cleanDataScope();
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    protected void setWhere(PlainSelect plainSelect) {
        Expression sqlSegment = this.getSqlSegment(plainSelect.getWhere());
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }

    @SneakyThrows
    public Expression getSqlSegment(Expression where) {
        //用户信息是在gateway中去获取的,获取完之后直接存入到header中,然后再调用过程中传递header
        //getLoginUser 是获取用户信息,在header中去获取 
        JSONObject loginUser = getLoginUser();
        if (loginUser == null){
            return where;
        }
        Integer deptId = loginUser.getInteger("deptId");
        String userId = loginUser.getString("userId");
        JSONArray roles = loginUser.getJSONArray("roles");
        StringBuilder sqlString = new StringBuilder();
        for (Object role : roles) {
            JSONObject roleJson = JSONObject.parseObject(role.toString());
            String dataScopeNum = roleJson.getString(SecurityConstants.DATA_SCOPE);
            Integer roleId = roleJson.getInteger("roleId");
            if (DATA_SCOPE_ALL.equals(dataScopeNum)) {
                // 全部数据权限
                sqlString = new StringBuilder();
                break;
            } else if (DATA_SCOPE_CUSTOM.equals(dataScopeNum)) {
                sqlString.append(" OR `sys_dept`.dept_id IN ( SELECT dept_id FROM `sys_role_dept` WHERE role_id = '")
                        .append(roleId)
                        .append("' ) ");
            } else if (DATA_SCOPE_DEPT.equals(dataScopeNum)) {
                sqlString.append(" OR `sys_dept`.dept_id = '").append(deptId).append("' ");
            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScopeNum)) {
                sqlString.append(" OR `sys_dept`.dept_id IN ( SELECT dept_id FROM `sys_dept` WHERE dept_id = '")
                        .append(deptId)
                        .append("' or find_in_set( '")
                        .append(deptId)
                        .append("' , ancestors ) ) ");
            }else if (DATA_SCOPE_SELF.equals(dataScopeNum)) {
                //TODO 暂时有问题
                sqlString.append(" OR `sys_user`.user_id = '").append(userId).append("' ");
            }
        }
        if (StringUtils.isNotBlank(sqlString.toString())) {
            if (where == null){
                where = new HexValue(" 1 = 1 ");
            }
            sqlString.insert(0," AND (");
            sqlString.append(")");
            sqlString.delete(7, 9);
            //判断是不是分页, 分页完成之后 清除权限标识
            return CCJSqlParserUtil.parseCondExpression(where + sqlString.toString());
        }else {
            return where;
        }
    }

}

修改MybatisDataPermissionIntercept

  • 修改MybatisDataPermissionInterceptPageHelper插件后面执行
/**
 * @description 数据权限插件 配置
 * @date 2022-04-01 17:01
 */
@AutoConfigureAfter(PageHelperAutoConfiguration.class)
@Configuration
public class MybatisInterceptorAutoConfiguration implements InitializingBean{

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Override
    @PostConstruct
    public void afterPropertiesSet() throws Exception {
        //创建自定义mybatis拦截器,添加到chain的最后面
        MybatisDataPermissionIntercept mybatisInterceptor = new MybatisDataPermissionIntercept();

        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
            //自己添加
            configuration.addInterceptor(mybatisInterceptor);
        }
    }

}

配置spring.factories

  • resources下面创建文件夹META-INF,再创建一个文件spring.factories,用spring.factories 是为了让自定义插件在PageHelperAutoConfiguration之后注入到spring中,后注入的先执行
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zhc.cloud.mybatis.intercept.MybatisInterceptorAutoConfiguration

代码示例

分页查询

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUserPO> implements ISysUserService {
    
	@Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysDeptMapper sysDeptMapper;

    /***
    * 查询列表
    * @return Result
    */
    @Override
    public Result<?> selectList(SysUserVO sysUserVO) {
        SecurityUtils.startDataScope();
        PageHelper.startPage(sysUserVO.getPageNum(), sysUserVO.getPageSize());
        SysUserDTO sysUserDTO = new SysUserDTO();
        BeanUtils.copyProperties(sysUserVO,sysUserDTO);
        List<SysUserPO> sysUserPOS = sysUserMapper.selectUserList(sysUserDTO);
        List<UserDTO> userDTOS = new ArrayList<>();
        for (SysUserPO sysUserPO : sysUserPOS) {
            UserDTO userDTO = new UserDTO();
            BeanUtils.copyProperties(sysUserPO,userDTO);
            SysDeptPO sysDeptPO = sysDeptMapper.selectById(sysUserPO.getDeptId());
            userDTO.setDeptName(sysDeptPO.getDeptName());
            userDTO.setPhone(SensitiveInfoUtils.mobilePhone(userDTO.getPhone()));
            userDTOS.add(userDTO);
        }
        PageInfo<UserDTO> pageInfo = new PageInfo<>(userDTOS);
        return Result.success(pageInfo);
    }
}

单个查询

@Service
public class SysDeptServiceImpl extends ServiceImpl<SysDeptMapper, SysDeptPO> implements ISysDeptService {
    @Autowired
    private SysDeptMapper sysDeptPOMapper;

    @Override
    public Result<?> treeselect(SysDeptVO dept) {
        SecurityUtils.startDataScope();
        LambdaQueryWrapper<SysDeptPO> entityWrapper = new LambdaQueryWrapper<SysDeptPO>();
        entityWrapper.eq((dept.getDeptId()!=null && dept.getDeptId() !=0),SysDeptPO::getDeptId,dept.getDeptId())
                .eq((dept.getParentId()!=null &&dept.getParentId()!=0),SysDeptPO::getParentId,dept.getDeptId())
                .like(StringUtils.isNotBlank(dept.getDeptName()),SysDeptPO::getDeptName,dept.getDeptName())
                .eq(StringUtils.isNotBlank(dept.getStatus()),SysDeptPO::getStatus,dept.getStatus())
                .orderByAsc(SysDeptPO::getParentId,SysDeptPO::getOrderNum);
        List<SysDeptPO> sysDeptPO = sysDeptPOMapper.selectList(entityWrapper);
        if(sysDeptPO == null){
            return Result.success();
        }
        sysDeptPO.sort(Comparator.comparing(SysDeptPO::getParentId));
        List<Long> tempList = new ArrayList<Long>();
        for (SysDeptPO deptPO : sysDeptPO) {
            tempList.add(deptPO.getParentId());
        }
        Long minParentId = Collections.min(tempList);
        return Result.success(buildTree(sysDeptPO,minParentId));
    }
}

注意点

  • 使用mybatis plus自定义拦截器的时候一定要遵守规范,否则可能会引起一些查询异常.
  • 使用mybatis plus自定义拦截器可根据实际业务去进行扩展
Last Updated 2024/5/24 16:21:58