1. 程式人生 > >Mybatis攔截器之資料許可權過濾與分頁整合

Mybatis攔截器之資料許可權過濾與分頁整合

解決方案之改SQL

原sql

SELECT
	a.id AS "id",
	a.NAME AS "name", a.sex_cd AS "sexCd", a.org_id AS "orgId", a.STATUS AS "status", a.create_org_id AS "createOrgId" FROM pty_person a WHERE a. STATUS = 0

org_id是單位的標識,也就是where條件裡再加個單位標識的過濾。

改後sql

SELECT
	a.id AS "id",
	a.NAME AS "name", a.sex_cd AS "sexCd", a.org_id AS "orgId", a.STATUS AS "status", a.create_org_id AS "createOrgId" FROM pty_person a WHERE a. STATUS = 0 and a.org_id LIKE concat(710701070102, '%')

當然通過這個辦法也可以實現資料的過濾,但這樣的話相比大家也都有同感,那就是每個業務模組 每個人都要進行SQL改動,這次是根據單位過濾、明天又再根據其他的屬性過濾,意味著要不停的改來改去,可謂是場面壯觀也,而且這種集體改造耗費了時間精力不說,還會有很多不確定因素,比如SQL寫錯,存在漏網之魚等等。因此這個解決方案肯定是直接PASS掉咯;

解決方案之攔截器

由於專案大部分採用的持久層框架是Mybatis,也是使用的Mybatis進行分頁攔截處理,因此直接採用了Mybatis攔截器實現資料許可權過濾。

1、自定義資料許可權過濾註解 PermissionAop,負責過濾的開關 

package com.raising.framework.annotation;

import java.lang.annotation.*;

/**
 * 資料許可權過濾自定義註解
 * @author lihaoshan
 * @date 2018-07-19 * */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PermissionAop { String value() default ""; } 

2、定義全域性配置 PermissionConfig 類載入 許可權過濾配置檔案

package com.raising.framework.config;

import com.raising.utils.PropertiesLoader;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; /** * 全域性配置 * 對應 permission.properties * @author lihaoshan */ public class PermissionConfig { private static Logger logger = LoggerFactory.getLogger(PropertiesLoader.class); /** * 儲存全域性屬性值 */ private static Map<String, String> map = new HashMap<>(16); /** * 屬性檔案載入物件 */ private static PropertiesLoader loader = new PropertiesLoader( "permission.properties"); /** * 獲取配置 */ public static String getConfig(String key) { if(loader == null){ logger.info("缺失配置檔案 - permission.properties"); return null; } String value = map.get(key); if (value == null) { value = loader.getProperty(key); map.put(key, value != null ? value : StringUtils.EMPTY); } return value; } }

3、建立許可權過濾的配置檔案 permission.properties,用於配置需要攔截的DAO的 namespace

(由於註解@PermissionAop是加在DAO層某個介面上的,而我們分頁介面為封裝的公共BaseDAO,所以如果僅僅使用註解方式開關攔截的話,會影響到所有的業務模組,因此需要結合額外的配置檔案)

# 需要進行攔截的SQL所屬namespace
permission.intercept.namespace=com.raising.modules.pty.dao.PtyGroupDao,com.raising.modules.pty.dao.PtyPersonDao

4、自定義許可權工具類

根據 StatementHandler 獲取Permission註解物件:

package com.raising.utils.permission;

import com.raising.framework.annotation.PermissionAop;
import org.apache.ibatis.mapping.MappedStatement;

import java.lang.reflect.Method;

/** * 自定義許可權相關工具類 * @author lihaoshan * @date 2018-07-20 * */ public class PermissionUtils { /** * 根據 StatementHandler 獲取 註解物件 * @author lihaoshan * @date 2018-07-20 */ public static PermissionAop getPermissionByDelegate(MappedStatement mappedStatement){ PermissionAop permissionAop = null; try { String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); String methodName = id.substring(id.lastIndexOf(".") + 1, id.length()); final Class cls = Class.forName(className); final Method[] method = cls.getMethods(); for (Method me : method) { if (me.getName().equals(methodName) && me.isAnnotationPresent(PermissionAop.class)) { permissionAop = me.getAnnotation(PermissionAop.class); } } }catch (Exception e){ e.printStackTrace(); } return permissionAop; } } 

