侧边栏壁纸
博主头像
GG's Blog博主等级

行动起来,活在当下

  • 累计撰写 23 篇文章
  • 累计创建 12 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

数据权限的一些实现思路

mrqinzh
2024-06-10 / 0 评论 / 0 点赞 / 41 阅读 / 5388 字

一、背景

管理类系统常用的权限控制方式基于角色为基础设计(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的改写。

0

评论区