Mybatis拦截器做数据范围权限DataScope

简介: Mybatis拦截器做数据范围权限DataScope

 目录

业务场景:

思路:

步骤:

1、定义Mybatis拦截器DataScopeInterceptor

2、定义注解DataScope

3、springboot装配该拦截器

4、使用


业务场景:

根据业务需要,这里将角色按照数据范围做权限限定,提供三级权限分别为:

1、全部:可以查看所有的数据

2、自定义:按照组织架构,可以查看当前所匹配的组织架构数据

3、个人:仅能查看由自己创建,或者数据流转到自己节点的数据

思路:

1、定义Mybatis拦截器DataScopeInterceptor,用于每次拦截查询sql语句,附带数据范围权限sql条件

2、定义注解DataScope,用来声明哪些操作需要做范围限制

3、springboot装配该拦截器

注:这里如果有使用MybatisPlus的分页插件,需要保证执行顺序:DataScopeInterceptor > PaginationInterceptor

步骤:

1、定义Mybatis拦截器DataScopeInterceptor

/**
 * 数据权限拦截器
 * ALL = 全部
 * CUSTOMIZE = 自定义
 * SELF = 个人
 *
 * @author Shamee
 * @date 2021-04-16
 */
@Slf4j
@AllArgsConstructor
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataScopeInterceptor extends AbstractSqlParserHandler implements Interceptor {
    final private Function<String, Map<String, Object>> function;
    @Override
    @SneakyThrows
    public Object intercept(Invocation invocation) throws Throwable {
        LOGGER.info("mybatis sql注入...");
        StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        this.sqlParser(metaObject);
        // 先判断是不是SELECT操作
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        String originalSql = boundSql.getSql();
        com.ruijie.upc.app.common.annotation.DataScope dsAnnotation = isDataScope(mappedStatement.getId());
        // 不含该注解,或者注解不开启DataScope校验
        if (ObjectUtil.isNull(dsAnnotation) || !dsAnnotation.isDataScope()) {
            return invocation.proceed();
        }
        String[] orgScopeNames = dsAnnotation.orgScopeNames();
        String[] selfScopeNames = dsAnnotation.selfScopeNames();
        String userId = ShiroUtils.getCurrentUserId();
        List<String> areaIds = new ArrayList<>();
        DataScopeType dsType = DataScopeType.SELF;
        if (CollectionUtil.isEmpty(areaIds)) {
            //查询当前用户的 角色 最小权限
            Map<String, Object> result = function.apply(userId);
            if (result == null) {
                return invocation.proceed();
            }
            Integer dataScopeType = (Integer) result.get("dataScopeType");
            dsType = DataScopeType.get(dataScopeType);
            areaIds = (List<String>) result.get("areaIds");
        }
        //查全部
        if (DataScopeType.ALL.equals(dsType)) {
            return invocation.proceed();
        }
        //查个人
        if (DataScopeType.SELF.equals(dsType)) {
            if(selfScopeNames != null && selfScopeNames.length > 0){
                String collect = Arrays.asList(selfScopeNames).stream().map(o -> {
                    return "temp_data_scope." + o + "='" + userId + "'";
                }).collect(Collectors.joining(" or "));
                originalSql = "select * from (" + originalSql + ") temp_data_scope where (" + collect + ")";
            }
        }
        //查其他
        else if (orgScopeNames != null && orgScopeNames.length > 0) {
            String join = CollectionUtil.join(areaIds, ",");
            String collect = Arrays.asList(selfScopeNames).stream().map(o -> {
                return "temp_data_scope." + o + " in (" + join + ")";
            }).collect(Collectors.joining(" or "));
            originalSql = "select * from (" + originalSql + ") temp_data_scope where (" + collect + ")";
        }
        metaObject.setValue("delegate.boundSql.sql", originalSql);
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object o) {
        if (o instanceof StatementHandler) {
            return Plugin.wrap(o, this);
        }
        return o;
    }
    @Override
    public void setProperties(Properties properties) {
    }
    /**
     * 校验是否含有DataScope注解
     * @param namespace
     * @return
     * @throws ClassNotFoundException
     */
    private com.ruijie.upc.app.common.annotation.DataScope isDataScope(String namespace) throws ClassNotFoundException {
        if(StrUtil.isBlank(namespace)){
            return null;
        }
        //获取mapper名称
        String className = namespace.substring(0, namespace.lastIndexOf("."));
        //获取方法名
        String methodName = namespace.substring(namespace.lastIndexOf(".") + 1, namespace.length());
        //获取当前mapper 的方法
        Method[] methods = Class.forName(className).getMethods();
        Optional<Method> first = Arrays.asList(methods).stream().filter(method -> method.getName().equals(methodName)).findFirst();
        com.ruijie.upc.app.common.annotation.DataScope annotation = first.get().getAnnotation(com.ruijie.upc.app.common.annotation.DataScope.class);
        return annotation;
    }
}

