一、需求背景介绍
1、需求介绍
需要实现数据权限管理,包含角色:普通用户、组长、管理员。其中普通用户只能看到自己创建的项目,组长能看到自己所管理的普通用户创建的项目,管理员能看到所有项目。相关表为:项目表(包含责任人owner字段,owner所属组group字段)、用户表(包含组id)、组长信息表、管理员表。
2、方案设计
采用Mybatis拦截器,在请求查询sql后拼条件。
(1)如果当前用户为普通用户,查询项目时拼上条件owner=user;
(2)如果当前用户为组长,查找当前user所管理的组list,拼上条件group in (…);
(3)如果当前用户为管理员,不拼额外条件。
二、Mybatis拦截器介绍
关于Mybatis拦截器,网上有不少博客介绍,本文不做详细解释,这里贴一个相关链接Mabatis拦截器。
Mybatis拦截器并不是每个对象里面的方法都可以被拦截的。Mybatis拦截器只能拦截Executor、ParameterHandler、StatementHandler、ResultSetHandler四个对象里的方法。
三、代码
本文采用拦截StatementHandler里的prepare方法。(不会影响分页结果。代码做了删减,无法直接使用,可做参考)
import java.io.StringReader;
import java.sql.Connection;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
@Slf4j
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})})
@Component
public class AuthInterceptor extends AbstractSqlParserHandler implements Interceptor {
public AuthInterceptor() {
}
/**
* 自定义的逻辑处理
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
//映射工具
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
//中间去判断多层
this.sqlParser(metaObject);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
//获取到执行的sql
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
CCJSqlParserManager parserManager = new CCJSqlParserManager();
//select
if (SqlCommandType.SELECT == mappedStatement.getSqlCommandType() && StatementType.CALLABLE != mappedStatement
.getStatementType()) {
Select select = (Select) parserManager.parse(new StringReader(boundSql.getSql()));
PlainSelect selectBody = (PlainSelect) select.getSelectBody();
// 获取表名
String tableName = ((Table) selectBody.getFromItem()).getName();
// 这里对需要拦截的表做下筛选
if (StringUtils.equalsAny(tableName, "project", "supplier")) {
log.info("权限拦截...");
String aliasName = ""; // 表别名
try {
aliasName = selectBody.getFromItem().getAlias().getName() + ".";
} catch (Exception ignored) {
}
// 需要拼的条件
String whereSql = getNewSql(tableName, aliasName);
// 把要拼的条件设置进去
if (StringUtils.isNotEmpty(whereSql)) {
Expression whereExpression = CCJSqlParserUtil.parseCondExpression(whereSql);
if (null != selectBody.getWhere()) {
selectBody.setWhere(new AndExpression(selectBody.getWhere(), new Parenthesis(whereExpression)));
} else {
selectBody.setWhere(whereExpression);
}
}
metaObject.setValue("delegate.boundSql.sql", selectBody.toString());
}
}
return invocation.proceed();
}
private String getNewSql(String tableName, String aliasName) {
StringBuilder whereSql = new StringBuilder();
UserInfo userInfo = UserUtil.getCurrentUser();
String userRole = userInfo.getRole(); // 当前用户角色,逗号分隔
String groupList = userInfo.getGroup(); // 当前用户所管理的组,逗号分隔
switch (tableName) {
case "project":
if (userRole.contains(Role.manager.getCode())) {
} else if (userRole.contains(Role.leader.getCode())) {
List<String> purchaseTeamList = Arrays.asList(sourcingProjectScope.split(","));
whereSql.append("(").append(aliasName).append("group in ('").append(String.join("','",
purchaseTeamList)).append("')");
} else {
whereSql.append(aliasName).append("owner = '").append(userInfo.getUsername()).append("'");
}
break;
case "supplier":
/*
如果拦截的是别的表,需要做什么操作,可以自己加。
*/
break;
default:
return null;
}
return whereSql.toString();
}
@Override
public Object plugin(Object target) {
return target instanceof StatementHandler ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
}
}
四、关于操作权限
数据权限做了简化处理,实际比上文介绍的复杂。比如组长及管理员不能查看普通用户新建、取消等状态的项目,组长及管理员只有只读权限没有编辑权限等。这个就涉及到操作权限了。
方案:将页面上的按钮、服务、Tab等资源赋予一个唯一资源id。对于一个用户所拥有的角色,该角色具有的操作权限资源集合作为A。对于只读角色,所能操作的资源集合作为B。那么一个用户进入到项目,owner字段如果不是当前user,则该用户不能编辑,只有只读权限,该用户所拥有的资源集合就为A∩B。将该资源集合返回给前端,前端给予控制展示。
另外,如果系统只允许用户表中存在的用户使用,那么可以在拦截器中做一层过滤,不是用户表中的用户,返回403跳转无权限页。在网关也可以做一层操作权限控制,如果当前用户所请求的uri不在该用户所能操作的资源uri集合A中,则直接拒绝。