在权限系统中通常会对数据可见权限进行控制,一般的会有可见数据行数的控制,也会存在对数据返回列的控制,本文主要对列的控制进行技术设计。
在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对象,在该对象中存在两个关键属性
在本例中数据信息如图所示。
可以发现在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) {
}
}
|