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

文章目录

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

相关代码已上传:https://gitee.com/lakernote/easy-admin
已开源基于SpringBoot+Mybatisplus+Layui+SnakerFlow前后端分离轻量级工作流引擎的脚手架项目 easy-admin

权限是什么?

为了解决用户和资源的操作关系, 让指定的用户,只能操作指定的资源

从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等_第1张图片

权限的方法论

权限功能

  • 菜单权限:某用户,某角色能看到某菜单,例如:超管能看到所有的菜单,普通员工只能看到请假菜单。
    • 粒度细的话可以做到按钮、标签显示不显示,字段显示不显示等。
    • 主要是前端的事情。
  • 接口权限:某用户,某角色能操作某接口,例如:超管能操作所有按钮接口,普通员工只能操作提交请假,查看请求列表。
    • 因为上面的操作是可以把按钮隐掉了,一部分小白是操作不了相关操作,但是如果另一个码农知道了你的接口,就可以使用接口模拟操作了。
    • 主要是后端的事情(鉴权)。
  • 数据权限,某用户,某角色能查看某数据,例如:超管能看到所有人的请假单,普通员工只能看到自己的请假单
    • 例如查看工资条接口,这个接口权限都能调用,但是工资条肯定只能看到自己的,超过可以看到所有人的。这里数据权限体会下。
    • 能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 按钮 接口 数据权限等_第2张图片

RBAC变种
从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等_第3张图片

实践

除了数据权限我看目前的实现都是基于RBAC做个权限标识符集合做判断,这里我们只讨论数据权限的。

菜单按钮类

此图来着互联网

从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等_第4张图片

菜单、按钮、接口权限一般就是标识符集合。

从零搭建开发脚手架 细说权限管理ACL RBAC 按钮 接口 数据权限等_第5张图片

一般数据权限需求

常规的业务系统,数据粒度主要分为如下几种:

  • 全部数据权限

  • 部门数据权限:查看用户所在部门的数据。

  • 部门及以下数据权限:查看用户所在部门及下属部门的数据。

  • 本人数据权限:只能查看自己的数据。

  • 自定义数据权限

    • 可以实现各种奇奇怪怪需求,自定义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<String> split = StrUtil.split(mappedStatementId, '.');
        ...
        try {

            switch (dataPower.get().getDataFilterType()) {
                // 查看全部
                case ALL:
                    return where;
                // 查看本人所在组织机构以及下属机构
                case DEPT_SETS:
                    // 创建IN 表达式
                    // 创建IN范围的元素集合
                    Set<Long> 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 ?

别名情况也能自动识别

你可能感兴趣的:(《从零搭建开发脚手架》,《三心二意,-,博览群书》,java,spring,boot,前端)