天天看點

mysql 過濾分頁_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.*;

@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;

public class PermissionConfig {

private static Logger logger = LoggerFactory.getLogger(PropertiesLoader.class);

private static Map 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;

public class PermissionUtils {

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進行查詢;

執行請求方法,擷取攔截後的分頁結果;

執行流程圖:

mysql 過濾分頁_Mybatis攔截器之資料權限過濾與分頁內建

攔截器源碼:

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;

@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 pageThreadLocal = new ThreadLocal();

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();

//擷取需要進行攔截的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("資料權限放行...");

}

}

}

}

}

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) {

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;

}

/**

*

* 把真正的參數對象解析出來

* Spring會自動封裝對個參數對象為Map對象

* 對于通過@Param指定key值參數我們不做處理,因為XML檔案需要該KEY值

* 而對于沒有@Param指定時,Spring會使用0,1作為主鍵

* 對于沒有@Param指定名稱的參數,一般XML檔案會直接對真正的參數對象解析,

* 此時解析出真正的參數作為根對象

*

* @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);

}

}

}

/**

*

* 生成分頁SQL

*

*

* @param page

* @param sql

* @return

*/

protected String buildPageSql(Page> page, String sql) {

if (MYSQL.equalsIgnoreCase(databaseType)) {