前言
我一年java,在小公司,权限这块都没有成熟的方案,目前我知道权限分为功能权限和数据权限,我不知道数据权限这块大家是怎么解决的,但在实际项目中我遇到数据权限真的复杂,你永远不知道业主在这方面的需求是什么。我也有去搜索在这方面是怎么做,但是我在gitee、github搜到的权限管理系统他们都是这么实现的:查看全部数据
、自定义数据权限
、本部门数据权限
、本部门及以下数据
、仅本人数据权限
,但是这种控制粒度完全不够的,所以就想自己实现一下。
需求
需求一 有一个
单位
和企业
的树,企业都是挂在某个单位下面的,企业是分类型的(餐饮企业
、经营企业
、生产企业
),业主需要单位的人限定某些单位只能看一个或他指定的某个类型的企业。现在指定角色A
只能查看餐饮
、经营
的企业
,那就只能使用查看自定义部门数据
这个,然后在10000家企业里面慢慢勾选符合的企业,这样可以是可以,但是我觉得这样做不太妥。估计有人说:那你把三种类型的企业分组,餐饮企业挂在餐饮分组下,其他同理。然后用自定义数据权限
选中那两个不就可以了吗? 可以是可以,但是我不是业主,业主要求了那些企业必须挂在哪些单位
下,在页面显示的树也不能显示什么餐饮企业分组
、生产企业
... 说到底,除非你有办法改变业主的想法。
需求二 类似订单吧,
角色A
只能查看未支付
的订单,角色B
只能看交易金额在100~1000元
的订单。
用通用的那5种权限对这两个需求已经是束手无策了。
设计思路
后来我看到一篇文章【数据权限就该这么实现(设计篇) - 掘金 (juejin.cn)】,对我有很大的启发,从数据库字段下手,用规则来处理
我以这个文章的思路为基础,设计了这么一个关系
主要还是这张规则表,通过在页面配置好相关的规则来实现对某个字段
的控制
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
的订单
规则配置
新增一个标记,可以理解成一个接口标识
这个接口下所有的规则
查看订单金额大于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})
,从而实现这个权限控制
给角色的这个订单列表接口
配置查看订单金额大于100且小于500的订单
这个规则,那么这个角色只能查看范围内的订单数据了。
代码
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 查看收货人地址
模糊查询钦南区
的订单
规则配置
新增一个规则,提供类型
为值
,单表查询可以不设置表别名,看图吧
配置角色在订单列表查询接口使用的规则
代码
在例子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