數據權限管理中心 - 基於mybatis攔截器實現


數據權限管理中心

由於公司大部分項目都是使用mybatis,也是使用mybatis的攔截器進行分頁處理,所以技術上也直接選擇從攔截器入手

需求場景

第一種場景:行級數據處理

原sql:

select id,username,region from sys_user ;

需要封裝成:

select * from (
    select id,username,region from sys_user 
) where 1=1 and region like “3210%";

解釋

用戶只能查詢當前所屬市以及下屬地市數據 其中 like 部分也可以為動態參數(下面會講到)

此場景還有以下情況:

# 判斷
select * from (select id,username,region from sys_user ) where 1=1 and region != 320101;
# 枚舉
select * from (select id,username,region from sys_user ) where 1=1 and region in (320101,320102,320103);
...

第二種場景:列級數據處理

原sql:

select id,username,region from sys_user ;
  • 用戶A可以看到 id,username,region
  • 用戶B只能查看 id,username 的值,region的值沒有權限查看。

應用流程圖

輸入圖片說明

應用鏈路邏輯圖

輸入圖片說明

技術實現

mybatis攔截器

在編寫mybatis的攔截器之前,我們先來了解下mybaits的攔截目標方法

  • 1、Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • 2、ParameterHandler (getParameterObject, setParameters)

  • 3、StatementHandler (prepare, parameterize, batch, update, query)

  • 4、ResultSetHandler (handleResultSets, handleOutputParameters)

這里選擇StatementHandler 的 prepare 方法作為sql執行之前的攔截進行sql封裝,使用ResultSetHandler 的 handleResultSets 方法作為sql執行之后的結果攔截過濾。

sql執行前

PrepareInterceptor.java

/**
 * mybatis數據權限攔截器 - prepare
 * @author GaoYuan
 * @date 2018/4/17 上午9:52
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class })
})
@Component
public class PrepareInterceptor implements Interceptor {
    /** 日志 */
    private static final Logger log = LoggerFactory.getLogger(PrepareInterceptor.class);

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if(log.isInfoEnabled()){
            log.info("進入 PrepareInterceptor 攔截器...");
        }
        if(invocation.getTarget() instanceof RoutingStatementHandler) {
            RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
            StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
            //通過反射獲取delegate父類BaseStatementHandler的mappedStatement屬性
            MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
            //千萬不能用下面注釋的這個方法,會造成對象丟失,以致轉換失敗
            //MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);
            if(permissionAop == null){
                if(log.isInfoEnabled()){
                    log.info("數據權限放行...");
                }
                return invocation.proceed();
            }
            if(log.isInfoEnabled()){
                log.info("數據權限處理【拼接SQL】...");
            }
            BoundSql boundSql = delegate.getBoundSql();
            ReflectUtil.setFieldValue(boundSql, "sql", permissionSql(boundSql.getSql()));
        }
        return invocation.proceed();
    }

    /**
     * 權限sql包裝
     * @author GaoYuan
     * @date 2018/4/17 上午9:51
     */
    protected String permissionSql(String sql) {
        StringBuilder sbSql = new StringBuilder(sql);
        String userMethodPath = PermissionConfig.getConfig("permission.client.userid.method");
        //當前登錄人
        String userId = (String)ReflectUtil.reflectByPath(userMethodPath);
        //如果用戶為 1 則只能查詢第一條
        if("1".equals(userId)){
            //sbSql = sbSql.append(" limit 1 ");
            //如果有動態參數 regionCd
            if(true){
                String premission_param = "regionCd";
                //select * from (select id,name,region_cd from sys_exam ) where region_cd like '${}%'
                String methodPath = PermissionConfig.getConfig("permission.client.params." + premission_param);
                String regionCd = (String)ReflectUtil.reflectByPath(methodPath);
                sbSql = new StringBuilder("select * from (").append(sbSql).append(" ) s where s.regionCd like concat("+ regionCd +",'%')  ");
            }

        }
        return sbSql.toString();
    }
}

sql執行后

ResultInterceptor.java

/**
 * mybatis數據權限攔截器 - handleResultSets
 * 對結果集進行過濾
 * @author GaoYuan
 * @date 2018/4/17 上午9:52
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
@Component
public class ResultInterceptor implements Interceptor {
    /** 日志 */
    private static final Logger log = LoggerFactory.getLogger(ResultInterceptor.class);

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if(log.isInfoEnabled()){
            log.info("進入 ResultInterceptor 攔截器...");
        }
        ResultSetHandler resultSetHandler1 = (ResultSetHandler) invocation.getTarget();
        //通過java反射獲得mappedStatement屬性值
        //可以獲得mybatis里的resultype
        MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(resultSetHandler1, "mappedStatement");
        //獲取切面對象
        PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);

        //執行請求方法,並將所得結果保存到result中
        Object result = invocation.proceed();
        if(permissionAop != null) {
            if (result instanceof ArrayList) {
                ArrayList resultList = (ArrayList) result;
                for (int i = 0; i < resultList.size(); i++) {
                    Object oi = resultList.get(i);
                    Class c = oi.getClass();
                    Class[] types = {String.class};
                    Method method = c.getMethod("setRegionCd", types);
                    // 調用obj對象的 method 方法
                    method.invoke(oi, "");
                    if(log.isInfoEnabled()){
                        log.info("數據權限處理【過濾結果】...");
                    }
                }
            }
        }
        return result;
    }
}

其中 PermissionAop 為 dao 層自定義切面,用於開關控制是否啟用數據權限過濾。

難點

  1. 如何在攔截器獲取dao層注解內容;
  2. 如何獲取當前登錄人標識;
  3. 如何傳遞動態參數;
  4. 需要考慮到與sql分頁的優先級。

解答

攔截器獲取dao層注解

不同方法的攔截器獲取方法稍微有所區別,具體在上面的 PrepareInterceptor.java 與 ResultInterceptor.java 代碼中自行查看。

獲取當前登錄人標識

由於不同框架或者不同項目,獲取當天登錄人的方法可能不一樣,那么就只能通過配置的方式動態將獲取當前登錄人的方法傳遞給權限中心。 配置文件中添加:

# 客戶端獲取當前登錄人標識
permission.client.userid.method=com.raising.sc.permission.example.util.UserUtils.getUserId

然后利用Java反射機制,觸發getUserId( )方法。

傳遞動態參數

比如用戶A只能查詢自己單位以及下屬單位的所有數據; 配置中心配置的where部分的sql如下:

org_cd like concat(${orgCd},'%')

然后通過PrepareInterceptor.java讀取到以上sql,並且通過數據庫或者配置文件中設置的參數【orgCd】相關聯的方法(類似獲取當前登錄人標識的方式),提前在權限參數(orgCd)配置好對應的方法路徑、參數值類型、返回值類型等。

配置文件或者數據庫獲取到 orgCd 對應的方法路徑:

com.raising.sc.permission.example.util.UserUtils.getRegionCdByUserId

當然,現在這樣只是簡單的動態參數,其余的還需要后續的開發,這里只是最簡單的嘗試。

拓展

從產品的角度來說,此模塊需要有三個部分組成:

1、foruo-permission-admin 數據權限管理平台 2、foruo-permission-server 數據權限服務端(提供權限相關接口) 3、foruo-permission-client 數據權限客戶端(封裝API)

在結合 應用鏈路邏輯圖 即可完成此模塊內容。

涉及知識點:

  • Mybatis攔截器
  • Java反射機制

項目源碼

碼雲:https://gitee.com/gmarshal/foruo-sc-permission


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM