来源:juejin.cn/post/7267090979537944631
来源:juejin.cn/post/7308992638468227109
-
1 前言
-
2 需求
-
3 设计思路
-
4 例子1 查看
订单金额大于100且小于500
的订单-
规则配置
-
代码
-
-
5 例子2 查看
收货人地址
模糊查询钦南区
的订单-
规则配置
-
代码
-
-
6 当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查
-
EntityController
-
EntityService
-
EntityServiceImpl
-
自定义注解
-
-
[7 项目地址 wonder-server: 一个有意思的权限管理系统2]
-
参考资料
1 前言
我一年java,在小公司,当前公司权限这块都没有成熟的方案,目前我知道权限分为功能权限和数据权限,我不知道数据权限这块大家是怎么解决的,但在实际项目中我遇到数据权限真的复杂,你永远不知道业主在这方面的需求是什么。
我也有去搜索在这方面是怎么做,但是我在gitee、github搜到的权限管理系统他们都是这么实现的:查看全部数据
、自定义数据权限
、本部门数据权限
、本部门及以下数据
、仅本人数据权限
,但是这种控制粒度完全不够的,所以就想自己实现一下。
2 需求
需求一 有一个单位
和企业
的树,企业都是挂在某个单位下面的,企业是分类型的(餐饮企业
、经营企业
、生产企业
),业主需要单位的人限定某些单位只能看一个或他指定的某个类型的企业。现在指定角色A
只能查看餐饮
、经营
的企业
,那就只能使用查看自定义部门数据
这个,然后在10000家企业里面慢慢勾选符合的企业,这样可以是可以,但是我觉得这样做不太妥。
估计有人说:那你把三种类型的企业分组,餐饮企业挂在餐饮分组下,其他同理。然后用自定义数据权限
选中那两个不就可以了吗?可以是可以,但是我不是业主,业主要求了那些企业必须挂在哪些单位
下,在页面显示的树也不能显示什么餐饮企业分组
、生产企业
... 说到底,除非你有办法改变业主的想法。
需求二 类似订单吧,角色A
只能查看未支付
的订单,角色B
只能看交易金额在100~1000元
的订单。
用通用的那5种权限对这两个需求已经是束手无策了。
3 设计思路
后来我看到一篇文章【数据权限就该这么实现(设计篇) [1]】,对我有很大的启发,从数据库字段下手,用规则来处理
图片
我以这个文章的思路为基础,设计了这么一个关系
图片
主要还是这张规则表,通过在页面配置好相关的规则来实现对某个字段
的控制
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提供的方法接口,那么需要开发者手动处理,返回什么那么是开发者自定义了。
所以字段你自己定,联表也没问题、反射执行什么方法、参数是什么、过程怎么样也是你自己定,灵活性很高(至少我是这么认为的,哈哈哈哈哈哈)
新建 DataScopeHandler
@Component
public class DataScopeHandler implements DataPermissionHandler {
Map<String, ExpressStrategy> expressStrategyMap = new HashMap<>();
@PostConstruct
public void init() {
expressStrategyMap.put(ExpressionEnum.EQ.toString(), new EqStrategyImpl());
expressStrategyMap.put(ExpressionEnum.NE.toString(), new NeStrategyImpl());
// ....其他情况
}
@Override
public Expression getSqlSegment(Expression oldWhere, String mappedStatementId) {
DataScopeAspect.DataScopeParam dataScopeParam = DataScopeAspect.getDataScopeParam();
// 没有规则就不限制
if (dataScopeParam == null || dataScopeParam.getDataScopeInfo() == null || CollectionUtil.isEmpty(dataScopeParam.getDataScopeInfo().getRuleList()) || SecurityUtil.isAdmin()) {
return oldWhere;
}
Expression newWhere = null;
DataScopeInfo dataScopeInfo = dataScopeParam.getDataScopeInfo();
List<RuleDto> ruleList = dataScopeInfo.getRuleList();
for (RuleDto rule : ruleList) {
ExpressStrategy expressStrategy = expressStrategyMap.get(rule.getExpression());
if (expressStrategy == null)
throw new IllegalArgumentException("错误的表达式:" + rule.getExpression());
newWhere = expressStrategy.apply(rule, newWhere);
}
return oldWhere == null ? newWhere : new AndExpression(oldWhere, new Parenthesis(newWhere));
}
}
使用策略模式 ExpressStrategy
public interface ExpressStrategy {
Expression apply(RuleDto rule, Expression where);
default Object getValue(RuleDto rule) {
if (rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {
return rule.getResult();
} else if (rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {
return rule.getValue1();
} else {
throw new IllegalArgumentException("错误的提供类型");
}
}
default Column getColumn(RuleDto rule) {
String sql = "".equals(rule.getTableAlias()) || rule.getTableAlias() == null ? rule.getColumnName() : rule.getTableAlias() + "." + rule.getColumnName();
return new Column(sql);
}
default boolean isOr(String spliceType) {
if (!spliceType.equals(SpliceTypeEnum.AND.toString()) && !spliceType.equals(SpliceTypeEnum.OR.toString())) {
throw new IllegalArgumentException("错误的拼接类型:" + spliceType);
}
return spliceType.equals(SpliceTypeEnum.OR.toString());
}
}
其中一种策略 EqStrategyImpl
这里只列举其中一种情况,我们处理 =
操作
public class EqStrategyImpl implements ExpressStrategy{
@Override
public Expression apply(RuleDto rule, Expression where) {
boolean or = isOr(rule.getSpliceType());
Column column = getColumn(rule);
Object value = getValue(rule);
StringValue valueExpression = new StringValue((String) value);
EqualsTo equalsTo = new EqualsTo(column, valueExpression);
if (or) {
where = where == null ? equalsTo : new OrExpression(where, equalsTo);
} else {
where = where == null ? equalsTo : new AndExpression(where, equalsTo);
}
return where;
}
}
注册 DataScopeHandler
@Configuration
public class MyBatisPlusConfig {
@Autowired
private DataScopeHandler dataScopeHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加自定义的数据权限处理器
DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
dataPermissionInterceptor.setDataPermissionHandler(dataScopeHandler);
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
自定义注解@DataScope
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 标记这是哪个接口,
*/
String value();
}
切面处理
@Aspect
@Slf4j
@Component
public class DataScopeAspect {
@Autowired
private MarkService dataScopeService;
// 通过ThreadLoc