数据权限
现在很多企业级应用都需要拦截数据权限, 只有配置了相应数据权限的人才能看到该数据
关于数据权限的实现, 个人想了两种实现方式
第一种是基于AOP, 配置相应的注解, 在切面中将数据权限的参数值强制设置到请求参数中去, 然后dao层利用mybatis的动态sql, 将权限值拼接进去, 该方案有前提条件, 数据权限控制的字段必须放到基类中, 其他的对象要继承该基类, Mapper.xml必须抽取一个公用的, 其他的Mapper需要引用该mapper文件作为权限控制(否则每个类和Mapper.xml都要独自维护一套, 不便于后续扩展和维护)
第二种是基于mybatis的拦截器, 拦截sql后将数据权限控制的sql拼接进去, 基于mybatis拦截器拼接sql的难点在于数据权限sql和原sql的拼接, 网上很多版本都是select * from (原sql) where 数据权限sql, 这种实际上并不可取, 一旦出现分页, 或者最终查出的结果集不包含权限控制字段的话, 就会出现bug
这里使用的是正则表达式去匹配后拼接权限sql, 其他的可以参考网上mybatis拦截器的方案
/**
* sql解析器
*
* @author wang.js on 2019/5/8.
* @version 1.0
*/
public class SqlParser {
private SqlParser() {
}
private static final String SQL_WHERE = "WHERE";
private static final String SQL_LEFT_JOIN = "LEFT JOIN ";
/**
* 将数据权限的sql拼进原sql中
*
* @param originSql 原sql
* @param privilegeSql 数据权限sql
* @return String
*/
public static String handlerSql(String originSql, String privilegeSql) {
if (originSql.endsWith(";")) {
originSql = originSql.substring(0, originSql.lastIndexOf(";"));
}
originSql = originSql.replace("\t", " ").replace("\n", " ");
originSql = originSql.replaceAll(" {2,}", " ");
originSql = originSql.replaceAll(" where ", " " + SQL_WHERE + " ");
originSql = addWhere(originSql);
originSql = originSql.replace("left", "LEFT");
originSql = originSql.replace("join", "JOIN");
originSql = originSql.replaceAll("LEFT[ ]+JOIN[ ]+", SQL_LEFT_JOIN);
List<String> matcherList = matcherTableSql(originSql);
for (String matcherSql : matcherList) {
if (originSql.contains(SQL_LEFT_JOIN + matcherSql)) {
continue;
}
String newMatcherSql = mergeSql(matcherSql, privilegeSql);
originSql = originSql.replace(matcherSql, newMatcherSql);
}
return originSql;
}
/**
* 添加where关键字
*
* @param originSql 原始sql
* @return String
*/
private static String addWhere(String originSql) {
if (originSql.contains("WHERE")) {
return originSql;
}
String[] split = originSql.split(" ");
for (int i = 0; i < split.length; i++) {
if (split[i].equalsIgnoreCase("GROUP") && (i + 1) < split.length && split[i + 1].equalsIgnoreCase("BY")) {
return originSql.replaceAll(split[i], "WHERE 1=1 GROUP");
}
if (split[i].equalsIgnoreCase("ORDER") && (i + 1) < split.length && split[i + 1].equalsIgnoreCase("BY")) {
return originSql.replaceAll(split[i], "WHERE 1=1 ORDER");
}
if (split[i].equalsIgnoreCase("LIMIT")) {
return originSql.replaceAll(split[i], "WHERE 1=1 LIMIT");
}
}
return originSql + " WHERE 1=1";
}
/**
* 合并sql
*
* @param originSql 原sql
* @param privilegeSql 数据权限sql
* @return String
*/
private static String mergeSql(String originSql, String privilegeSql) {
return originSql.replace(SQL_WHERE, SQL_WHERE + " " + privilegeSql);
}
/**
* 配置符合
*
* @param originSql 原sql
* @return List<String>
*/
private static List<String> matcherTableSql(String originSql) {
List<String> matcharList = new ArrayList<>();
String regex = "\\w[ ,](.*?)WHERE";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(originSql);
while (matcher.find()) {
matcharList.add(matcher.group());
}
return matcharList;
}
}
写个测试类
@Test
public void t1() {
String privilegeSql = " brand_code in (1, 2, 3) and region_code in (2, 3) and";
String originSql = "select * from (select t1, t2, t3 from tableA where 1=1) t1, (select t1, t2, t3 from tableB s where 1=1) t2";
// String originSql = "select t1, t2, t3 from tableA";
System.out.println(SqlParser.handlerSql(originSql, privilegeSql));
}
测试后输出的结果为
select * from (select t1, t2, t3 from tableA WHERE brand_code in (1, 2, 3) and region_code in (2, 3) and 1=1) t1, (select t1, t2, t3 from tableB s WHERE brand_code in (1, 2, 3) and region_code in (2, 3) and 1=1) t2
这里需要强调的是, 必须保证每条需要权限控制的语句都含有where关键字, 否则数据权限sql拼接不进去
总结
第一种基于AOP的方案需要维护基类和Mapper的通用代码, 其他的必须继承或引用这两个文件, 但是好处是不容易出现bug
第二种基于mybatis拦截器的方案需要保证每条需要权限控制的sql都有where关键字, 好处是mapper.xml和基类都没有强制性要求
具体使用哪一种就看实际项目需要了