解決方案之改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進行查詢;
執行請求方法,擷取攔截後的分頁結果;
執行流程圖:

攔截器源碼:
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)) {