柏竹 柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档

柏竹

奋斗柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档
  • Java基础

  • JavaWeb

  • 拓展技术

  • 框架技术

  • 数据库

  • 数据结构

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringClound

  • Ruoyi-Vue-Plus

    • ruoyi-vue-plus-基础功能
    • ruoyi-vue-plus-权限控制
      • 权限控制
        • 部门管理
        • 角色管理
        • 菜单权限
        • 数据权限
      • 实现原理
        • DataScopeType数据权限模板定义
        • DataPermission&DataColumn数据权限组注解
        • PlusDataPermissionInterceptor数据权限SQL拦截器
        • PlusDataPermissionHandler数据权限处理器
        • DataPermissionHelper数据权限助手
        • SysDataScopeServiceBean处理权限
      • 自定义数据权限
    • ruoyi-vue-plus-表格操作
    • ruoyi-vue-plus-缓存功能
    • ruoyi-vue-plus-日志功能
    • ruoyi-vue-plus-线程相关
    • ruoyi-vue-plus-OSS功能
    • ruoyi-vue-plus-代码生成功能
    • ruoyi-vue-plus-多数据源
    • ruoyi-vue-plus-任务调度
    • ruoyi-vue-plus-监控功能
    • ruoyi-vue-plus-国际化
    • ruoyi-vue-plus-XSS功能
    • ruoyi-vue-plus-防重幂&限流 功能
    • ruoyi-vue-plus-推送功能
    • ruoyi-vue-plus-序列化功能
    • ruoyi-vue-plus-数据加密
    • ruoyi-vue-plus-单元测试
    • ruoyi-vue-plus-前端插件
    • ruoyi-vue-plus-前端工具篇
    • ruoyi-vue-plus-部署篇
    • ruoyi-vue-plus-前端篇
    • ruoyi-vue-plus-后端工具篇
    • ruoyi-vue-plus-框架篇
    • ruoyi-vue-plus-问题解决
  • 后端
  • Ruoyi-Vue-Plus
柏竹
2023-11-14
目录

ruoyi-vue-plus-权限控制

ruoyi-vue-plus应用文档 : https://plus-doc.dromara.org (opens new window)

# 权限控制

该框架主要以 部门 和 角色 两个维度进行权限分配

前置说明

  1. 表必须包含字段 : user_id 和 dept_id 作为 当前表的数据控制
  2. 新增数据前必须为以上两个字段进行添加值

涉及基本库表

表 说明
sys_dept 部门表
sys_role 角色表
sys_user 用户表
sys_role_dept 角色部门关联表
sys_user_role 用户角色关联表
sys_role_menu 角色菜单关联表

# 部门管理

顾名思义 , 就是将用户划分不同组织 , 方便管理

不涉及整体权限控制 , 仅用 dept_id 划分范围应用

# 角色管理

角色在系统中 , 充当相对重要的功能 , 能够划分 功能细分化 , 根据不同用户划分角色即可分配权限等明细应用!

# 菜单权限

菜单权限的赋予 , 在库表中 , 也不难发现 , sys_role_menu关联表 主要实现 不同角色赋予不同的菜单

# 数据权限

数据权限的控制是比角色更为细化的权限分配 , 是在库表中的字段层面控制 , 主要通过 user_id 和 dept_id 字段所控制

权限分配分为几个角度进行约束 , 以及 SQL约束结构 . 自定义拓展约束点击跳转

  • 全部数据权限 (无约束)

  • 自定义数据权限 (指定部门约束)

    dept_id IN ( xx , xx , .. )

  • 本部门约束 (当前部门约束)

    dept_id = xx

  • 本部门及以下数据权限 (父子联动)

    dept_id IN ( xx, xx, .. )

  • 仅本人数据权限

    user_id = xx

提示

在实际场景中 , 可通过多身份实现SQL约束 . 也就是说给即将要限制的 用户 加多一个 本部门及以下数据权限 即可

# 实现原理