image.gif

2、定义注解DataScope

/**
 * 数据权限校验注解
 * @author Shamee
 * @date 2021-04-27
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
    /**
     * 是否开启DataScope校验,默认是
     * @return
     */
    boolean isDataScope() default true;
    /**
     * 限制范围的字段名称 (除个人外),暂时限定到省区
     */
    String[] orgScopeNames() default {"province_id"};
    /**
     * 限制数据流装,范围是个人时的字段
     */
    String[] selfScopeNames() default {"created_by"};
}

image.gif

由于为了兼容每个表命名字段不一致问题,这里采用传参的方式,由业务开发人员自由传参

再定义一个枚举:

/**
 * 数据范围枚举
 * @author Shamee
 * @date 2021-04-16
 */
@Getter
@AllArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum DataScopeType implements IEnum<Integer> {
    /**
     * ALL="全部"
     */
    ALL(1, "全部"),
    /**
     * CUSTOMIZE=“自定义”
     */
    CUSTOMIZE(2, "自定义"),
    /**
     * SELF="个人"
     */
    SELF(3, "个人")
    ;
    private Integer value;
    private String text;
    @Override
    public Integer getValue() {
        return value;
    }
    public static DataScopeType get(Integer val) {
        return match(val, null);
    }
    public static DataScopeType get(String val) {
        return match(val, null);
    }
    public static DataScopeType match(String val, DataScopeType def) {
        for (DataScopeType enm : DataScopeType.values()) {
            if (enm.name().equalsIgnoreCase(val)) {
                return enm;
            }
        }
        return def;
    }
    public static DataScopeType match(Integer val, DataScopeType def) {
        if (val == null) {
            return def;
        }
        for (DataScopeType enm : DataScopeType.values()) {
            if (val.equals(enm.getValue())) {
                return enm;
            }
        }
        return def;
    }
    public String getCode(){
        return this.name();
    }
}

image.gif

3、springboot装配该拦截器

/**
 * 配置mybatis信息
 * @author Shamee
 * @date 2021-04-28
 */
@Configuration
@Slf4j
public class MybatisAutoConfiguration {
    @Resource
    private List<SqlSessionFactory> sqlSessionFactoryList;
    /**
     *  这里使用构造回调的方式提高DataScope拦截器的执行顺序,
     *  执行顺序必须:DataScopeInterceptor> PaginationInterceptor
     */
    @PostConstruct
    public void addMybatisInterceptors() {
        DataScopeInterceptor dataScopeInterceptor = dataScopeInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(dataScopeInterceptor);
        }
    }
    public DataScopeInterceptor dataScopeInterceptor() {
        return new DataScopeInterceptor((userId) -> SpringUtils.getBean(UserService.class).getDataScopeById(userId));
    }
}

image.gif

注:

1、这里由于公司基础架构决定,这里没办法使用@Order来提高执行顺序,故采用@PostConstruct方式来处理,比较粗暴。

2、getDataScopeById方法为数据库按照业务规则拉取角色所匹配到的数据范围,如自定义则为匹配到的组织架构数据

4、使用

public interface SheetSpecialProjectDao extends BaseMapper<SheetSpecialProject> {
  @DataScope(selfScopeNames = {"created_by", "service_representative"})
  IPage<SheetSpecialProjectPageDto> querySpecialProjectPage(IPage pageInput);
}

image.gif

5、附加说明

1、Mybatis拦截器(插件)是采用代理的方式,装载到InterceptorChain中,这里的执行顺序会与配置的顺序相反来执行,即@Order越大,越优先执行,与Spring相反。具体可以查看mybatis官网或者源码。

