事件起因:一邊開發一邊完善需求,突然要求爲每個數據的查詢新增權限。需求新增,賬號分爲市、區、街、園賬號。每次查詢都只能查詢自己範圍以下的數據。
情況分析:已經開發的所有代碼,都需要逐個排查,分析是否需要增加查詢、或者刪修的條件。
解決方案:
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:
}
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>