5、建立分頁攔截器 MybatisSpringPageInterceptor 或進行改造(本文是在Mybatis分頁攔截器基礎上進行的資料許可權攔截改造,SQL包裝一定要在執行分頁之前,也就是獲取到原始SQL後就進行資料過濾包裝) 

首先看資料許可權攔截核心程式碼:

  • 獲取需要進行攔截的DAO層namespace拼接串;
  • 獲取當前mapped所屬namespace;
  • 判斷配置檔案中的namespace是否包含當前的mapped所屬的namespace,如果包含則繼續,否則直接放行;
  • 獲取資料許可權註解物件,及註解的值;
  • 判斷註解值是否為DATA_PERMISSION_INTERCEPT,是則攔截、並進行過濾SQL包裝,否則放行;
  • 根據包裝後的SQL查分頁總數,不能使用原始SQL進行查詢;
  • 執行請求方法,獲取攔截後的分頁結果;

執行流程圖:

攔截器原始碼:

package com.raising.framework.interceptor;

import com.raising.StaticParam;
import com.raising.framework.annotation.PermissionAop;
import com.raising.framework.config.PermissionConfig;
import com.raising.modules.sys.entity.User; import com.raising.utils.JStringUtils; import com.raising.utils.UserUtils; import com.raising.utils.permission.PermissionUtils; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.*; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Properties; /** * 分頁攔截器 * @author GaoYuan * @author lihaoshan 增加了資料許可權的攔截過濾 * @datetime 2017/12/1 下午5:43 */ @Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }), @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) }) public class MybatisSpringPageInterceptor implements Interceptor { private static final Logger log = LoggerFactory.getLogger(MybatisSpringPageInterceptor.class); public static final String MYSQL = "mysql"; public static final String ORACLE = "oracle"; /**資料庫型別,不同的資料庫有不同的分頁方法*/ protected String databaseType; @SuppressWarnings("rawtypes") protected ThreadLocal<Page> pageThreadLocal = new ThreadLocal<Page>(); public String getDatabaseType() { return databaseType; } public void setDatabaseType(String databaseType) { if (!databaseType.equalsIgnoreCase(MYSQL) && !databaseType.equalsIgnoreCase(ORACLE)) { throw new PageNotSupportException("Page not support for the type of database, database type [" + databaseType + "]"); } this.databaseType = databaseType; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { String databaseType = properties.getProperty("databaseType"); if (databaseType != null) { setDatabaseType(databaseType); } } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public Object intercept(Invocation invocation) throws Throwable { // 控制SQL和查詢總數的地方 if (invocation.getTarget() instanceof StatementHandler) { Page page = pageThreadLocal.get(); //不是分頁查詢 if (page == null) { return invocation.proceed(); } RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget(); StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate"); BoundSql boundSql = delegate.getBoundSql(); Connection connection = (Connection) invocation.getArgs()[0]; // 準備資料庫型別 prepareAndCheckDatabaseType(connection); MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement"); String sql = boundSql.getSql(); /** 單位資料許可權攔截 begin */ //獲取需要進行攔截的DAO層namespace拼接串 String interceptNamespace = PermissionConfig.getConfig("permission.intercept.namespace"); //獲取當前mapped的namespace String mappedStatementId = mappedStatement.getId(); String className = mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")); if(JStringUtils.isNotBlank(interceptNamespace)){ //判斷配置檔案中的namespace是否與當前的mapped namespace匹配,如果包含則進行攔截,否則放行 if(interceptNamespace.contains(className)){ //獲取資料許可權註解物件 PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement); if (permissionAop != null){ //獲取註解的值 String permissionAopValue = permissionAop.value(); //判斷註解是否開啟攔截 if(StaticParam.DATA_PERMISSION_INTERCEPT.equals(permissionAopValue) ){ if(log.isInfoEnabled()){ log.info("資料許可權攔截【拼接SQL】..."); } //返回攔截包裝後的sql sql = permissionSql(sql); ReflectUtil.setFieldValue(boundSql, "sql", sql); } else { if(log.isInfoEnabled()){ log.info("資料許可權放行..."); } } } } } /** 單位資料許可權攔截 end */ if (page.getTotalPage() > -1) { if (log.isTraceEnabled()) { log.trace("已經設定了總頁數, 不需要再查詢總數."); } } else { Object parameterObj = boundSql.getParameterObject(); /// MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement"); queryTotalRecord(page, parameterObj, mappedStatement, sql,connection); } String pageSql = buildPageSql(page, sql); if (log.isDebugEnabled()) { log.debug("分頁時, 生成分頁pageSql......"); } ReflectUtil.setFieldValue(boundSql, "sql", pageSql); return invocation.proceed(); } else { // 查詢結果的地方 // 獲取是否有分頁Page物件 Page<?> page = findPageObject(invocation.getArgs()[1]); if (page == null) { if (log.isTraceEnabled()) { log.trace("沒有Page物件作為引數, 不是分頁查詢."); } return invocation.proceed(); } else { if (log.isTraceEnabled()) { log.trace("檢測到分頁Page物件, 使用分頁查詢."); } } //設定真正的parameterObj invocation.getArgs()[1] = extractRealParameterObject(invocation.getArgs()[1]); pageThreadLocal.set(page); try { // Executor.query(..) Object resultObj = invocation.proceed(); if (resultObj instanceof List) { /* @SuppressWarnings({ "unchecked", "rawtypes" }) */ page.setResults((List) resultObj); } return resultObj; } finally { pageThreadLocal.remove(); } } } protected Page<?> findPageObject(Object parameterObj) { if (parameterObj instanceof Page<?>) { return (Page<?>) parameterObj; } else if (parameterObj instanceof Map) { for (Object val : ((Map<?, ?>) parameterObj).values()) { if (val instanceof Page<?>) { return (Page<?>) val; } } } return null; } /** * <pre> * 把真正的引數物件解析出來 * Spring會自動封裝對個引數物件為Map<String, Object>物件 * 對於通過@Param指定key值引數我們不做處理,因為XML檔案需要該KEY值 * 而對於沒有@Param指定時,Spring會使用0,1作為主鍵 * 對於沒有@Param指定名稱的引數,一般XML檔案會直接對真正的引數物件解析, * 此時解析出真正的引數作為根物件 * </pre> * @param parameterObj * @return */ protected Object extractRealParameterObject(Object parameterObj) { if (parameterObj instanceof Map<?, ?>) { Map<?, ?> parameterMap = (Map<?, ?>) parameterObj; if (parameterMap.size() == 2) { boolean springMapWithNoParamName = true; for (Object key : parameterMap.keySet()) { if (!(key instanceof String)) { springMapWithNoParamName = false; break; } String keyStr = (String) key; if (!"0".equals(keyStr) && !"1".equals(keyStr)) { springMapWithNoParamName = false; break; } } if (springMapWithNoParamName) { for (Object value : parameterMap.values()) { if (!(value instanceof Page<?>)) { return value; } } } } } return parameterObj; } protected void prepareAndCheckDatabaseType(Connection connection) throws SQLException { if (databaseType == null) { String productName = connection.getMetaData().getDatabaseProductName(); if (log.isTraceEnabled()) { log.trace("Database productName: " + productName); } productName = productName.toLowerCase(); if (productName.indexOf(MYSQL) != -1) { databaseType = MYSQL; } else if (productName.indexOf(ORACLE) != -1) { databaseType = ORACLE; } else { throw new PageNotSupportException("Page not support for the type of database, database product name [" + productName + "]"); } if (log.isInfoEnabled()) { log.info("自動檢測到的資料庫型別為: " + databaseType); } } } /** * <pre> * 生成分頁SQL * </pre> * * @param page * @param sql * @return */ protected String buildPageSql(Page<?> page, String sql) { if (MYSQL.equalsIgnoreCase(databaseType)) { return buildMysqlPageSql(page, sql); } else if (ORACLE.equalsIgnoreCase(databaseType)) { return buildOraclePageSql(page, sql); } return sql; } /** * <pre> * 生成Mysql分頁查詢SQL * </pre> * * @param page * @param sql * @return */ protected String buildMysqlPageSql(Page<?> page, String sql) { // 計算第一條記錄的位置,Mysql中記錄的位置是從0開始的。 int offset = (page.getPageNo() - 1) * page.getPageSize(); if(offset<0){ return " limit 0 "; } return new StringBuilder(sql).append(" limit ").append(offset).append(",").append(page.getPageSize()).toString(); } /** * <pre> * 生成Oracle分頁查詢SQL * </pre> * * @param page * @param sql * @return */ protected String buildOraclePageSql(Page<?> page, String sql) { // 計算第一條記錄的位置,Oracle分頁是通過rownum進行的,而rownum是從1開始的 int offset = (page.getPageNo() - 1) * page.getPageSize() + 1; StringBuilder sb = new StringBuilder(sql); sb.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize()); sb.insert(0, "select * from (").append(") where r >= ").append(offset); return sb.toString(); } /** * <pre> * 查詢總數 * </pre> * * @param page * @param parameterObject * @param mappedStatement * @param sql * @param connection * @throws SQLException */ protected void queryTotalRecord(Page<?> page, Object parameterObject, MappedStatement mappedStatement, String sql, Connection connection) throws SQLException { BoundSql boundSql = mappedStatement.getBoundSql(page); /// String sql = boundSql.getSql(); String countSql = this.buildCountSql(sql); if (log.isDebugEnabled()) { log.debug("分頁時, 生成countSql......"); } List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, parameterObject); ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBoundSql); PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = connection.prepareStatement(countSql); parameterHandler.setParameters(pstmt); rs = pstmt.executeQuery(); if (rs.next()) { long totalRecord = rs.getLong(1); page.setTotalRecord(totalRecord); } } finally { if (rs != null) { try { rs.close(); } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("關閉ResultSet時異常.", e); } } } if (pstmt != null) { try { pstmt.close(); } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("關閉PreparedStatement時異常.", e); } } } } } /** * 根據原Sql語句獲取對應的查詢總記錄數的Sql語句 * * @param sql * @return */ protected String buildCountSql(String sql) { //查出第一個from,先轉成小寫 sql = sql.toLowerCase(); int index = sql.indexOf("from"); return "select count(0) " + sql.substring(index); } /** * 利用反射進行操作的一個工具類 * */ private static class ReflectUtil { /** * 利用反射獲取指定物件的指定屬性 * * @param obj 目標物件 * @param fieldName 目標屬性 * @return 目標屬性的值 */ public static Object getFieldValue(Object obj, String fieldName) { Object result = null; Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { field.setAccessible(true); try { result = field.get(obj); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return result; } /** * 利用反射獲取指定物件裡面的指定屬性 * * @param obj 目標物件 * @param fieldName 目標屬性 * @return 目標欄位 */ private static Field getField(Object obj, String fieldName) { Field field = null; for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) { try { field = clazz.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { // 這裡不用做處理,子類沒有該欄位可能對應的父類有,都沒有就返回null。 } } return field; } /** * 利用反射設定指定物件的指定屬性為指定的值 * * @param obj 目標物件 * @param fieldName 目標屬性 * @param fieldValue 目標值 */ public static void setFieldValue(Object obj, String fieldName, String fieldValue) { Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { try { field.setAccessible(true); field.set(obj, fieldValue); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public static class PageNotSupportException extends RuntimeException { /** serialVersionUID*/ private static final long serialVersionUID = 1L; public PageNotSupportException() { super(); } public PageNotSupportException(String message, Throwable cause) { super(message, cause); } public PageNotSupportException(String message) { super(message); } public PageNotSupportException(Throwable cause) { super(cause); } } /** * 資料許可權sql包裝【只能檢視本級單位及下屬單位的資料】 * @author lihaoshan * @date 2018-07-19 */ protected String permissionSql(String sql) { StringBuilder sbSql = new StringBuilder(sql); //獲取當前登入人 User user = UserUtils.getLoginUser(); String orgId =null; if (user != null) { //獲取當前登入人所屬單位標識 orgId = user.getOrganizationId(); } //如果有動態引數 orgId if(orgId != null){ sbSql = new StringBuilder("select * from (") .append(sbSql) .append(" ) s ") .append(" where s.createOrgId like concat("+ orgId +",'%') "); } return sbSql.toString(); } }