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

前言

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

需求

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

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

设计思路

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

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

sql 复制代码
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='规则表';

例子

规则配置

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

  2. 这个接口下所有的规则

  3. 查看订单金额大于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}),从而实现这个权限控制

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

代码

这样即实现对金额字段的限制 在看看其他更简单的需求角色C的权限:收货人地址模糊查询钦南区的订单,就可以如下配置

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

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

还有很多可以完善的地方,忽略我的垃圾技术,只是想实现我的思路。有兴趣的可以移步到仓库看看,一个人维护好难啊,但是也不想放弃,写都写到这是吧,总感觉一个权限管理系统就要成了哈哈哈哈哈

欢迎来看看 wonder-server: wonder-server基础的RBAC权限管理系统 (gitee.com)

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

EntityController

java 复制代码
public abstract class EntityController<T extends BaseEntity<T>, S extends EntityService<T, D, Q>, D extends EntityDto, Q extends EntityQuery> {
    @Autowired
    private S service;

    @RequestLogger
    @ApiOperation("获取")
    @GetMapping("/get/{id}")
    public Result<D> get(@PathVariable Long id){
        T t = service.getById(id);
        return Result.ok("操作成功", service.afterQueryHandler(t));
    }

    @RequestLogger
    @ApiOperation("删除")
    @GetMapping("/delete/{id}")
    public Result<Boolean> delete(@PathVariable Long id){
        return Result.ok("操作成功", service.removeById(id));
    }

    @RequestLogger
    @ApiOperation("保存")
    @PostMapping("save")
    public Result<T> save(@Validated(CreateGroup.class) @RequestBody D d){
        return Result.ok("操作成功", service.save(d));
    }

    @RequestLogger
    @ApiOperation("更新")
    @PostMapping("update")
    public Result<Boolean> update(@Validated(UpdateGroup.class) @RequestBody D d){
        return Result.ok("操作成功", service.updateById(d));
    }

    @RequestLogger
    @ApiOperation("分页")
    @PostMapping("page")
    public Result<PageData<D>> page(@RequestBody Q q){
        return Result.ok("操作成功", service.page(q));
    }

    @RequestLogger
    @ApiOperation("列表")
    @PostMapping("list")
    public Result<List<D>> list(@RequestBody Q q){
        return Result.ok("操作成功", service.list(q));
    }

}

基础的增删改查业务,直接继承EntityController即可,当然Service也不用写

java 复制代码
@Api(tags = "订单相关")
@RestController
@RequestMapping("order")
public class OrderController extends EntityController<Order, OrderService, OrderDto, OrderQuery> {
    // 不用任何代码
}

EntityService

java 复制代码
public interface EntityService<T extends BaseEntity<T>, D extends EntityDto, Q extends EntityQuery> extends IService<T> {

    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<D> afterQueryHandler(List<T> list);

    void afterDeleteHandler(T t);

    default Class<T> getTClass() {
        return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 0);
    }

    default Class<D> getDClass() {
        return (Class<D>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 1);
    }

    default Class<Q> getQClass() {
        return (Class<Q>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 2);
    }

    boolean isExist(Long id);

    D beforeSaveOrUpdateHandler(D d);

    D beforeSaveHandler(D d);

    D beforeUpdateHandler(D d);

    PageData<D> page(Q q);

    QueryWrapper<T> queryWrapperHandler(Q q);

    List<D> list(Q q);

}

EntityServiceImpl

java 复制代码
public abstract class EntityServiceImpl<M extends BaseMapper<T>, T extends BaseEntity<T>, D extends EntityDto, Q extends EntityQuery> extends ServiceImpl<M, T> implements EntityService<T, D, Q> {

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

            Class<T> 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<D> dClass = getDClass();
            Class<? super D> superclass = dClass.getSuperclass();
            Field fieldId = superclass.getDeclaredField("id");
            fieldId.setAccessible(true);
            long id = (long) fieldId.get(d);
            T t = getById(id);
            if (t == null) {
                throw new RuntimeException(StrUtil.format("【{}】不存在", id));
            }

            BeanUtil.copyProperties(d, t);
            boolean b = super.updateById(t);
            if (b) {
                afterUpdateHandler(t);
            }
            return b;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public T getById(Serializable id) {
        if (id == null)
            return null;
        return super.getById(id);
    }

    @Override
    public boolean removeById(T entity) {
        return removeById(entity.getId());
    }

    @Override
    public boolean removeById(Serializable id) {
        if (id == null) {
            throw new RuntimeException("id不能为空");
        }

        T t = getById(id);

        boolean b = SqlHelper.retBool(getBaseMapper().deleteById(id));

        if (b) {
            afterDeleteHandler(t);
        }
        return b;
    }

