我也想自己实现一套数据权限,不仅仅是用户、部门这些纬度

2023年 8月 15日 83.6k 0

前言

我一年java,在小公司,权限这块都没有成熟的方案,目前我知道权限分为功能权限和数据权限,我不知道数据权限这块大家是怎么解决的,但在实际项目中我遇到数据权限真的复杂,你永远不知道业主在这方面的需求是什么。我也有去搜索在这方面是怎么做,但是我在gitee、github搜到的权限管理系统他们都是这么实现的:查看全部数据自定义数据权限本部门数据权限本部门及以下数据仅本人数据权限,但是这种控制粒度完全不够的,所以就想自己实现一下。

image.png

需求

需求一 有一个单位企业的树,企业都是挂在某个单位下面的,企业是分类型的(餐饮企业经营企业生产企业),业主需要单位的人限定某些单位只能看一个或他指定的某个类型的企业。现在指定角色A只能查看餐饮经营企业,那就只能使用查看自定义部门数据这个,然后在10000家企业里面慢慢勾选符合的企业,这样可以是可以,但是我觉得这样做不太妥。估计有人说:那你把三种类型的企业分组,餐饮企业挂在餐饮分组下,其他同理。然后用自定义数据权限选中那两个不就可以了吗? 可以是可以,但是我不是业主,业主要求了那些企业必须挂在哪些单位下,在页面显示的树也不能显示什么餐饮企业分组生产企业... 说到底,除非你有办法改变业主的想法。

需求二 类似订单吧,角色A只能查看未支付的订单,角色B只能看交易金额在100~1000元的订单。

用通用的那5种权限对这两个需求已经是束手无策了。

设计思路

后来我看到一篇文章【数据权限就该这么实现(设计篇) - 掘金 (juejin.cn)】,对我有很大的启发,从数据库字段下手,用规则来处理
image.png
我以这个文章的思路为基础,设计了这么一个关系
image.png

主要还是这张规则表,通过在页面配置好相关的规则来实现对某个字段的控制

