天天看点

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