一、背景
管理类系统常用的权限控制方式基于角色为基础设计(RBAC,Role-Based Access Control),主要由用户,角色,权限(资源)三个实体;用户角色关系,
角色权限关系两个中间关联控制。
图1.1 RBAC权限控制基本关系图
通过给角色关联权限,给用户授予角色的方式最终把用户和权限(资源)关联起来,前端使用自定义标签(后端模板,如jsp,thymeleaf,FreeMarker;纯前端框架如vue在登录后查询所有权限存储,
使用v-if标签控制显示隐藏),后端结合自定义注解和AOP技术(或filter,spring intercepter)。能够对前端的菜单,按钮以及后端接口灵活地进行鉴权控制。
RBAC的权限管理粒度比较粗放,仅能到按钮(接口)层级,对于某些需要涉及到数据权限控制的场景不大好操作。
例如,某一用户新增的某条数据仅能同部门员工查看,或仅能创建者查看。
二、目的
在过去,数据权限大多使用硬编码方式在SQL中定义查询条件,虽然实现了功能,可是代码冗余,拓展性和维护性差,不灵活。
所以设计一套通用的动态数据权限框架将大大降低开发的工作量。
三、实现方式
数据权限的控制最终都会落到SQL的查询条件上,如果能动态控制执行的SQL,添加上与权限相关的条件,问题将得到解决。
常用的MyBatis分页拦截器(MyBatisPlus中的PaginationInterceptor)都是在查询执行前依据不同数据库类型动态修改SQL,然后获取分页数据的。
所以模仿分页插件,对需要限制数据权限的查询进行修改即可实现数据权限功能。
@Intercepts(
{@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class DataScopeInterceptor implements Interceptor {
private static final Logger LOG = LoggerFactory.getLogger(DataScopeInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1.获取执行的RoutingStatementHandler
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
// 2.获取MetaObject
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 3.获取MappedStatement, 用于判定方法上的注解
MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 4.MappedStatement的ID即为Mapper方法的全路径名
String methodId = ms.getId();
LOG.info("mapper method: {}", methodId);
// 5.获取Mapper的Class名称
String clazzName = methodId.substring(0, methodId.lastIndexOf("."));
// 6.获取拦截的方法名
String methodName = methodId.substring(methodId.lastIndexOf(".") + 1);
LOG.info("clazzName: {}, methodName: {}", clazzName, methodName);
// 7.反射获取方法上的注解内容
Method[] methods = Class.forName(clazzName).getDeclaredMethods();
DataScope dataScope = null;
for (Method md : methods) {
if (methodName.equalsIgnoreCase(md.getName())) {
dataScope = md.getAnnotation(DataScope.class);
}
}
if (dataScope == null) {
return invocation.proceed();
}
String type = dataScope.type();
String column = dataScope.column();
if (StringUtils.isAnyEmpty(type, column)) {
return invocation.proceed();
}
// 8.获取原始执行的SQL
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
sql = sql.replaceAll("\\n", "").replaceAll("\\t", "");
LOG.info("original SQL: {}", sql);
// 9.根据注解内容修改SQL后执行
if ("DEPT".equals(type)) {
String newSql = "SELECT * FROM (" + sql + ") sss WHERE sss." + column + " = 1";
LOG.info("new SQL: {}", newSql);
metaObject.setValue("delegate.boundSql.sql", newSql);
}
return invocation.proceed();
}
}
增加数据权限注解基于反射拼装数据权限:
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
// 权限的类型
String type() default "";
// 限制的字段
String column() default "";
}
样例代码:
@Mapper
public interface UserMapper {
@SqlParser(filter = true)
@DataScope(type = "DEPT", column = "create_dept")
IPage<User> selectPage(IPage<User> page, @Param("m") Map<String, Object> queryMap);
}
四 总结
整体的数据权限实现方案就是,通过注解标注当前mapper需要数据权限控制,然后基于反射或者其他方式识别当前标记,并基于mybatis拦截器对sql进行增加,动态的改写sql支持权限内容。
其中比较核心的功能点就是如何更灵活的设计权限,以及如何实现sql的改写。
评论区