通过 拦截器InnerInterceptor接口 , 重写 sql 操作的方法 . 最为核心的还是使用了 SpEl表达式解析 (opens new window)

约束过程必要信息

  • 权限范围 根据不同身份划分 , 从而锁定 DataScopeType枚举 中指定的模板
  • SQL填充的字段名 , 根据 Mapper层 的 @DataPermission&@DataColumn注解
  • SQL填充的匹配值
    • 根据 DataPermissionHelper类 请求上下文临时存储使用 (使用方式跟Map误差)
    • 根据 Bean解析器 , 调取方法返回 字符串 填充

提示

@DataPermission可以写在类上 , 使其类的所有方法均可生效

涉及类

类 说明
DataScopeType 数据权限模板定义
DataPermission 数据权限组注解
DataColumn 具体的数据全新字段标注
PlusDataPermissionInterceptor 数据权限SQL拦截器
PlusDataPermissionHandler 数据权限处理器
DataPermissionHelper 数据权限助手
SysDataScopeService 自定义Bean处理数据权限

提示

以上关键类 , 可以进行点击跳转

# DataScopeType数据权限模板定义

点击展开
@Getter
@AllArgsConstructor
public enum DataScopeType {

    /**
     * 全部数据权限
     */
    ALL("1", "", ""),

    /**
     * 自定数据权限
     */
    CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""),

    /**
     * 部门数据权限
     */
    DEPT("3", " #{#deptName} = #{#user.deptId} ", ""),

    /**
     * 部门及以下数据权限
     */
    DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),

    /**
     * 仅本人数据权限
     */
    SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
	
    /**
    *  模板唯一
    */
    private final String code;

    /**
     * 语法 采用 spel 模板表达式
     */
    private final String sqlTemplate;

    /**
     * 不满足 sqlTemplate 则填充
     */
    private final String elseSql;

    public static DataScopeType findCode(String code) {
        if (StringUtils.isBlank(code)) {
            return null;
        }
        for (DataScopeType type : values()) {
            if (type.getCode().equals(code)) {
                return type;
            }
        }
        return null;
    }
}

可以看到拼接的占位符应用

占位符 说明
#{#deptName} 在 @DataColumn注解 中的key 对应的模板占位符填充其值
#{#user.userId} 在 LoginHelper类 中拿去当前用户userId
#{@sdss.getRoleCustom( #user.roleId )} 通过在 SysDataScopeService自定义的Bean中调取方法拿到字符串
#{@sdss.getDeptAndChild( #user.deptId )} ) 通过在 SysDataScopeService自定义的Bean中调取方法拿到字符串

# DataPermission&DataColumn数据权限组注解

嵌套复合注解应用

点击展开
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
    DataColumn[] value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {

    /**
     * 占位符关键字
     */
    String[] key() default "deptName";

    /**
     * 占位符替换值
     */
    String[] value() default "dept_id";

}

# PlusDataPermissionInterceptor数据权限SQL拦截器

数据权限SQL拦截器 . 继承了 JsqlParserSupport(sql解析器) , 实现了 InnerInterceptor(MP内部拦截器)

拦截重写的方法意图

  • beforeQuery() 查询前 : 检查 忽略注解 和 无注解方法 , 并且通过 JSqlParser工具 将SQL解析 , 方便操作
  • beforePrepare() 准备前 : 对 删除和修改 通过 JSqlParser工具 将SQL解析 , 方便操作
  • processSelect() 查询操作 : 对 查询 进行SQL约束处理
  • processUpdate() 修改操作 : 对 修改 进行SQL约束处理
  • processDelete() 删除操作 : 对 删除 进行SQL约束处理

对SQL约束处理的核心方法 dataPermissionHandler.getSqlSegment()