    @Override
    public T afterSaveHandler(T t) {
        return t;
    }

    @Override
    public T afterUpdateHandler(T t) {
        return t;
    }

    @Override
    public D afterQueryHandler(T t) {
        Class<D> dClass = getDClass();
        return BeanUtil.copyProperties(t, dClass);
    }

    @Override
    public List<D> afterQueryHandler(List<T> list) {
        List<D> dList = new ArrayList<>();

        if (CollectionUtil.isEmpty(list)) {
            return dList;
        }

        for (T t : list) {
            D d = afterQueryHandler(t);
            dList.add(d);
        }
        return dList;
    }

    @Override
    public void afterDeleteHandler(T t) {

    }

    @Override
    public boolean isExist(Long id) {
        if (id == null)
            throw new RuntimeException("id 为空");

        long count = count(new QueryWrapper<T>().eq("id", id));
        return count > 0;
    }

    @Override
    public D beforeSaveOrUpdateHandler(D d) {
        return d;
    }

    @Override
    public D beforeSaveHandler(D d) {
        return d;
    }

    @Override
    public D beforeUpdateHandler(D d) {
        return d;
    }

    @Override
    public PageData<D> page(Q q) {
        try {
            QueryWrapper<T> queryWrapper = queryWrapperHandler(q);

            IPage<T> page = new Page<>(q.getCurPage(), q.getPageSize());

            page(page, queryWrapper);

            List<D> dList = afterQueryHandler(page.getRecords());

            return new PageData<>(dList, page.getTotal(), page.getPages());

        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public QueryWrapper<T> queryWrapperHandler(Q q) {
        try {
            Class<? extends EntityQuery> qClass = q.getClass();

            Field[] fields = qClass.getDeclaredFields();

            QueryWrapper<T> queryWrapper = new QueryWrapper<>();

            Map<String, Field[]> betweenFieldMap = new HashMap<>();

            // 处理@SelectColumn
            SelectColumn selectColumn = qClass.getAnnotation(SelectColumn.class);
            if (selectColumn != null && selectColumn.value() != null && selectColumn.value().length > 0) {
                String[] strings = selectColumn.value();
                for (int i = 0; i < strings.length; i++) {
                    strings[i] = StrUtil.toUnderlineCase(strings[i]);
                }
                queryWrapper.select(strings);
            }

            String sortColumn = "";
            String sortOrder = "";

            for (Field field : fields) {
                // if (isBusinessField(field.getName())) {
                field.setAccessible(true);
                Object value = field.get(q);

                // 判断该属性是否存在值
                if (Objects.isNull(value) || String.valueOf(value).equals("null") || value.equals("")) {
                    continue;
                }

                // FIXME 存在bug,应该在判空前执行
                // 是否存在注解@QuerySort
                QuerySort querySort = field.getDeclaredAnnotation(QuerySort.class);
                if (querySort != null) {
                    String paramValue = (String) field.get(q);
                    sortColumn = paramValue.isEmpty() ? querySort.value() : paramValue;
                }

                // 是否存在注解@QueryOrder
                QueryOrder queryOrder = field.getDeclaredAnnotation(QueryOrder.class);
                if (queryOrder != null) {
                    String paramValue = (String) field.get(q);
                    sortOrder = paramValue.isEmpty() ? queryOrder.value() : paramValue;
                }

                // 是否存在注解@Query
                Query query = field.getDeclaredAnnotation(Query.class);
                if (query == null) {
                    continue;
                }

                String columnName = StrUtil.isBlank(query.column()) ? StrUtil.toUnderlineCase(field.getName()) : query.column();

                if (query.expression().equals(ExpressionEnum.EQ)) {
                    queryWrapper.eq(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.NE)) {
                    queryWrapper.ne(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.LIKE)) {
                    queryWrapper.like(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.GT)) {
                    queryWrapper.gt(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.GE)) {
                    queryWrapper.ge(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.LT)) {
                    queryWrapper.lt(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.LE)) {
                    queryWrapper.le(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.IN)) {
                    queryWrapper.in(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.NOT_IN)) {
                    queryWrapper.notIn(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.IS_NULL)) {
                    queryWrapper.isNull(columnName);
                } else if (query.expression().equals(ExpressionEnum.NOT_NULL)) {
                    queryWrapper.isNotNull(columnName);
                } else if (query.expression().equals(ExpressionEnum.BETWEEN)) {
                    if (betweenFieldMap.containsKey(columnName)) {
                        Field[] f = betweenFieldMap.get(columnName);
                        Field[] tempList = new Field[2];
                        tempList[0] = f[0];
                        tempList[1] = field;
                        betweenFieldMap.put(columnName, tempList);
                    } else {
                        betweenFieldMap.put(columnName, new Field[]{field});
                    }
                }

            }
            // }

            Set<String> keySet = betweenFieldMap.keySet();
            for (String key : keySet) {
                // 已在编译时做了相关校验,在此无须做重复且耗时的校验
                Field[] itemFieldList = betweenFieldMap.get(key);
                if (itemFieldList.length != 2){
                    throw new IllegalArgumentException("查询参数数量对应异常");
                }

                Field field1 = itemFieldList[0];
                Field field2 = itemFieldList[1];

                Query query1 = field1.getDeclaredAnnotation(Query.class);

                if (field1.get(q) instanceof Date) {
                    if (query1.left()) {
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') >= date_format({0},'%y%m%d')", field1.get(q));
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') <= date_format({0},'%y%m%d')", field2.get(q));
                    } else {
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') <= date_format({0},'%y%m%d')", field1.get(q));
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') >= date_format({0},'%y%m%d')", field2.get(q));
                    }
                } else {
                    if (query1.left()) {
                        queryWrapper.between(key, field1.get(q), field2.get(q));
                    } else {
                        queryWrapper.between(key, field2.get(q), field1.get(q));
                    }
                }
            }

            if (sortOrder.equalsIgnoreCase("desc")) {
                queryWrapper.orderByDesc(StrUtil.isNotBlank(sortColumn), StrUtil.toUnderlineCase(sortColumn));
            } else {
                queryWrapper.orderByAsc(StrUtil.isNotBlank(sortColumn), StrUtil.toUnderlineCase(sortColumn));
            }

            return queryWrapper;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public List<D> list(Q q) {
        try {
            QueryWrapper<T> queryWrapper = queryWrapperHandler(q);
            return afterQueryHandler(list(queryWrapper));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

这个怎么说呢,还是方便的,生成代码后就启动了,增删改查的接口就有了,如果自定义业务,重写controller、service实现就可以。

也设计了一些自定义注解来进行辅助查询

java 复制代码
// 查询特定字段
@SelectColumn({"id", "create_time", "create_by", "update_time", "update_by", "deleted", "dict_name", "dict_code", "dict_type", "sort", "remark"})
@ApiModel(value = "DictQuery对象", description = "系统字典")
public class DictQuery extends EntityQuery {

    @Query(expression = ExpressionEnum.LIKE) // dictName字段模糊查询
    @ApiModelProperty("字典名称")
    private String dictName;

    @Query // dictCode字段精确查询
    @ApiModelProperty("字典编码(唯一)")
    private String dictCode;

    @Query
    @ApiModelProperty("字典类型,0-列表,1-树")
    private Integer dictType;

    @ApiModelProperty("排序")
    private Integer sort;

    @ApiModelProperty("备注")
    private String remark;

    // create_time 字段范围查询
    @Query(column = "create_time", expression = ExpressionEnum.BETWEEN, left = true)
    @ApiModelProperty("开始日期")
    private Date beginDate;
    
    // create_time 字段范围查询
    @Query(column = "create_time", expression = ExpressionEnum.BETWEEN, left = false)
    @ApiModelProperty("结束日期")
    private Date endDate;

    @QuerySort("sort") // 如果sortColumn为null,根据sort字段排序
    @ApiModelProperty("排序字段")
    private String sortColumn;

    @QueryOrder // 如果sortOrder为null,默认asc
    @ApiModelProperty("排序方式-asc/desc")
    private String sortOrder;

}

有没有感兴趣的来一起维护维护,非常欢迎~wonder-server: wonder-server基础的RBAC权限管理系统 (gitee.com)

提问:掌握什么技能才能去深广,我也想去看看,现在就业这么难吗,投简历都没有人鸟~

相关推荐
计算机学姐2 小时前
基于微信小程序的高校班务管理系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
摇滚侠7 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 核心语法 笔记39
spring boot·笔记·后端·thymeleaf
九丶弟8 小时前
SpringBoot的cache使用说明
java·spring boot·spring·cache
lang201509289 小时前
打造专属Spring Boot Starter
java·spring boot·后端
lang2015092811 小时前
Spring Boot RSocket:高性能异步通信实战
java·spring boot·后端
蹦跑的蜗牛13 小时前
Spring Boot使用Redis实现消息队列
spring boot·redis·后端
凤山老林14 小时前
SpringBoot 如何实现零拷贝:深度解析零拷贝技术
java·linux·开发语言·arm开发·spring boot·后端
Chan1615 小时前
流量安全优化:基于 Nacos 和 BloomFilter 实现动态IP黑名单过滤
java·spring boot·后端·spring·nacos·idea·bloomfilter
YUELEI11816 小时前
Springboot WebSocket
spring boot·后端·websocket
小蒜学长17 小时前
springboot基于JAVA的二手书籍交易系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端