您的位置:  首页 > 技术 > 数据库 > 正文

动态过滤SQL查询字段 MyBatis

2022-01-19 12:00 https://my.oschina.net/huifer/blog/5403597 huifer 次阅读 条评论

动态过滤SQL查询字段 MyBatis

在权限系统中通常会对数据可见权限进行控制,一般的会有可见数据行数的控制,也会存在对数据返回列的控制,本文主要对列的控制进行技术设计。

需求分析

在MyBatis中对SQL查询结果的数据字段进行过滤。举例主要执行sql如下

select a.name,a.id,b.name,b.id from user a left join dept b on a.dept_id=b.id 

在业务场景中需要将dept表的id不做结果返回。

根据上述需求可以对需要执行的sql语句进行修改,将不必要的数据字段进行移除。需要使用到的技术:Sql解析(net.sf.jsqlparser)。

技术实现

本节将介绍如何进行sql解析以及sql查询结果的过滤。首先需要将sql语句(字符串)转换为jsqlparser中的对象,具体代码如下。

    String sql = "select a.name,a.id,b.name,b.id from user a left join dept b on a.dept_id=b.id ";
    Select select = (Select) CCJSqlParserUtil.parse(sql);
    PlainSelect selectBody = (PlainSelect) select.getSelectBody();

通过上述代码得到selectBody对象,在该对象中存在两个关键属性

  1. fromItem表示sql语句中from关键字后面的表信息
  2. joins表示sql语句中join关键字后面的表信息

在本例中数据信息如图所示。

可以发现在joins的元素中和fromItem对象中都存在Table类型的数据,通过Table类型可以从中获取表名,具体获取代码如下。

private static void extracted(Map<String, String> aliasMapTable, FromItem fromItem) {
  if (fromItem instanceof Table) {
    String name = ((Table) fromItem).getName();
    Alias alias = fromItem.getAlias();

    if (alias.getName() != null) {
      aliasMapTable.put(alias.getName(), name);
    } else {
      aliasMapTable.put(name, name);
    }
  }
}

至此就可以将sql语句中的别名和表真名的关系解析得到,后续需要解决的问题是对sql语句中的查询字段进行过滤。首先需要获取sql语句中的查询字段,获取代码如下。

        List<SelectItem> selectItems = selectBody.getSelectItems();

在本例中上述代码执行结果如图所示

在图中可以发现sql语句中的查询结果都已经获取成功,接下来需要进行移除字段,设计一个存储容器用来存放数据表的可见字段,设计如下。

  static Map<String, List<String>> tableIncludeCol = new HashMap<>();

  static {
    List<String> a = new ArrayList<>();
    a.add("name");
    tableIncludeCol.put("user", a);
    List<String> b = new ArrayList<>();
    b.add("id");
    tableIncludeCol.put("dept", b);
  }

key:表名

value:可见字段名称

确定上述内容后接下来需要将数据进行移除操作,移除操作代码如下

    Iterator<SelectItem> iterator = selectItems.iterator();
    while (iterator.hasNext()) {
      SelectItem next = iterator.next();

      if (next instanceof SelectExpressionItem) {
        Alias alias = ((SelectExpressionItem) next).getAlias();
        Expression expression = ((SelectExpressionItem) next).getExpression();

        if (expression instanceof Column) {
          Table table = ((Column) expression).getTable();

          String columnName = ((Column) expression).getColumnName();

          String fullTableName = aliasMapTable.get(table.getName());
          if (!tableIncludeCol.get(fullTableName).contains(columnName)) {
            iterator.remove();
          }
        }
      }
    }

此时将代码执行查看sql语句,内容如下。

SELECT a.name, b.id FROM user a LEFT JOIN dept b ON a.dept_id = b.id

可以发现这个效果和前文对可见字段的配置符合。

通过这样的方式我们还可以增加字段,不过不建议这么做。笔者建议一开始就将字段全部列出来再过滤,如果是动态增加字段在处理过程中会比较麻烦(Java对象的改动成本)。

有了上述技术准备接下来就可以与Mybatis结合,结合的核心是通过拦截器,实现Mybatis中的Interceptor接口。具体实现代码如下。

@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Object.class})})
public class PermissionInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(PermissionInterceptor.class);


    static Map<String, List<String>> tableIncludeCol = new HashMap<>();

    static {
        List<String> a = new ArrayList<>();
        a.add("name");
        tableIncludeCol.put("user", a);
        List<String> b = new ArrayList<>();
        b.add("id");
        tableIncludeCol.put("dept", b);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 判断是否需要使用过滤
        PermissionObject permissionObject = PermissionHelper.getLocalPermission();
        if (!permissionObject.getPermission()) {
            return invocation.proceed();
        } else {
            PermissionHelper.reset();
        }
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        String sql = (String) metaObject.getValue("delegate.boundSql.sql");
        Select select = (Select) CCJSqlParserUtil.parse(sql);
        PlainSelect selectBody = (PlainSelect) select.getSelectBody();

        Map<String, String> aliasMapTable = new HashMap<>(8);

        FromItem fromItem = selectBody.getFromItem();
        extracted(aliasMapTable, fromItem);

        List<Join> joins = selectBody.getJoins();

        for (Join join : joins) {
            FromItem rightItem = join.getRightItem();
            extracted(aliasMapTable, rightItem);
        }

        List<SelectItem> selectItems = selectBody.getSelectItems();

        Iterator<SelectItem> iterator = selectItems.iterator();

        while (iterator.hasNext()) {
            SelectItem next = iterator.next();

            if (next instanceof SelectExpressionItem) {
                Alias alias = ((SelectExpressionItem) next).getAlias();
                Expression expression = ((SelectExpressionItem) next).getExpression();

                if (expression instanceof Column) {
                    Table table = ((Column) expression).getTable();

                    String columnName = ((Column) expression).getColumnName();

                    String fullTableName = aliasMapTable.get(table.getName());
                    if (!tableIncludeCol.get(fullTableName).contains(columnName)) {
                        iterator.remove();
                    }
                }
            }
        }
        selectBody.setSelectItems(selectItems);
        System.out.println(select);
        metaObject.setValue("delegate.boundSql.sql", select.toString());
        return invocation.proceed();
    }

    private void extracted(Map<String, String> aliasMapTable, FromItem fromItem) {
        if (fromItem instanceof Table) {
            String name = ((Table) fromItem).getName();
            Alias alias = fromItem.getAlias();

            if (alias.getName() != null) {
                aliasMapTable.put(alias.getName(), name);
            } else {
                aliasMapTable.put(name, name);
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
展开阅读全文
  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接