切換語言為:簡體

使用mybatis切片實現資料許可權控制

  • 爱糖宝
  • 2024-07-08
  • 2067
  • 0
  • 0

一、使用方式

資料許可權控制需要對查詢出的資料進行篩選,對業務入侵最少的方式就是利用mybatis或者資料庫連線池的切片對已有業務的sql進行修改。切片邏輯完成後,僅需要在業務中加入少量標記程式碼,就可以實現對資料許可權的控制。這種修改方式,對老業務的邏輯沒有入侵或只有少量入侵,基本不影響老業務的邏輯和可讀性;對新業務,業務開發人員無需過多關注許可權問題,可以集中精力處理業務邏輯。

由於部門程式碼中使用的資料庫連線池種類較多,不利於切片控制邏輯的快速完成,而sql拼接的部分基本只有mybatis和java字串直接拼接兩種方式,因此使用mybatis切片的方式來完成資料許可權控制邏輯。在mybatis的mapper檔案的介面上新增註解,註解中寫明需要控制的許可權種類、要控制的表名、列名即可控制介面的資料許可權。

使用mybatis切片實現資料許可權控制

由於mybatis的mapper檔案中的同一介面在多個地方被呼叫,有的需要控制資料許可權,有的不需要,因此增加一種許可權控制方式:透過ThreadLocal傳遞許可權控制規則來控制當前sql執行時控制資料許可權。

使用mybatis切片實現資料許可權控制

許可權控制規則格式如下:

限權規則code1(表名1.欄位名1,表名2.欄位名2);限權規則code2(表名3.欄位名3,表名4.欄位名4)

例如:enterprise(channel.enterprise_code);account(table.column);channel(table3.id)

上下文傳遞工具類如下所示,使用回撥的方式傳遞ThreadLocal可以防止使用者忘記清除上下文。

public class DataAuthContextUtil {
    /**
     * 不方便使用註解的地方,可以直接使用上下文設定資料規則
     */
    private static ThreadLocal<String> useDataAuth = new ThreadLocal<>();

    /**
     * 有的sql只在部分情況下需要使用資料許可權限制

     * 上下文和註解中均可設定資料許可權規則,都設定時,上下文中的優先
     *
     * @param supplier
     */
    public static <T> T executeSqlWithDataAuthRule(String rule, Supplier<T> supplier) {
        try {
            useDataAuth.set(rule);
            return supplier.get();
        } finally {
            useDataAuth.remove();
        }
    }

    /**
     * 獲取資料許可權標誌
     *
     * @return
     */
    public static String getUseDataAuthRule() {
        return useDataAuth.get();
    }
}

二、切片實現流程



使用mybatis切片實現資料許可權控制



三、其他技術細節

(1)在切面中獲取原始sql

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.util.function.Tuple2;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Component
@Intercepts({
//        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Slf4j
public class DataAuthInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
            String sql = boundSql.getSql();
        } catch (Exception e) {
            log.error("資料許可權新增出錯,當前sql未加資料許可權限制!", e);
            throw e;
        }
        return invocation.proceed();
    }
}

(2)將許可權項加入原始sql中

使用druid附帶的ast解析功能修改sql,程式碼如下

/**
     * 許可權限制寫入sql
     *
     * @param sql
     * @param tableAuthMap key:table value1:column value2:values許可權項
     * @return
     */
    public static StringBuilder addAuthLimitToSql(String sql, Map<String, Tuple2<String, Set<String>>> tableAuthMap) {
        List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, "mysql");
        StringBuilder authSql = new StringBuilder();
        for (SQLStatement stmt : stmtList) {
            stmt.accept(new MySqlASTVisitorAdapter() {
                @Override
                public boolean visit(MySqlSelectQueryBlock x) {
                    SQLTableSource from = x.getFrom();
                    Set<String> tableList = new HashSet<>();
                    getTableList(from, tableList);
                    for (String tableName : tableList) {
                        if (tableAuthMap.containsKey(tableName)) {
                            x.addCondition(tableName + "in (...略)");
                        }
                    }
                    return true;
                }
            });
            authSql.append(stmt);
        }
        return authSql;
    }
    
    private static void getTableList(SQLTableSource from, Set<String> tableList) {
        if (from instanceof SQLExprTableSource) {
            SQLExprTableSource tableSource = (SQLExprTableSource) from;
            String name = tableSource.getTableName().replace("`", "");
            tableList.add(name);
            String alias = tableSource.getAlias();
            if (StringUtils.isNotBlank(alias)) {
                tableList.add(alias.replace("`", ""));
            }
        } else if (from instanceof SQLJoinTableSource) {
            SQLJoinTableSource joinTableSource = (SQLJoinTableSource) from;
            getTableList(joinTableSource.getLeft(), tableList);
            getTableList(joinTableSource.getRight(), tableList);
        } else if (from instanceof SQLSubqueryTableSource) {
            SQLSubqueryTableSource tableSource = (SQLSubqueryTableSource) from;
            tableList.add(tableSource.getAlias().replace("`", ""));
        } else if (from instanceof SQLLateralViewTableSource) {
            log.warn("SQLLateralView不用處理");
        } else if (from instanceof SQLUnionQueryTableSource) {
            //union 不需要處理
            log.warn("union不用處理");
        } else if (from instanceof SQLUnnestTableSource) {
            log.warn("Unnest不用處理");
        } else if (from instanceof SQLValuesTableSource) {
            log.warn("Values不用處理");
        } else if (from instanceof SQLWithSubqueryClause) {
            log.warn("子查詢不用處理");
        } else if (from instanceof SQLTableSourceImpl) {
            log.warn("Impl不用處理");
        }
    }
}

(3)將修改過後的sql寫回mybatis

        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
        // 組裝 MappedStatement
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), new MySqlSource(boundSql), ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        MappedStatement newMappedStatement = builder.build();
        MetaObject metaObject = MetaObject.forObject(newMappedStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
        metaObject.setValue("sqlSource.boundSql.sql", newSql);
        invocation.getArgs()[0] = newMappedStatement;



參考文章: blog.csdn.net/e_anjing/ar…

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.