《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等


文章目录

    • 权限是什么?
    • 权限的方法论
      • 权限功能
      • 权限模型
        • ACL
        • RBAC
    • 实践
      • 一般数据权限需求
      • 实现原理
        • SQL改造
        • 拦截时机
      • 基于MP的实现
        • 1.0版本
        • 2.0版本
    • 验证权限
      • 简单查询
      • 分页查询
      • 多表查询

相关代码已上传:https://gitee.com/lakernote/easy-admin
已开源基于SpringBoot+Mybatisplus+Layui+SnakerFlow前后端分离轻量级工作流引擎的脚手架项目 easy-admin
权限是什么? 为了解决用户和资源的操作关系, 让指定的用户,只能操作指定的资源。
《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等
文章图片

权限的方法论 权限功能
  • 菜单权限:某用户,某角色能看到某菜单,例如:超管能看到所有的菜单,普通员工只能看到请假菜单。
    • 粒度细的话可以做到按钮、标签显示不显示,字段显示不显示等。
    • 主要是前端的事情。
  • 接口权限:某用户,某角色能操作某接口,例如:超管能操作所有按钮接口,普通员工只能操作提交请假,查看请求列表。
    • 因为上面的操作是可以把按钮隐掉了,一部分小白是操作不了相关操作,但是如果另一个码农知道了你的接口,就可以使用接口模拟操作了。
    • 主要是后端的事情(鉴权)。
  • 数据权限,某用户,某角色能查看某数据,例如:超管能看到所有人的请假单,普通员工只能看到自己的请假单
    • 例如查看工资条接口,这个接口权限都能调用,但是工资条肯定只能看到自己的,超过可以看到所有人的。这里数据权限体会下。
    • 能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还有其他几种变种,但是核心一样。
数据模型
《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等
文章图片

RBAC变种
《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等
文章图片

实践 除了数据权限我看目前的实现都是基于RBAC做个权限标识符集合做判断,这里我们只讨论数据权限的。
菜单按钮类
此图来着互联网
《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等
文章图片

菜单、按钮、接口权限一般就是标识符集合。
《从零搭建开发脚手架》|从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等
文章图片

一般数据权限需求
常规的业务系统,数据粒度主要分为如下几种:
  • 全部数据权限
  • 部门数据权限:查看用户所在部门的数据。
  • 部门及以下数据权限:查看用户所在部门及下属部门的数据。
  • 本人数据权限:只能查看自己的数据。
  • 自定义数据权限
    • 可以实现各种奇奇怪怪需求,自定义SQL。
    • 例如 财务人员只能看金额 小于一万的数据。
实现原理
有用这种sql改造型的,也有结合上面ACL模式实现的,具体情况请结合业务选择,这里也是讲下SQL改造型的。
SQL改造 原理是在业务sql实际执行前改造原sql,也即是在原sql查询条件加上create_by = xxxcreate_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拦截器
基于MP的实现
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 按钮 接口 数据权限等】别名情况也能自动识别

    推荐阅读