數據權限管理——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的值沒有權限查看。

 

應用流程圖.png
應用流程圖

 

images/PyDJiBwAKCB8a7s3xJmzxifFJRYWRGez.png
應用鏈路邏輯圖

 

技術實現

MyBatis攔截器

在編寫MyBatis攔截器之前,先了解MyBatis的攔截目標方法:

  1. Executor(updaqte,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的handResultSets方法作爲Sql執行之後的結果攔截過濾。

SQL執行前
Preparameterceptor.java:
/**
 * 
    * @ClassName: Preparamrterceptor.java
    * @Description: MyBatis數據攔截器
    * @author 龍隊今天贏了嗎
    * @date 2018年8月20日 下午2:27:14
    *
 */
@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();
    }
/**
 * 
    * @Description: 權限SQL包裝
    * @author 龍隊今天贏了嗎
    * @date 2018年8月20日 下午2:27:14
    *
 */
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
/**
 * 
    * @ClassName: ResultInterceptor.java
    * @Description: MyBatis數據權限攔截器 - handleResultSets 對結果進行過濾
    * @author 龍隊今天贏了嗎
    * @date 2018年8月20日 下午2:27:14
    *
 */
@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層註解內容:不同方法的攔截器獲取方法稍微有所區別,具體如上(PrepareInterceptor.java與ResultInterceptor.java)。
  2. 如何獲取登錄人標識:不同框架不同項目獲取當前登錄人的方法不盡相同,可以通過配置的方法動態將獲取當前登錄人的方法傳遞給權限中心。配置文件添加:
    # 客戶端獲取當前登錄人標識
    permission.client.userid.method=com.raising.sc.permission.example.util.UserUtils.getUserId
    然後利用Java反射機制,觸發getUserId()方法。
  3. 如何傳遞動態參數:比如用戶A只能查詢自己單位以及下屬單位的所有數據;配置的where部分的sql如下:
    org_cd like concat(${orgCd},'%')
    然後通過PrepareInterceptor.java讀取以上sql,並且通過數據庫或者配置文件中設置的參數(orgCd)相關聯的方法(類似獲取當前登錄人標識的方法),提前在權限參數(orgCd)配置好對用的方法路徑、數值類型、返回類型等。配置文件或者數據庫獲取到orgCd對用的方法路徑:
    com.raising.sc.permission.example.util.UserUtils.getRegionCdByUserId
    這樣就能簡單傳遞動態參數了。
  4. 需要考慮到與sql分頁的優先級。

從產品來說,此模塊應該有以下幾個部分組成:

  • foruo-permission-admin數據權限管理平臺
  • foruo-permission-server數據權限服務端(提供權限相關接口)
  • foruo-permission-client數據權限客戶端(封裝API)
  • 應用鏈接邏輯圖的其他部分。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章