ToB企服应用市场:ToB评测及商务社交产业平台

标题: 芋道源码解析之数据权限 [打印本页]

作者: 徐锦洪    时间: 3 天前
标题: 芋道源码解析之数据权限
文章首发于我的博客:https://blog.liuzijian.com/post/source-code-yudao-data-permission.html
博主和芋道源码作者及其官方开发团队无任何关联
一、引言

芋道的数据权限模块代码,涉及的类和方法很多,环环相扣,必要运行项目一步一步debug分析才能看懂。该模块的代码按照功能细分,大抵可以分为两部门:
1.数据权限SQL拦截器:根据定义好的数据权限规则来为涉及到的表在更新、查询和删除时重写(追加)SQL条件,使得用户只能访问到权限范围内的数据。
2.数据权限注解处理器:基于Spring AOP实现,通过自定义一个数据权限注解并实现一个注解处理器来为某些方法单独指定数据权限规则。
两个部门必要共同利用。
二、数据权限SQL拦截器

2.4.0-jdk8-SNAPSHOT版本的数据权限功能是基于mybatis-plus的插件机制实现的,具体是对实行修改、删除和查询的SQL进行拦截、解析,然后再根据数据权限规则对必要限制的表重写(追加)查询条件。利用该插件必要实现MultiDataPermissionHandler接口。
2.1 主要涉及类和接口

2.1.1 Class Diagram


2.1.2 mybatis-plus

2.1.3 yudao

2.2 实行流程源码解读

2.2.1 Sequence Diagram


2.2.2 DataPermissionInterceptor

该类是mybatis-plus数据权限插件的实行入口,是SQL解析和重写功能的起点。
该类在SQL实行前,会对实行的动作进行拦截,并拿到要实行的SQL,递归对SQL语句各处进行扫描,扫描到表和条件时,调用DataPermissionHandler获取当前表的数据权限where条件(Expression)对象,再和业务逻辑的where条件拼在一起,从而实现数据库层面的数据权限控制。
  1. public class DataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
  2.     private DataPermissionHandler dataPermissionHandler;
  3.     @SuppressWarnings("RedundantThrows")
  4.     @Override
  5.     public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  6.         if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
  7.             return;
  8.         }
  9.         PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
  10.         mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
  11.     }
  12.     @Override
  13.     public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
  14.         PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
  15.         MappedStatement ms = mpSh.mappedStatement();
  16.         SqlCommandType sct = ms.getSqlCommandType();
  17.         if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
  18.             if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
  19.                 return;
  20.             }
  21.             PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
  22.             mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
  23.         }
  24.     }
  25.     ......
  26.     @Override
  27.     public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
  28.         if (dataPermissionHandler == null) {
  29.             return null;
  30.         }
  31.         // 只有新版数据权限处理器才会执行到这里
  32.         final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
  33.         return handler.getSqlSegment(table, where, whereSegment);
  34.     }   
  35. }
复制代码


解读:
2.2.3 DataPermissionRuleHandler

该类是接口DataPermissionHandler的实现,供拦截器DataPermissionInterceptor调用,用于找到某个表在当前业务下适用的全部的数据权限规则,并汇总,然后再返回一个总的数据权限规则对象给拦截器
  1. @RequiredArgsConstructor
  2. public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
  3.     private final DataPermissionRuleFactory ruleFactory;
  4.     @Override
  5.     public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
  6.         // 获得 Mapper 对应的数据权限的规则
  7.         List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
  8.         if (CollUtil.isEmpty(rules)) {
  9.             return null;
  10.         }
  11.         // 生成条件
  12.         Expression allExpression = null;
  13.         for (DataPermissionRule rule : rules) {
  14.             // 判断表名是否匹配
  15.             String tableName = MyBatisUtils.getTableName(table);
  16.             if (!rule.getTableNames().contains(tableName)) {
  17.                 continue;
  18.             }
  19.             // 单条规则的条件
  20.             Expression oneExpress = rule.getExpression(tableName, table.getAlias());
  21.             if (oneExpress == null) {
  22.                 continue;
  23.             }
  24.             // 拼接到 allExpression 中
  25.             allExpression = allExpression == null ? oneExpress
  26.                     : new AndExpression(allExpression, oneExpress);
  27.         }
  28.         return allExpression;
  29.     }
  30. }
复制代码

解读:
2.2.4 DataPermissionRuleFactory

数据权限规则"工厂",供DataPermissionRuleHandler调用来获取当前业务下适用的数据权限规则,该类会共同数据权限注解处理器来利用,从线程上下文DataPermissionContextHolder中获取加了@DataPermission数据权限注解且是最近一级调用当前mapper实行SQL的谁人业务方法上面的@DataPermission注解,根据注解上的数据权限规则进行匹配,返回当前业务方法下具体适用的数据权限规则,而不是简朴的把全部定义好了的数据权限规则都返回。
  1. public interface DataPermissionRuleFactory {
  2.     /**
  3.      * 获得所有数据权限规则数组
  4.      *
  5.      * @return 数据权限规则数组
  6.      */
  7.     List<DataPermissionRule> getDataPermissionRules();
  8.     /**
  9.      * 获得指定 Mapper 的数据权限规则数组
  10.      *
  11.      * @param mappedStatementId 指定 Mapper 的编号
  12.      * @return 数据权限规则数组
  13.      */
  14.     List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
  15. }