点击展开
public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {

    private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 检查忽略注解
        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            return;
        }
        // 检查是否无效 无数据权限注解
        if (dataPermissionHandler.isInvalid(ms.getId())) {
            return;
        }
        // 解析 sql 分配对应方法
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        // 准备前处理更新 和 删除
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
                return;
            }
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
        }
    }
	
    /**
     * 查询前 , 重写sql
     */
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect) {
            // 简单查询
            this.setWhere((PlainSelect) selectBody, (String) obj);
        } else if (selectBody instanceof SetOperationList) {
            // 联合查询
            SetOperationList setOperationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = setOperationList.getSelects();
            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
        }
    }
	
    /**
     * 修改前 , 重写sql
     */
    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
        if (null != sqlSegment) {
            update.setWhere(sqlSegment);
        }
    }
	
    /**
     * 删除前 , 重写sql
     */
    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
        if (null != sqlSegment) {
            delete.setWhere(sqlSegment);
        }
    }

    /**
     * 设置 where 条件
     *
     * @param plainSelect       查询对象
     * @param mappedStatementId 执行方法id
     */
    protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }

}

# PlusDataPermissionHandler数据权限处理器

数据权限过滤 核心处理类

大致流程

  1. 获取注解填充数据 , 根据全限定名方法 反射获取

    DataColumn[] dataColumns = findAnnotation(mappedStatementId);

  2. 请求上下文设置参数填充 值 (当前用户)

    DataPermissionHelper.setVariable("user", currentUser);

  3. 构建SQL Where约束

    String dataFilterSql = buildDataFilter(dataColumns, isSelect);

  4. 响应 SQL约束表达式 设置where约束

    plainSelect.setWhere(sqlSegment);

点击展开
@Slf4j
public class PlusDataPermissionHandler {

    /**
     * 方法或类(名称) 与 注解的映射关系缓存
     */
    private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();

    /**
     * 无效注解方法缓存用于快速返回
     */
    private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();