CREATE TABLE `sys_rule` (
  `id` bigint NOT NULL,
  `remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注',
  `mark_id` bigint DEFAULT NULL,
  `table_alias` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表别名',
  `column_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '数据库字段名',
  `splice_type` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '拼接类型 SpliceTypeEnum',
  `expression` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表达式 ExpressionEnum',
  `provide_type` tinyint DEFAULT NULL COMMENT 'ProvideTypeEnum 值提供类型,1-值,2-方法',
  `value1` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '值1',
  `value2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '值2',
  `class_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '全限定类名',
  `method_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '方法名',
  `formal_param` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '形参,分号隔开',
  `actual_param` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '实参,分号隔开',
  `create_time` datetime DEFAULT NULL,
  `create_by` bigint DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `update_by` bigint DEFAULT NULL,
  `deleted` bit(1) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='规则表';

整体思路就是通过页面来对特定的接口设置规则,如果提供类型是@DataScope注解用在方法上,那么默认机会在执行SQL前去拼接对应的数据权限。如果提供类型是方法@DataScope注解用在方法上,那么会根据你配置的方法名参数类型去反射执行对应的方法,得到该规则能查看的所有idList,然后在执行SQL前去拼接对应的数据权限,这是默认的处理方式。如果@DataScope注解使用在形参上或者使用Service提供的方法接口,那么需要开发者手动处理,返回什么那么是开发者自定义了。所以字段你自己定,联表也没问题、反射执行什么方法、参数是什么、过程怎么样也是你自己定,灵活性很高(至少我是这么认为的,哈哈哈哈哈哈)

@Component
public class DataScopeHandler implements DataPermissionHandler {

    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {
        DataScopeAspect.DataScopeParam dataScopeParam = DataScopeAspect.getDataScopeParam();
        if (dataScopeParam == null || SecurityUtil.isAdmin()) {
            return where;
        }

        DataScopeInfo dataScopeInfo = dataScopeParam.getDataScopeInfo();
        RuleDto dto = dataScopeInfo.getDto();
        List idList = dataScopeInfo.getIdList();
        String sql = "".equals(dto.getTableAlias()) || dto.getTableAlias() == null ? dto.getColumnName() : dto.getTableAlias() + "." + dto.getColumnName();

        if (dto.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {
            if (CollectionUtil.isEmpty(idList))
                throw new NoDataException("没有查看权限");

            ItemsList itemsList = new ExpressionList(idList.stream().map(LongValue::new).collect(Collectors.toList()));
            InExpression inExpression = new InExpression(new Column(sql), itemsList);
            if (dto.getExpression().equals(ExpressionEnum.IN.toString())) {
                if (dto.getSpliceType().equals(SpliceTypeEnum.OR.toString())) {
                    return where == null ? inExpression : new OrExpression(where, inExpression);
                } else if (dto.getSpliceType().equals(SpliceTypeEnum.AND.toString())) {
                    return where == null ? inExpression : new AndExpression(where, inExpression);
                } else
                    throw new RuntimeException("错误的拼接类型:" + dto.getSpliceType());

            } else if (dto.getExpression().equals(ExpressionEnum.NOT_IN.toString())) {
                NotExpression notExpression = new NotExpression(inExpression);

                if (dto.getSpliceType().equals(SpliceTypeEnum.OR.toString())) {
                    return where == null ? notExpression : new OrExpression(where, notExpression);
                } else if (dto.getSpliceType().equals(SpliceTypeEnum.AND.toString())) {
                    return where == null ? notExpression : new AndExpression(where, notExpression);
                } else
                    throw new RuntimeException("错误的拼接类型:" + dto.getSpliceType());
            } else
                throw new RuntimeException("错误的表达式:" + dto.getExpression());

        } else if (dto.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {
            if (dto.getExpression().equals(ExpressionEnum.EQ.toString())) {
                StringValue valueExpression = new StringValue(dto.getValue1());
                EqualsTo equalsTo = new EqualsTo(new Column(sql), valueExpression);

                if (dto.getSpliceType().equals(SpliceTypeEnum.OR.toString())) {
                    return where == null ? equalsTo : new OrExpression(where, equalsTo);
                } else if (dto.getSpliceType().equals(SpliceTypeEnum.AND.toString())) {
                    return where == null ? equalsTo : new AndExpression(where, equalsTo);
                } else
                    throw new RuntimeException("错误的拼接类型:" + dto.getSpliceType());

            } 
            
            .......处理所有的情况
            
            else (dto.getExpression().equals(ExpressionEnum.IS_NULL.toString()) || dto.getExpression().equals(ExpressionEnum.NOT_NULL.toString())) {
                Column column = new Column(dto.getColumnName());

                IsNullExpression isNullExpression = new IsNullExpression();
                if (dto.getExpression().equals(ExpressionEnum.NOT_NULL.toString())) {
                    isNullExpression.setNot(true);
                }
                isNullExpression.setLeftExpression(column);

                if (dto.getSpliceType().equals(SpliceTypeEnum.OR.toString())) {
                    return where == null ? isNullExpression : new OrExpression(where, isNullExpression);
                } else if (dto.getSpliceType().equals(SpliceTypeEnum.AND.toString())) {
                    return where == null ? isNullExpression : new AndExpression(where, isNullExpression);
                } else
                    throw new RuntimeException("错误的拼接类型:" + dto.getSpliceType());

            } else
                throw new RuntimeException("错误的表达式:" + dto.getExpression());

        } else
            throw new RuntimeException("无效的提供方式:" + dto.getProvideType());
    }

}

例子1 查看订单金额大于100且小于500的订单

规则配置

  • 新增一个标记,可以理解成一个接口标识
    image.png

  • 这个接口下所有的规则
    image.png

  • 查看订单金额大于100且小于500的订单的需求的具体配置,这个配置的目的是通过反射执行com.gitee.whzzone.admin.business.service.impl.OrderServiceImpl这个类下的limitAmountBetween(BigDecimal, BigDecimal)的方法,也就是执行limitAmountBetween(100, 500),返回符合条件的orderIds,然后会在执行sql前去拼接 select ... from order where ... and id in ({这里是返回的orderIds}),从而实现这个权限控制
    image.png

  • 给角色的这个订单列表接口配置查看订单金额大于100且小于500的订单这个规则,那么这个角色只能查看范围内的订单数据了。
    image.png

  • 代码

    controller

    @Api(tags = "订单相关")
    @RestController
    @RequestMapping("order")
    public class OrderController extends EntityController {
        // 通用的增删改查不用写,父类已实现
    }
    

    service

    public interface OrderService extends EntityService {
        // 通用的增删改查不用写,父类已实现
        
        /**
         * 查询订单范围内的 orderIds
         * @param begin 订单金额开始
         * @param end 订单金额结束
         * @return
         */
        List limitAmountBetween(BigDecimal begin, BigDecimal end);
    }
    

    impl

    @Service
    public class OrderServiceImpl extends EntityServiceImpl implements OrderService {
        
        @DataScope("order-list") // 使用在方法上,交给AOP默认处理,标记这个方法为订单列表查询
        @Override // 重写父类列表查询
        public List list(OrderQuery query) {
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
            queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverName()), Order::getReceiverName, query.getReceiverName());
            queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverPhone()), Order::getReceiverPhone, query.getReceiverPhone());
            queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverAddress()), Order::getReceiverAddress, query.getReceiverAddress());
            queryWrapper.eq(query.getOrderStatus() != null, Order::getOrderStatus, query.getOrderStatus());
            return afterQueryHandler(list(queryWrapper));
        }
    
        // 具体实现
        @Override
        public List limitAmountBetween(BigDecimal begin, BigDecimal end) {
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
            queryWrapper.between(Order::getOrderAmount, begin, end);
            List list = list(queryWrapper);
            if (CollectionUtil.isEmpty(list))
                return new ArrayList();
    
            return list.stream().map(BaseEntity::getId).collect(Collectors.toList());
        }
    }
    

    这样就实现了查看订单金额大于100且小于500的订单的需求

    例子2 查看收货人地址模糊查询钦南区的订单

    规则配置

  • 新增一个规则,提供类型,单表查询可以不设置表别名,看图吧
    image.png

  • 配置角色在订单列表查询接口使用的规则
    image.png

  • 代码

    例子1的基础上不用做任何改动

    这样就实现了这个简单的需求

    到这里以上前面说的两个例子就可以搞定了,这查看全部数据自定义数据权限本部门数据权限本部门及以下数据仅本人数据权限五种权限在无形中实现了,针对你的用户id字段、部门id字段配几条对应的规则就可以。

    当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查

    EntityController

    public abstract class EntityController {
        @Autowired
        private S service;
    
        @RequestLogger
        @ApiOperation("获取")
        @GetMapping("/get/{id}")
        public Result get(@PathVariable Long id){
            T t = service.getById(id);
            return Result.ok("操作成功", service.afterQueryHandler(t));
        }
    
        @RequestLogger
        @ApiOperation("删除")
        @GetMapping("/delete/{id}")
        public Result delete(@PathVariable Long id){
            return Result.ok("操作成功", service.removeById(id));
        }
    
        @RequestLogger
        @ApiOperation("保存")
        @PostMapping("save")
        public Result save(@Validated(CreateGroup.class) @RequestBody D d){
            return Result.ok("操作成功", service.save(d));
        }
    
        @RequestLogger
        @ApiOperation("更新")
        @PostMapping("update")
        public Result update(@Validated(UpdateGroup.class) @RequestBody D d){
            return Result.ok("操作成功", service.updateById(d));
        }
    
        @RequestLogger
        @ApiOperation("分页")
        @PostMapping("page")
        public Result page(@RequestBody Q q){
            return Result.ok("操作成功", service.page(q));
        }
    
        @RequestLogger
        @ApiOperation("列表")
        @PostMapping("list")
        public Result list(@RequestBody Q q){
            return Result.ok("操作成功", service.list(q));
        }
    
    }
    

    EntityService

    public interface EntityService extends IService {
    
        T save(D d);
    
        boolean updateById(D d);
    
        @Override
        T getById(Serializable id);
    
        @Override
        boolean removeById(T entity);
    
        @Override
        boolean removeById(Serializable id);
    
        T afterSaveHandler(T t);
    
        T afterUpdateHandler(T t);
    
        D afterQueryHandler(T t);
    
        List afterQueryHandler(List list);
    
        void afterDeleteHandler(T t);
    
        default Class getTClass() {
            return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 0);
        }
    
        default Class getDClass() {
            return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 1);
        }
    
        default Class getQClass() {
            return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 2);
        }
    
        boolean isExist(Long id);
    
        D beforeSaveOrUpdateHandler(D d);
    
        D beforeSaveHandler(D d);
    
        D beforeUpdateHandler(D d);
    
        PageData page(Q q);
    
        QueryWrapper queryWrapperHandler(Q q);
    
        List list(Q q);
    
    }
    

    EntityServiceImpl

    public abstract class EntityServiceImpl extends ServiceImpl implements EntityService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public T save(D d) {
    try {
    d = beforeSaveOrUpdateHandler(d);
    d = beforeSaveHandler(d);

    Class dClass = getTClass();
    T t = dClass.getDeclaredConstructor().newInstance();

    BeanUtil.copyProperties(d, t);
    boolean save = save(t);
    if (!save) {
    throw new RuntimeException("操作失败");
    }
    afterSaveHandler(t);
    return t;
    } catch (Exception e) {
    e.printStackTrace();
    throw new RuntimeException(e.getMessage());
    }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateById(D d) {
    try {
    d = beforeSaveOrUpdateHandler(d);
    d = beforeUpdateHandler(d);

    Class dClass = getDClass();
    Class

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论