事件起因:一边开发一边完善需求,突然要求为每个数据的查询新增权限。需求新增,账号分为市、区、街、园账号。每次查询都只能查询自己范围以下的数据。
情况分析:已经开发的所有代码,都需要逐个排查,分析是否需要增加查询、或者删修的条件。
解决方案:
1、利用Spring的web层拦截器拿到该账号具体对应什么权限。ThreadLocal保存数据。
2、Mybatis插件拦截一部分需要修改的Sql语句,拦截之后按某种规则修改。
3、把部分信息通过Sql注释的方式写在Mybatis的sqlXML文件中。保证sql在能正常执行的前提下,比较优雅的完成修改。
1、Mybatis插件+配置
package com.xx.xx.xx.xx.xx;
import com.xx.xx.xx.dto.BaseDataRangeDTO;
import com.xx.xx.xx.utils.BizInfoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanMap;
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.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@Slf4j
@Intercepts({
// @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
// @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })
@Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
public class BaseDataRangeInterceptor implements Interceptor {
private static final String INTERCEPTOR_PREFIX = "withAuthority";
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
String[] sqlids = mappedStatement.getId().split("\\.");
String sqlid = sqlids[sqlids.length - 1];
BaseDataRangeDTO dataRange = BizInfoUtils.getCurrentDataRange();
log.info("[BaseDataRangeInterceptor] sqlid -> {} dataRange -> {}", sqlid, dataRange);
if (sqlid.startsWith(INTERCEPTOR_PREFIX) && dataRange != null) {
BoundSql boundSql = statementHandler.getBoundSql();
List<ParameterMapping> list = boundSql.getParameterMappings();
String sql = boundSql.getSql();
Map<?, ?> map = new BeanMap(boundSql.getParameterObject());
log.info("[BaseDataRangeInterceptor] ParameterMap -> {}", map);
String endsql = "";
switch (dataRange.getLevelRange()) {
case 1:
endsql = "city_code = " + dataRange.getCityCode();
break;
case 2:
endsql = "district_code = " + dataRange.getDistrictCode();
break;
case 3:
endsql = "department_code = " + dataRange.getDepartmentCode();
break;
case 4:
endsql = "street_code = " + dataRange.getStreetCode();
break;
case 5:
endsql = "kindergarten_code = " + dataRange.getKindergartenCode();
break;
default:
}
//关键点,拆开注释写入权限sql
String mSql = sql.replaceAll("/\\*(.*)\\*/", "$1" + endsql);
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, mSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
package com.xx.xx.xx.xx.xx.xx.config;
import com.xx.xx.xx.xx.BaseDataRangeInterceptor;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class InterceptorConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(org.apache.ibatis.session.Configuration configuration) {
configuration.setMapUnderscoreToCamelCase(true);
configuration.addInterceptor(new BaseDataRangeInterceptor());
}
};
}
}
2、Web拦截器设置权限参数
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanMap;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Configuration
@Slf4j
public class WebControllerAspect {
//计时
ThreadLocal<Long> localTimer = new ThreadLocal<>();
@Pointcut("execution(* com.pingan.pcloud.lg.info.controller.web.*.*Controller.*(..))")
public void weblog() {
}
@Around("weblog()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String url = request.getRequestURL().toString();
String method = request.getMethod();
String queryString = request.getQueryString();
localTimer.set(System.currentTimeMillis());
//false data
BizInfoUtils.setCurrentDataRange(new BaseDataRangeDTO(1, 0, 1, 1, 0, 0, 0, 0));
log.info("<WebControllerAspect> [{}] -> method -> [{}] -> with params [{}]", url, method, queryString);
Object result = pjp.proceed();
log.info("<WebControllerAspect> [{}] -> time -> [{}] -> return data [{}]", url, System.currentTimeMillis() - localTimer.get(), new BeanMap(result));
return result;
}
}
3、sqlXML里的注释巧妙使用
<select id="withAuthorityGetOrganizationTree" parameterType="int" resultMap="organizationResp">
select * from xxxxx o WHERE o.is_del = 0 /*and o.*/ AND o.parent_id = #{id}
</select>