    /**
     * spel 解析器
     */
    private final ExpressionParser parser = new SpelExpressionParser();
    private final ParserContext parserContext = new TemplateParserContext();
    /**
     * bean解析器 用于处理 spel 表达式中对 bean 的调用
     */
    private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());

    /**
     * SQL 拼接核心方法
     * @param where 约束操作的表达式
     * @param mappedStatementId mapper全限定名路径
     * @param isSelect 是否查询
     */
    public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
        DataColumn[] dataColumns = findAnnotation(mappedStatementId);
        // 缓存无效注解 , 避免不必要的数据加载
        if (ArrayUtil.isEmpty(dataColumns)) {
            // 将没有注解的方法缓存起来 , 根据全限定名方法判断是否含有缓存
            invalidCacheSet.add(mappedStatementId);
            return where;
        }
        // 存储当前用户 user , 一遍请求应用
        LoginUser currentUser = DataPermissionHelper.getVariable("user");
        if (ObjectUtil.isNull(currentUser)) {
            currentUser = LoginHelper.getLoginUser();
            DataPermissionHelper.setVariable("user", currentUser);
        }
        // 如果是超级管理员,则不过滤数据
        if (LoginHelper.isAdmin()) {
            return where;
        }
        // 拿到注解信息构建 约束sql
        String dataFilterSql = buildDataFilter(dataColumns, isSelect);
        if (StringUtils.isBlank(dataFilterSql)) {
            return where;
        }
        try {
            Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
            // 数据权限使用单独的括号 防止与其他条件冲突
            Parenthesis parenthesis = new Parenthesis(expression);
            if (ObjectUtil.isNotNull(where)) {
                return new AndExpression(where, parenthesis);
            } else {
                return parenthesis;
            }
        } catch (JSQLParserException e) {
            throw new ServiceException("数据权限解析异常 => " + e.getMessage());
        }
    }

    /**
     * 构造数据过滤sql
     */
    private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
        // 更新或删除需满足所有条件 (查询采用 OR ; 删除/修改 采用 AND)
        String joinStr = isSelect ? " OR " : " AND ";
        LoginUser user = DataPermissionHelper.getVariable("user");
        // 评估对象 (用户构建约束sql)
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 设置 bean解析器
        context.setBeanResolver(beanResolver);
        // 将 请求的所有上下文 设置到 context 中
        DataPermissionHelper.getContext().forEach(context::setVariable);
        Set<String> conditions = new HashSet<>();
        for (RoleDTO role : user.getRoles()) {
            user.setRoleId(role.getRoleId());
            // 获取角色权限泛型 (根据其对象获取模板sql)
            DataScopeType type = DataScopeType.findCode(role.getDataScope());
            if (ObjectUtil.isNull(type)) {
                throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
            }
            // 全部数据权限直接返回
            if (type == DataScopeType.ALL) {
                return "";
            }
            boolean isSuccess = false;
            // 拼接环节
            for (DataColumn dataColumn : dataColumns) {
                if (dataColumn.key().length != dataColumn.value().length) {
                    throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
                }
                // 不包含 key 变量 则不处理
                if (!StringUtils.containsAny(type.getSqlTemplate(),
                    Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
                )) {
                    continue;
                }
                // 设置注解变量 key 为表达式变量 value 为变量值
                for (int i = 0; i < dataColumn.key().length; i++) {
                    context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
                }

                // 解析sql模板并填充 (bean解析填充)
                // bean解析器调用方法填充数据
                String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
                conditions.add(joinStr + sql);
                isSuccess = true;
            }
            // 未处理成功则填充替补模板方案
            if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
                conditions.add(joinStr + type.getElseSql());
            }
        }

        if (CollUtil.isNotEmpty(conditions)) {
            // 将 conditions 内的 字符串按顺序拼接起来
            String sql = StreamUtils.join(conditions, Function.identity(), "");
            /**
             * 拼接后的结果可能为以下结果 , 需要排除前缀
             * OR  dept_id IN ( xx,xx,xx ) OR ...
             */
            return sql.substring(joinStr.length());
        }
        return "";
    }

    /**
     * 通过方法的全限定名称进行反射解析获取反射数据
     */
    private DataColumn[] findAnnotation(String mappedStatementId) {
        StringBuilder sb = new StringBuilder(mappedStatementId);
        int index = sb.lastIndexOf(".");
        // 类全限定名路径
        String clazzName = sb.substring(0, index);
        // 方法名
        String methodName = sb.substring(index + 1, sb.length());
        Class<?> clazz = ClassUtil.loadClass(clazzName);
        // 过滤掉不相符的方法名
        List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
            .filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
        DataPermission dataPermission;
        // 获取方法注解
        for (Method method : methods) {
            dataPermission = dataPermissionCacheMap.get(mappedStatementId);
            if (ObjectUtil.isNotNull(dataPermission)) {
                return dataPermission.value();
            }
            if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
                dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
                dataPermissionCacheMap.put(mappedStatementId, dataPermission);
                return dataPermission.value();
            }
        }
        // 检查类是否包含 DataPermission注解 并且获取
        dataPermission = dataPermissionCacheMap.get(clazz.getName());
        if (ObjectUtil.isNotNull(dataPermission)) {
            return dataPermission.value();
        }
        // 获取类注解
        if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
            dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
            dataPermissionCacheMap.put(clazz.getName(), dataPermission);
            return dataPermission.value();
        }
        return null;
    }

    /**
     * 是否为无效方法 无数据权限
     */
    public boolean isInvalid(String mappedStatementId) {
        return invalidCacheSet.contains(mappedStatementId);
    }
}

# DataPermissionHelper数据权限助手

数据权限助手 . 通过 SaHolder.getStorage() , 拿到请求上下文存储数据

点击展开
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked cast")
public class DataPermissionHelper {

    private static final String DATA_PERMISSION_KEY = "data:permission";

    public static <T> T getVariable(String key) {
        Map<String, Object> context = getContext();
        return (T) context.get(key);
    }


    public static void setVariable(String key, Object value) {
        Map<String, Object> context = getContext();
        context.put(key, value);
    }