2、插件类型:

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    • ParameterHandler (getParameterObject, setParameters)
    • ResultSetHandler (handleResultSets, handleOutputParameters)
    • StatementHandler (prepare, parameterize, batch, update, query)

    我们这里需要解析sql并重组,所以使用StatementHandler。具体查看:mybatis – MyBatis 3 | 配置


    相关文章
    |
    3月前
    |
    Java 数据库连接 数据库
    mybatis查询数据,返回的对象少了一个字段
    mybatis查询数据,返回的对象少了一个字段
    203 8
    |
    2月前
    |
    SQL JSON Java
    mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
    这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
    60 1
    mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
    |
    3月前
    |
    SQL Java 数据库连接
    解决mybatis-plus 拦截器不生效--分页插件不生效
    本文介绍了在使用 Mybatis-Plus 进行分页查询时遇到的问题及解决方法。依赖包包括 `mybatis-plus-boot-starter`、`mybatis-plus-extension` 等,并给出了正确的分页配置和代码示例。当分页功能失效时,需将 Mybatis-Plus 版本改为 3.5.5 并正确配置拦截器。
    762 6
    解决mybatis-plus 拦截器不生效--分页插件不生效
    |
    4月前
    |
    Java 数据库连接 测试技术
    SpringBoot 3.3.2 + ShardingSphere 5.5 + Mybatis-plus:轻松搞定数据加解密,支持字段级!
    【8月更文挑战第30天】在数据驱动的时代,数据的安全性显得尤为重要。特别是在涉及用户隐私或敏感信息的应用中,如何确保数据在存储和传输过程中的安全性成为了开发者必须面对的问题。今天,我们将围绕SpringBoot 3.3.2、ShardingSphere 5.5以及Mybatis-plus的组合,探讨如何轻松实现数据的字段级加解密,为数据安全保驾护航。
    301 1
    |
    4月前
    |
    SQL 关系型数据库 MySQL
    解决:Mybatis-plus向数据库插入数据的时候 报You have an error in your SQL syntax
    该博客文章讨论了在使用Mybatis-Plus向数据库插入数据时遇到的一个常见问题:SQL语法错误。作者发现错误是由于数据库字段中使用了MySQL的关键字,导致SQL语句执行失败。解决方法是将这些关键字替换为其他字段名称,以避免语法错误。文章通过截图展示了具体的操作步骤。
    |
    4月前
    |
    SQL Java 数据库连接
    springboot+mybatis+shiro项目中使用shiro实现登录用户的权限验证。权限表、角色表、用户表。从不同的表中收集用户的权限、
    这篇文章介绍了在Spring Boot + MyBatis + Shiro项目中,如何使用Shiro框架实现登录用户的权限验证,包括用户、角色和权限表的设计,以及通过多个表查询来收集和验证用户权限的方法和代码实现。
    springboot+mybatis+shiro项目中使用shiro实现登录用户的权限验证。权限表、角色表、用户表。从不同的表中收集用户的权限、
    |
    4月前
    |
    SQL Java 关系型数据库
    MyBatis-Plus 分页魅力绽放!紧跟技术热点,带你领略数据分页的高效与便捷
    【8月更文挑战第29天】在 Java 开发中,数据处理至关重要,尤其在大量数据查询与展示时,分页功能尤为重要。MyBatis-Plus 作为一款强大的持久层框架,提供了便捷高效的分页解决方案。通过封装数据库分页查询语句,开发者能轻松实现分页功能。在实际应用中,只需创建 `Page` 对象并设置页码和每页条数,再通过 `QueryWrapper` 构建查询条件,调用 `selectPage` 方法即可完成分页查询。MyBatis-Plus 不仅生成分页 SQL 语句,还自动处理参数合法性检查,并支持条件查询和排序等功能,极大地提升了系统性能和稳定性。
    63 0
    |
    4月前
    |
    存储 SQL Java
    MyBatis batchInsert 批量插入数据
    MyBatis batchInsert 批量插入数据
    89 0
    |
    4月前
    |
    前端开发 JavaScript Java
    解决springboot+vue+mybatis中,将后台数据分页显示在前台,并且根据页码自动跳转对应页码信息
    该博客文章讲述了如何在Spring Boot + Vue + MyBatis的项目中实现后台数据的分页查询,并在前端进行显示和页码跳转,包括后端的分页查询实现、前端与后端的交互以及使用Element UI进行分页展示的方法。
    |
    6月前
    |
    SQL 人工智能 Java
    mybatis-plus配置sql拦截器实现完整sql打印
    _shigen_ 博主分享了如何在MyBatis-Plus中打印完整SQL,包括更新和查询操作。默认日志打印的SQL用?代替参数,但通过自定义`SqlInterceptor`可以显示详细信息。代码示例展示了拦截器如何替换?以显示实际参数,并计算执行时间。配置中添加拦截器以启用此功能。文章提到了分页查询时的限制,以及对AI在编程辅助方面的思考。
    779 5
    mybatis-plus配置sql拦截器实现完整sql打印