基于mybatis拦截器的行级数据权限管理方案

基于mybatis拦截器的行级数据权限管理方案,第1张


最近在做一个资源管理项目,其中有一个需求,需要对数据进行地域纬度管理。不同登录用户查询对应部分数据,但涉及业务表数据较多,单个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);

}
  1. 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 条件 。 最终实现数据权限自动维护和查询时,自动过滤。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/721942.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存