    public static Map<String, Object> getContext() {
        SaStorage saStorage = SaHolder.getStorage();
        Object attribute = saStorage.get(DATA_PERMISSION_KEY);
        if (ObjectUtil.isNull(attribute)) {
            saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
            attribute = saStorage.get(DATA_PERMISSION_KEY);
        }
        if (attribute instanceof Map) {
            return (Map<String, Object>) attribute;
        }
        throw new NullPointerException("data permission context type exception");
    }

    /**
     * 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
     */
    public static void enableIgnore() {
        InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
    }

    /**
     * 关闭忽略数据权限
     */
    public static void disableIgnore() {
        InterceptorIgnoreHelper.clearIgnoreStrategy();
    }

    /**
     * 在忽略数据权限中执行
     *
     * @param handle 处理执行方法
     */
    public static void ignore(Runnable handle) {
        enableIgnore();
        try {
            handle.run();
        } finally {
            disableIgnore();
        }
    }

    /**
     * 在忽略数据权限中执行
     *
     * @param handle 处理执行方法
     */
    public static <T> T ignore(Supplier<T> handle) {
        enableIgnore();
        try {
            return handle.get();
        } finally {
            disableIgnore();
        }
    }

}

# SysDataScopeServiceBean处理权限

数据权限 服务 , 通过 Bean解析器 解析调用方法 , 对其SQL进行填充约束值

如果自定义写一个Bean , 并且是用于模板中填充使用的方法 , 则当前Service不能 调用含有 @DataPermission注解 方法 , 可能会导致递归死循环问题

点击展开
@RequiredArgsConstructor
@Service("sdss")
public class SysDataScopeServiceImpl implements ISysDataScopeService {

    private final SysRoleDeptMapper roleDeptMapper;
    private final SysDeptMapper deptMapper;

    @Override
    public String getRoleCustom(Long roleId) {
        List<SysRoleDept> list = roleDeptMapper.selectList(
            new LambdaQueryWrapper<SysRoleDept>()
                .select(SysRoleDept::getDeptId)
                .eq(SysRoleDept::getRoleId, roleId));
        if (CollUtil.isNotEmpty(list)) {
            return StreamUtils.join(list, rd -> Convert.toStr(rd.getDeptId()));
        }
        return null;
    }

    @Override
    public String getDeptAndChild(Long deptId) {
        List<SysDept> deptList = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
            .select(SysDept::getDeptId)
            .apply(DataBaseHelper.findInSet(deptId, "ancestors")));
        List<Long> ids = StreamUtils.toList(deptList, SysDept::getDeptId);
        ids.add(deptId);
        List<SysDept> list = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
            .select(SysDept::getDeptId)
            .in(SysDept::getDeptId, ids));
        if (CollUtil.isNotEmpty(list)) {
            return StreamUtils.join(list, d -> Convert.toStr(d.getDeptId()));
        }
        return null;
    }

}

# 自定义数据权限

大概流程

  1. 在前端数据权限的下拉框中追加标识 src/views/system/role/index.vue dataScopeOptions数组追加对象 (写死)
  2. 在 DataScopeType类 中 , 写一个自己的sql约束模板 , 需要需要对应上
  3. 在 mapper层 , 使用 @DataPermission注解 填充占位符
  4. 在调用 mapper方法 前 , 可通过 *DataPermissionHelper.serVariable()*方法 , 将其值填充到模板
  5. 测试查询观察SQL拼接情况 ...

注意

  • 在写SQL模板时 , 前后部分建议追加一个空格 , 防止拼接时 前后混淆
  • DataPermissionHelper类 中设置的值 , 仅在本请求上下文有效
#ruoyi-vue-plus

← ruoyi-vue-plus-基础功能 ruoyi-vue-plus-表格操作→

最近更新
01
HTTPS自动续签
10-21
02
博客搭建-简化版(脚本)
10-20
03
ruoyi-vue-plus-部署篇
07-13
更多文章>
Theme by Vdoing | Copyright © 2019-2024 | 桂ICP备2022009417号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式