最近在做一个资源管理项目,其中有一个需求,需要对数据进行地域纬度管理。不同登录用户查询对应部分数据,但涉及业务表数据较多,单个sql重写,极其浪费效率,故考虑把关联权限的sql抽取出来,进行sql拦截重写实现权限自动关联。
第一步:数据准备,每个业务数据新增行政区域纬度字段:area_gov_code
第二步:基于mybatis-plus数据录入方案,数据录入时,对每条数据的area_gov_code进行自动录入:
2.1 定义一个实体类,包含字段areaGovCode。需要自动录入的该字段的表继承该类。
@Data
public class DefaultFieldInsertUpdate {
@TableField(value = "area_gov_code",fill = FieldFill.INSERT_UPDATE)
private String areaGovCode;
}
2.2 业务数据实体类继承统一字段实体类
@Data
@TableName("tra_know_info")
public class TraKnowInfo extends DefaultFieldInsertUpdate implements Serializable {
private String id;
private String knowName;
private String knowType;
private String knowTypeName;
private String remark;
private String content;
private String createUser;
private Date createDate;
private String enclosure;
private static final long serialVersionUID = 1L;
}
2.3 创建 MetaObjectHadler实现类
@Component
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
if(isExistField("areaGovCode",metaObject.getOriginalObject())) {
SysUser sysUser= WebSecurityUtil.loginUser();
this.setFieldValByName("areaGovCode", sysUser.getAreaGovCode().substring(0,6),metaObject);
}
}
@Override
public void updateFill(MetaObject metaObject) {
if(isExistField("areaGovCode",metaObject.getOriginalObject())) {
SysUser sysUser = WebSecurityUtil.loginUser();
this.setFieldValByName("areaGovCode", sysUser.getAreaGovCode().substring(0, 6), metaObject);
}
}
// 判断实体类是否有 areaGovCode 字段
public static Boolean isExistField(String field, Object obj) {
if (obj == null || StringUtils.isEmpty(field)) {
return null;
}
Object o = JSON.toJSON(obj);
JSONObject jsonObj = new JSONObject();
if (o instanceof JSONObject) {
jsonObj = (JSONObject) o;
}
return jsonObj.containsKey(field);
}
- 4 实例化handler
public class EmsApplication {
public static void main(String[] args) {
SpringApplication.run(EmsApplication.class, args);
}
/**
* 自动填充功能
* @return
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MybatisPlusMetaObjectHandler());
return globalConfig;
}
}
以上流程,实现了每个继承了DefaultFieldInsertUpdate的实体类,即可完成sql新增、编辑时自动
第三步: 通过StatementHandler 拦截对sql进行重写。
大概步骤:
1、定义一个 AuthFilter注解,用于是否重写sql的开关。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD , ElementType.TYPE})
@Documented
public @interface AuthFilter {
/*String userFiled() default "userId";
String orgFiled() default "orgId";
boolean ignoreUserFiled() default false;
boolean ignoreOrgFiled() default false;*/
}
2、通过 StatementHandler 拿到拦截 要执行的 dao方法。 判断dao方法是否被 AuthFilter注解(这里主要为了规避一下复杂的sql,或者类似登陆的sql不要进行重写)。 ;
3、如果拦截的dao,被 AuthFilter注解了就拿到 StatementHandler 的sql。 对sql进行重写。
4、sql重写:CCJSqlParserManager 进行sql解析。 拿到所有 select 的语句。 用PlainSelect 对select 的 where 语句进行重写
@Slf4j
@AllArgsConstructor
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Component
public class DataPermissionInterceptor implements Interceptor {
private static final Map<Class<?>, Map<String, List<List<Class>>>> mapperCache = new ConcurrentHashMap<Class<?>, Map<String, List<List<Class>>>>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 先判断是不是SELECT *** 作 不是直接过滤
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
return invocation.proceed();
}
//获取拦截的dao方法
String id = mappedStatement.getId();
//获取拦截的dao的类名称
String clazzName = id.substring(0, id.lastIndexOf('.'));
//获取拦截的dao的方法名称
String mapperMethod = id.substring(id.lastIndexOf('.') + 1);
//获取拦截dao的参数
Object[] paramArr = getParamArr(statementHandler.getBoundSql().getParameterObject());
Class<?> clazz = Class.forName(clazzName);
Method method1 = null;
try {
//实例化 dao的方法,如果实例失败说明不是我们写的dao,不需要重写sql
method1 = getMethod(clazz, mapperMethod, paramArr);
return invocation.proceed();
}catch (Exception e){
e.printStackTrace();
}
//判断方法时候实例,以及方法是否被AuthFilter.class注解。注解了就重写sql
if (method1!=null && method1.isAnnotationPresent(AuthFilter.class))
{
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
// 拿到执行的SQL语句
String originalSql = boundSql.getSql();
// SQL语句的参数
Object parameterObject = boundSql.getParameterObject();
// 这里对执行SQL进行自定义处理...
String finalSql = this.handleSql(originalSql);
System.err.println("数据权限处理过后的SQL: " + finalSql);
// 装载改写后的sql
metaObject.setValue("delegate.boundSql.sql", finalSql);
return invocation.proceed();
}
return invocation.proceed();
}
/** 获取 mapper 相应 Method 反射类 */
private Method getMethod(Class<?> clazz, String mapperMethod, Object[] paramArr) throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
// 1、查 mapper 接口缓存
if(!mapperCache.containsKey(clazz)) // mapper 没有缓存, 就进行缓存
{
cacheMapper(clazz);
}
// 2、返回相应 method
A:for(List<Class> paramList : mapperCache.get(clazz).get(mapperMethod)) {
if(!paramList.isEmpty()) {
for(int i = 0;i < paramArr.length;i ++) { // 比较参数列表class
if(paramArr[i] != null)
if(!compareClass(paramList.get(i), paramArr[i].getClass())) continue A;
}
return clazz.getMethod(mapperMethod, paramList.toArray(new Class[paramList.size()]));
}
}
return clazz.getMethod(mapperMethod); // 返回无参方法
}
/** 对 mapper 方法字段进行缓存 */
private void cacheMapper(Class<?> clazz) {
Map<String, List<List<Class>>> methodMap = new HashMap<String, List<List<Class>>>();
for(Method method : clazz.getMethods()) {
List<List<Class>> paramLists = methodMap.containsKey(method.getName()) ?
methodMap.get(method.getName()) : new ArrayList<List<Class>>();
List<Class> paramClass = new ArrayList<Class>();
for (Type type : method.getParameterTypes())
{
paramClass.add((Class) type);
}
paramLists.add(paramClass);
methodMap.put(method.getName(), paramLists);
}
mapperCache.put(clazz, methodMap);
}
/** class 比较 */
private boolean compareClass(Class<?> returnType, Class<?> paramType) throws NoSuchFieldException, IllegalAccessException {
if(returnType == paramType) {
return true;
}
else if(returnType.isAssignableFrom(paramType)) { // 判断 paramType 是否为 returnType 子类或者实现类
return true;
}
// 基本数据类型判断
else if(returnType.isPrimitive()) { // paramType为包装类
return returnType == paramType.getField("TYPE").get(null);
}
else if(paramType.isPrimitive()) { // returnType为包装类
return paramType == returnType.getField("TYPE").get(null);
}
return false;
}
/**
* 获取 mybatis 中 mapper 接口的参数列表的参数值
* @param parameter
* @return
*/
private Object[] getParamArr(Object parameter) {
Object[] paramArr = null;
// mapper 接口中使用的是 paramMap, 传多个参数
if(parameter instanceof MapperMethod.ParamMap)
{
Map map = ((Map) parameter);
if(!map.isEmpty()) {
StringBuilder builder = new StringBuilder();
// 初始化 param_arr
int size = map.size() >> 1;
paramArr = new Object[size];
for(int i = 1;i <= size;i ++)
{
// mapper 接口中使用 param0 ~ paramN 命名参数
paramArr[i - 1] = map.get(builder.append("param").append(i).toString());
builder.setLength(0);
}
}
}
else if(parameter != null)
{
paramArr = new Object[1];
paramArr[0] = parameter;
}
return paramArr;
}
/**
* 改写SQL
** @return 处理后的SQL
*/
private String handleSql(String originalSql) throws JSQLParserException {
CCJSqlParserManager parserManager = new CCJSqlParserManager();
Select select = (Select) parserManager.parse(new StringReader(originalSql));
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
this.setWhere((PlainSelect) selectBody);
} else if (selectBody instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s));
}
return select.toString();
}
/**
* 设置 where 条件 -- 使用CCJSqlParser将原SQL进行解析并改写
*
*/
@SneakyThrows(Exception.class)
protected void setWhere(PlainSelect plainSelect) {
Table fromItem = (Table) plainSelect.getFromItem();
// 有别名用别名,无别名用表名,防止字段冲突报错
Alias fromItemAlias = fromItem.getAlias();
String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();
// 构建子查询 -- 数据权限过滤SQL
SysUser sysUser= WebSecurityUtil.loginUser();
String dataPermissionSql = mainTableName + ".area_gov_code like concat("+sysUser.getAreaGovCode().substring(0,6).replaceAll("^(0+)", "").replaceAll("(0+)$", "")+",'%')) ";
if (plainSelect.getWhere() == null) {
plainSelect.setWhere(CCJSqlParserUtil.parseCondExpression(dataPermissionSql));
} else {
plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), CCJSqlParserUtil.parseCondExpression(dataPermissionSql)));
}
}
/**
* 生成拦截对象的代理
*
* @param target 目标对象
* @return 代理对象
*/
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
/**
* mybatis配置的属性
*
* @param properties mybatis配置的属性
*/
@Override
public void setProperties(Properties properties) {
}
}
5、对需要进行数据管理的管理dao方法进行注解
@Repository
public interface TraKnowInfoDao extends BaseMapper<TraKnowInfo>{
@AuthFilter
Integer queryCount(TraKnowInfoQueryCondition query);
@AuthFilter
List<TraKnowInfo> query(TraKnowInfoQueryCondition query);
}
总结:
通过mybatis-plus对业务数据表 insert和update数据时,进行默认参数封装。实现业务数据新增规划字段动态维护 ; 通过拦截StatementHandler,实现对select的sql二次重写 自动装配.area_gov_code like 条件 。 最终实现数据权限自动维护和查询时,自动过滤。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)