复制代码
  1. @RequiredArgsConstructor
  2. public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
  3.     /**
  4.      * 数据权限规则数组
  5.      */
  6.     private final List<DataPermissionRule> rules;
  7.     @Override
  8.     public List<DataPermissionRule> getDataPermissionRules() {
  9.         return rules;
  10.     }
  11.     @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
  12.     public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
  13.         // 1. 无数据权限
  14.         if (CollUtil.isEmpty(rules)) {
  15.             return Collections.emptyList();
  16.         }
  17.         // 2. 未配置,则默认开启
  18.         DataPermission dataPermission = DataPermissionContextHolder.get();
  19.         if (dataPermission == null) {
  20.             return rules;
  21.         }
  22.         // 3. 已配置,但禁用
  23.         if (!dataPermission.enable()) {
  24.             return Collections.emptyList();
  25.         }
  26.         // 4. 已配置,只选择部分规则
  27.         if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
  28.             return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
  29.                     .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
  30.         }
  31.         // 5. 已配置,只排除部分规则
  32.         if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
  33.             return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
  34.                     .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
  35.         }
  36.         // 6. 已配置,全部规则
  37.         return rules;
  38.     }
  39. }
复制代码
解读:
2.2.5 DataPermissionRule

DataPermissionRule,数据权限规则接口,用于定义某种数据权限规则,必要通过getTableNames()来声明适用的表,再通过Expression getExpression(String tableName, Alias tableAlias)来定义某个表的数据权限条件
  1. public interface DataPermissionRule {
  2.     /**
  3.      * 返回需要生效的表名数组
  4.      * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
  5.      *
  6.      * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
  7.      *
  8.      * @return 表名数组
  9.      */
  10.     Set<String> getTableNames();
  11.     /**
  12.      * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
  13.      *
  14.      * @param tableName 表名
  15.      * @param tableAlias 别名,可能为空
  16.      * @return 过滤条件 Expression 表达式
  17.      */
  18.     Expression getExpression(String tableName, Alias tableAlias);
  19. }
复制代码
DeptDataPermissionRule,yudao自带的一个默认的数据权限规则实现类,可以针对体系中全部的表实现本人、本部门、本部门及以下、指定部门、无任何权限和无任何限制的6种数据权限。必要利用该规则的模块只必要将必要限制数据权限的表和其中对应的字段注册到这个类中,即可实现根据每个用户的数据权限范围对不同的表进行个人和部门级别的数据权限控制,实现这6种权限。
[code]@AllArgsConstructor@Slf4jpublic class DeptDataPermissionRule implements DataPermissionRule {    /**     * LoginUser 的 Context 缓存 Key     */    protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName();    private static final String DEPT_COLUMN_NAME = "dept_id";    private static final String USER_COLUMN_NAME = "user_id";    static final Expression EXPRESSION_NULL = new NullValue();    private final PermissionApi permissionApi;    /**     * 基于部门的表字段配置     * 一样平常情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。     *     * key:表名     * value:字段名     */    private final Map deptColumns = new HashMap();    /**     * 基于用户的表字段配置     * 一样平常情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。     *     * key:表名     * value:字段名     */    private final Map userColumns = new HashMap();    /**     * 全部表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集     */    private final Set TABLE_NAMES = new HashSet();    @Override    public Set getTableNames() {        return TABLE_NAMES;    }    @Override    public Expression getExpression(String tableName, Alias tableAlias) {        // 只有有登陆用户的情况下,才进行数据权限的处理        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();        if (loginUser == null) {            return null;        }        // 只有管理员范例的用户,才进行数据权限的处理        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {            return null;        }        // 得到数据权限        DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);        // 从上下文中拿不到,则调用逻辑进行获取        if (deptDataPermission == null) {            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());            if (deptDataPermission == null) {                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",                        loginUser.getId(), tableName, tableAlias.getName()));            }            // 添加到上下文中,避免重复计算            loginUser.setContext(CONTEXT_KEY, deptDataPermission);        }        // 情况一,假如是 ALL 可查看全部,则无需拼接条件        if (deptDataPermission.getAll()) {            return null;        }        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限        if (CollUtil.isEmpty(deptDataPermission.getDeptIds())            && Boolean.FALSE.equals(deptDataPermission.getSelf())) {            return new EqualsTo(null, null); // WHERE null = null,可以包管返回的数据为空        }        // 情况三,拼接 Dept 和 User 的条件,末了组合        Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());        Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());        if (deptExpression == null && userExpression == null) {            // TODO 芋艿:得到不到条件的时间,暂时不抛出异常,而是不返回数据            log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",//                    loginUser.getId(), tableName, tableAlias.getName()));            return EXPRESSION_NULL;        }        if (deptExpression == null) {            return userExpression;        }        if (userExpression == null) {            return deptExpression;        }        // 现在,假如有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)        return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression));    }    private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) {        // 假如不存在配置,则无需作为条件        String columnName = deptColumns.get(tableName);        if (StrUtil.isEmpty(columnName)) {            return null;        }        // 假如为空,则无条件        if (CollUtil.isEmpty(deptIds)) {            return null;        }        // 拼接条件        return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),                // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号                new ParenthesedExpressionList(new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))));    }    private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {        // 假如不查看自己,则无需作为条件        if (Boolean.FALSE.equals(self)) {            return null;        }        String columnName = userColumns.get(tableName);        if (StrUtil.isEmpty(columnName)) {            return null;        }        // 拼接条件        return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));    }    // ==================== 添加配置 ====================    public void addDeptColumn(Class




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4