文章目录
-
- 权限是什么?
- 权限的方法论
-
- 权限功能
- 权限模型
-
- ACL
- RBAC
- 实践
-
- 一般数据权限需求
- 实现原理
-
- SQL改造
- 拦截时机
- 基于MP的实现
-
- 1.0版本
- 2.0版本
- 验证权限
-
- 简单查询
- 分页查询
- 多表查询
相关代码已上传:https://gitee.com/lakernote/easy-admin权限是什么? 为了解决用户和资源的操作关系, 让指定的用户,只能操作指定的资源。
已开源基于SpringBoot+Mybatisplus+Layui+SnakerFlow前后端分离轻量级工作流引擎的脚手架项目 easy-admin
文章图片
权限的方法论 权限功能
- 菜单权限:某用户,某角色能看到某菜单,例如:超管能看到所有的菜单,普通员工只能看到请假菜单。
- 粒度细的话可以做到按钮、标签显示不显示,字段显示不显示等。
- 主要是前端的事情。
- 接口权限:某用户,某角色能操作某接口,例如:超管能操作所有按钮接口,普通员工只能操作提交请假,查看请求列表。
- 因为上面的操作是可以把按钮隐掉了,一部分小白是操作不了相关操作,但是如果另一个码农知道了你的接口,就可以使用接口模拟操作了。
- 主要是后端的事情(鉴权)。
- 数据权限,某用户,某角色能查看某数据,例如:超管能看到所有人的请假单,普通员工只能看到自己的请假单
- 例如查看工资条接口,这个接口权限都能调用,但是工资条肯定只能看到自己的,超过可以看到所有人的。这里数据权限体会下。
- 能crud哪些数据。
- 能看到哪些字段。
- 主要是后端的事情。
ACL 基于资源,英文全程Access Control List
ACL是最早也是最基本的一种访问控制机制,它的原理非常简单:每一项资源,都配有一个权限列表,这个列表记录的就是哪些用户可以对这项资源执行CRUD中的那些操作。
当用户访问某资源时,会先检查这个列表中是否有关于当前用户的访问权限,从而确定当前用户可否执行相应的操作。总得来说,ACL是一种面向资源的访问控制模型,它的机制是围绕“资源”展开的。
优点:
实现简单,方便项目集成。
缺点:
需要维护大量的权限列表,在性能上有明显的缺陷。另外,对于拥有大量用户与众多资源的应用,管理访问控制列表本身就变成非常繁重的工作。
应用场景:在分享资源的场景,某个资源分享给某些人可用,例如分享我们的解说视频给群里的小伙伴。
权限列表示例:
resource_id | user_id | privilege |
---|---|---|
12 | 123 | 读/写/读写等 |
public static final Permission READ = new BasePermission(1 << 0, 'R');
// 1
public static final Permission WRITE = new BasePermission(1 << 1, 'W');
// 2
public static final Permission CREATE = new BasePermission(1 << 2, 'C');
// 4
public static final Permission DELETE = new BasePermission(1 << 3, 'D');
// 8
public static final Permission ADMINISTRATION = new BasePermission(1 << 4, 'A');
// 16
RBAC 基于角色,英文全程Role Based Access Control
RBAC是把用户按角色进行归类,通过用户的角色来确定用户能否针对某项资源进行某项操作。
优点:
RBAC相对于ACL最大的优势就是它简化了用户与权限的管理,通过对用户进行分类,使得角色与权限关联起来,而用户与权限变成了间接关联。
RBAC模型使得访问控制,特别是对用户的授权管理变得非常简单和易于维护,因此有广泛的应用。
除两上述两种主要的模型之外,还有包括:基于属性的访问控制ABAC和基于策略的访问控制PBAC等等。数据模型
RBAC还有其他几种变种,但是核心一样。
文章图片
RBAC变种
文章图片
实践 除了数据权限我看目前的实现都是基于RBAC做个权限标识符集合做判断,这里我们只讨论数据权限的。
菜单按钮类
此图来着互联网
文章图片
菜单、按钮、接口权限一般就是标识符集合。
文章图片
一般数据权限需求
常规的业务系统,数据粒度主要分为如下几种:
- 全部数据权限
- 部门数据权限:查看用户所在部门的数据。
- 部门及以下数据权限:查看用户所在部门及下属部门的数据。
- 本人数据权限:只能查看自己的数据。
- 自定义数据权限
- 可以实现各种奇奇怪怪需求,自定义SQL。
- 例如 财务人员只能看金额 小于一万的数据。
有用这种sql改造型的,也有结合上面ACL模式实现的,具体情况请结合业务选择,这里也是讲下SQL改造型的。
SQL改造 原理是在业务sql实际执行前改造原sql,也即是在原sql查询条件加上
create_by = xxx
,create_dept_id = xxx
等过滤条件。例如原业务sql如下:
select * from leave where leave_day > #{day}
如果当前用户只有本人数据权限则改造后sql如下:
select * from leave where leave_day > #{day} and create_by = #{currentUserId}
拦截时机
- 基于自定义注解+aop,在controller service层处理
- 基于mybatis拦截器
1.0版本 使用内置的
DataPermissionInterceptor
拦截器。第一步:实现自己的DataPermissionHandler
/**
* 这种只能处理查询 不能处理 cud
* 且不支持别名
*/
@Slf4j
public class LakerDataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
List split = StrUtil.split(mappedStatementId, '.');
...
try {switch (dataPower.get().getDataFilterType()) {
// 查看全部
case ALL:
return where;
// 查看本人所在组织机构以及下属机构
case DEPT_SETS:
// 创建IN 表达式
// 创建IN范围的元素集合
Set deptIds = userInfoAndPowers.getDeptIds();
// 把集合转变为JSQLParser需要的元素列表
ItemsList itemsList = new ExpressionList(deptIds.stream().map(LongValue::new).collect(Collectors.toList()));
InExpression inExpression = new InExpression(new Column("create_dept_id"), itemsList);
AndExpression andExpression = new AndExpression(where, inExpression);
log.info(WHERE, andExpression);
return andExpression;
// 查看当前部门的数据
case DEPT:
//= 表达式
// dept_id = deptId
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("create_dept_id"));
equalsTo.setRightExpression(new LongValue(userInfoAndPowers.getDeptId()));
// 创建 AND 表达式 拼接Where 和 = 表达式
// WHERE xxx AND dept_id = 3
AndExpression deptAndExpression = new AndExpression(where, equalsTo);
log.info(WHERE, deptAndExpression);
return deptAndExpression;
// 查看自己的数据
case SELF:
// create_by = userId
EqualsTo selfEqualsTo = new EqualsTo();
selfEqualsTo.setLeftExpression(new Column("create_by"));
selfEqualsTo.setRightExpression(new LongValue(userInfoAndPowers.getUserId()));
AndExpression selfAndExpression = new AndExpression(where, selfEqualsTo);
log.info(WHERE, selfAndExpression);
return selfAndExpression;
case DIY:
return new AndExpression(where, new StringValue(userInfoAndPowers.getSql()));
default:
break;
}
} catch (Exception e) {
log.error("LakerDataPermissionHandler.err", e);
}
...
return where;
}
}
第二步:把数据权限拦截器加入到拦截器链路中
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加数据权限插件
DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
LakerDataPermissionHandler lakerDataPermissionHandler = new LakerDataPermissionHandler();
// 添加自定义的数据权限处理器
dataPermissionInterceptor.setDataPermissionHandler(lakerDataPermissionHandler);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
但是这种实现方式,只支持数据查询权限,不支持数据删除权限、数据修改权限等。
2.0版本 第一步:实现自定义的权限拦截器,默认的权限拦截器只有
beforeQuery()
public class LakerDataPermissionV2Interceptor extends JsqlParserSupport implements InnerInterceptor {
private LakerV2DataPermissionHandler dataPermissionHandler = new LakerV2DataPermissionHandler();
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) return;
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect, (String) obj);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
parserSingle(mpBs.sql(), ms.getId());
}
...
}
第二步:实现自己的DataPermissionHandler
public class LakerV2DataPermissionHandler {
public static final String WHERE = " where {}";
@SneakyThrows
public Expression getSqlSegment(PlainSelect plainSelect, String mappedStatementId) {
// 获取原SQL Where 条件表达式
Expression where = plainSelect.getWhere();
// 获取sql语句的from 主表
Table fromItem = (Table) plainSelect.getFromItem();
// 有别名用别名,无别名用表名,防止字段冲突报错
Alias fromItemAlias = fromItem.getAlias();
String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();
...
switch (dataPower.get().getDataFilterType()) {// 查看自己的数据
case SELF:
// create_by = userId
EqualsTo selfEqualsTo = new EqualsTo();
selfEqualsTo.setLeftExpression(new Column(mainTableName + ".create_by"));
selfEqualsTo.setRightExpression(new LongValue(userInfoAndPowers.getUserId()));
AndExpression selfAndExpression = new AndExpression(where, selfEqualsTo);
log.info(WHERE, selfAndExpression);
return selfAndExpression;
case DIY:
return new AndExpression(where, new StringValue(userInfoAndPowers.getSql()));
default:
break;
...
}
}
第三步:把数据权限拦截器加入到拦截器链路中
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// V2版本
LakerDataPermissionV2Interceptor dataPermissionInterceptor = new LakerDataPermissionV2Interceptor();
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
验证权限 简单查询
原始sql
SELECT * FROM ext_leave WHERE (leave_day >= ?)
数据权限过滤sql
SELECT * FROM ext_leave WHERE (leave_day >= ?) AND ext_leave.create_by = 16
分页查询
原始sql
SELECT * FROM ext_leave WHERE (leave_day >= ?) LIMIT ?
数据权限过滤sql
SELECT * FROM ext_leave WHERE (leave_day >= ?) AND ext_leave.create_by = 16 LIMIT ?
多表查询
原始sql
SELECT l.*, u.nick_name uNickName, d.dept_name uDeptName
FROM ext_leave l
LEFT JOIN sys_user u ON u.user_id = l.create_by
LEFT JOIN sys_dept d ON d.dept_id = u.dept_id
WHERE (l.leave_day >= ?) ORDER BY l.create_time DESC LIMIT ?
数据权限过滤sql
SELECT l.*, u.nick_name uNickName, d.dept_name uDeptName
FROM ext_leave l
LEFT JOIN sys_user u ON u.user_id = l.create_by
LEFT JOIN sys_dept d ON d.dept_id = u.dept_id
WHERE (l.leave_day >= ?) AND l.create_by = 16 ORDER BY l.create_time DESC LIMIT ?
【《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等】别名情况也能自动识别
推荐阅读
- 【spring源码学习】事务
- 大数据|关于NLP相关技术全部在这里(预训练模型、图神经网络、模型压缩、知识图谱、信息抽取、序列模型、语法分析、文本处理...)
- SpringData ( xxx_xxx is not mapped)
- 镜像分层原理及容器层写时复制
- 一起刷好题|《力扣每日一题》—— 合并两个有序链表
- 一起刷好题|《牛客每日一题》链表分割、输出链表的倒数第k个结点
- 一起刷好题|力扣每日一题(环形链表II)
- 力扣简单题目集|力扣——合并两个有序数组
- Web服务教程入门介绍