Spring Boot使用注解结合 MyBatis 实现基于维度的通用校验器

news2024/9/22 19:39:58

在业务系统中,控制用户对数据的访问权限是非常重要的。不同用户角色需要根据其关联的维度(如国家、地区等)来访问特定的数据。为实现这一功能,我们可以通过自定义注解和 MyBatis 拦截器来动态拼接 SQL 语句,将维度过滤逻辑灵活地应用于数据库查询。
本篇文章将详细展示如何通过 Spring Boot、MyBatis、自定义注解等技术,实现通用的维度过滤器,并结合用户、角色和维度表的设计,来动态控制数据访问权限。

1. 项目环境

使用以下技术栈:

  • Spring Boot 2.x
  • MyBatis
  • Maven
  • JDK 8+

2. 数据库表设计

为了实现用户、角色与维度之间的关联,我们设计了四个表:

  1. users:存储用户基本信息。
  2. roles:存储角色信息。
  3. dimensions:存储维度信息,如国家、地区等。
  4. user_role_dimension:绑定用户、角色和维度的关系。
2.1 users

存储用户的基本信息,如用户名和密码。

CREATE TABLE `users` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `username` VARCHAR(255) NOT NULL,
  `password` VARCHAR(255) NOT NULL
);
2.2 roles

存储系统中的角色信息,如管理员(ADMIN)、客户(CUSTOMER)等。

CREATE TABLE `roles` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `role_name` VARCHAR(50) NOT NULL
);
2.3 dimensions

维度表用于存储可供过滤的数据维度,例如国家、地区、组织等。

CREATE TABLE `dimensions` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `dimension_key` VARCHAR(255) NOT NULL,  -- 维度的键,例如 country、region
  `dimension_value` VARCHAR(255) NOT NULL -- 维度的值,例如 CN、US、EU 等
);
2.4 user_role_dimension

这个表用于将用户、角色和维度进行关联。每个用户可以通过角色与维度关联,从而确定该用户在特定角色下能够访问的数据范围。

CREATE TABLE `user_role_dimension` (
  `user_id` BIGINT NOT NULL,
  `role_id` BIGINT NOT NULL,
  `dimension_id` BIGINT NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`, `dimension_id`),
  FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
  FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`),
  FOREIGN KEY (`dimension_id`) REFERENCES `dimensions`(`id`)
);

3. 功能目标

通过自定义注解 @DimensionFilter,在 MyBatis 的 Mapper 方法中动态添加维度过滤逻辑,过滤维度值由用户在当前角色下的维度关系决定。拦截器会在查询时根据用户、角色和维度表中的关联关系拼接 SQL 语句。

4. Maven 依赖

首先,配置项目的 Maven 依赖,确保 Spring Boot、MyBatis 和 Lombok 的使用。

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MyBatis Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>

    <!-- MySQL Driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

5. 自定义注解 @DimensionFilter

首先定义一个注解 @DimensionFilter,用于在 MyBatis 的查询方法上标注需要过滤的维度字段。

package com.example.ecommerce.annotation;

import java.lang.annotation.*;

/**
 * 用于在 Mapper 查询方法中动态添加维度过滤条件。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DimensionFilter {
    /**
     * 指定需要过滤的维度列名,例如 country、region 等。
     */
    String column();
}

注解的 column 参数表示 SQL 查询中需要过滤的字段,例如 countryregion

6. 用户上下文 UserContext

为了在查询时能够动态获取当前用户的角色和维度信息,我们可以使用 ThreadLocal 来存储当前请求的用户上下文。

package com.example.ecommerce.util;

public class UserContext {
    private static final ThreadLocal<Long> userId = new ThreadLocal<>();
    private static final ThreadLocal<Long> roleId = new ThreadLocal<>();

    public static void setUserId(Long id) {
        userId.set(id);
    }

    public static Long getUserId() {
        return userId.get();
    }

    public static void setRoleId(Long id) {
        roleId.set(id);
    }

    public static Long getRoleId() {
        return roleId.get();
    }

    public static void clear() {
        userId.remove();
        roleId.remove();
    }
}

在实际场景中,UserContext 可以在用户登录或请求开始时设置,确保每个请求有其独立的用户和角色上下文。

7. 查询用户角色维度关系

我们通过 user_role_dimension 表查找当前用户和角色绑定的维度,并将这些维度值应用于 SQL 查询。


import com.example.ecommerce.model.Dimension;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface UserRoleDimensionMapper {

    // 查询当前用户在指定角色下的维度
    @Select("SELECT d.dimension_value FROM dimensions d " +
            "JOIN user_role_dimension urd ON d.id = urd.dimension_id " +
            "WHERE urd.user_id = #{userId} AND urd.role_id = #{roleId}")
    List<String> findUserDimensions(Long userId, Long roleId);
}

8. 实现 MyBatis 拦截器

MyBatis 提供了拦截器(Interceptor)接口,允许我们在 SQL 执行之前或之后对 SQL 进行动态修改。这里我们使用拦截器在执行 SQL 之前,拼接维度过滤条件。

package com.example.ecommerce.interceptor;

import com.example.ecommerce.annotation.DimensionFilter;
import com.example.ecommerce.util.UserContext;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.util.List;
import java.util.Properties;

/**
 * MyBatis 拦截器,动态拼接维度过滤条件。
 */
@Component
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DimensionInterceptor implements Interceptor {

    @Autowired
    private UserRoleDimensionMapper userRoleDimensionMapper;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String originalSql = boundSql.getSql();

        // 获取 Mapper 方法上的注解
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        DimensionFilter dimensionFilter = getDimensionFilter(mappedStatement);

        // 如果注解存在,动态添加维度过滤条件
        if (dimensionFilter != null) {
            Long userId = UserContext.getUserId();
            Long roleId = UserContext.getRoleId();

            // 查询当前用户和角色的维度
            List<String> dimensionValues = userRoleDimensionMapper.findUserDimensions(userId, roleId);

            if (!dimensionValues.isEmpty()) {
                // 动态拼接 WHERE 条件
                String dimensionClause = dimensionFilter.column() + " IN (" +
                        String.join(",", dimensionValues.stream().map(v -> "'" + v + "'").toArray(String[]::new)) + ")";
                String newSql = originalSql + " WHERE " + dimensionClause;
                setFieldValue(boundSql, "sql", newSql);
            }
        }

        return invocation.proceed();
    }

    private DimensionFilter getDimensionFilter(MappedStatement mappedStatement) {
        try {
            // 通过反射获取 Mapper 方法上的注解
            Class<?> clazz = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
            String methodName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1);
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getName().equals(methodName)) {
                    return method.getAnnotation(DimensionFilter.class);
                }
            }
        } catch (Exception e) {
            // ignore
        }
        return null;
    }

    private void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

9. 使用示例

假设我们有一个 ProductMapper,通过 @DimensionFilter 注解指定需要过滤的维度字段。

import com.example.ecommerce.annotation.DimensionFilter;
import com.example.ecommerce.model.Product;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface ProductMapper {

    @DimensionFilter(column = "country")
    @Select("SELECT * FROM products")
    List<Product> findProducts();
}

在实际运行时,根据用户的上下文,MyBatis 拦截器将动态为查询语句添加维度过滤条件,例如:

SELECT * FROM products WHERE country IN ('CN', 'US');

结论

通过上述方案,结合 MyBatis 拦截器、自定义注解和用户角色维度表,我们可以实现基于维度的动态数据过滤。这个设计既保证了代码的可维护性,也让系统对不同角色、维度的访问控制更加灵活。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2155760.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习 git 命令行的简单操作, 能够将代码上传到 Gitee 上

首先登录自己的gitee并创建好仓库 将仓库与Linux终端做链接 比如说我这里已经创建好了一个我的Linux学习仓库 点开克隆/下载&#xff1a; 在你的终端中粘贴上图中1中的指令 此时他会让你输入你的用户名和密码&#xff0c;用户名就是上图中3中Username for ....中后面你的一个…

预付费计量系统实体模型

1. 预付费计量系统实体模型 A generic entity model for electricity payment metering systems is shown in Figure 2. Although it provides a limited perspective, it does serve to convey certain essential concepts. 关于电子式预付费电表系统的实体模型见图 2…

李宏毅结构化学习 03

文章目录 一、Sequence Labeling 问题概述二、Hidden Markov Model(HMM)三、Conditional Random Field(CRF)四、Structured Perceptron/SVM五、Towards Deep Learning 一、Sequence Labeling 问题概述 二、Hidden Markov Model(HMM) 上图 training data 中的黑色字为x&#xff…

如何备份SqlServer数据库

第一步&#xff1a;登录你要备份的服务器数据库ssms 第二步&#xff1a;选择你要备份的数据库 此处已PZ-SJCS 数据库为例 右键该数据库-->任务-->备份 第三步&#xff1a;选择你备份的类型备份组件等&#xff0c;目标磁盘 &#xff0c;点击添加选择将你备份的文件备份那…

全面详尽的 PHP 环境搭建教程

目录 目录 PHP 环境搭建概述 在 Windows 上搭建 PHP 环境 使用集成环境 XAMPP 安装步骤 配置和测试 常用配置 手动安装 Apache、PHP 和 MySQL 安装 Apache 安装 PHP 安装 MySQL 配置 PHP 连接 MySQL 在 Linux 上搭建 PHP 环境 使用 LAMP 方案 安装 Apache 安装 …

【25.6】C++智能交友系统

常见错误总结 const-1 如下代码会报错 原因如下&#xff1a; man是一个const修饰的对象&#xff0c;即man不能修改任何内容&#xff0c;但是man所调用的play函数只是一个普通的函数&#xff0c;所以出现了报错。我们需要在play函数中加上const修饰&#xff0c;或者删除man对…

《论分布式存储系统架构设计》写作框架,软考高级系统架构设计师

论文真题 分布式存储系统&#xff08;Distributed Storage System&#xff09;通常将数据分散存储在多台独立的设备上。传统的网络存储系统采用集中的存储服务器存放所有数据&#xff0c;存储服务器成为系统性能的瓶颈&#xff0c;也是可靠性和安全性的焦点&#xff0c;不能满…

FreeRTOS-时间片调度

FreeRTOS-时间片调度 一、时间片调度简介二、时间片调度实验 一、时间片调度简介 同等优先级任务轮流的享有相同的CPU时间(可设置)&#xff0c;叫时间片&#xff0c;在FreeRTOS中&#xff0c;一个时间片就等于SysTick中断周期&#xff0c;所以说时间片大小取决于滴答定时器中断…

windows安装Anaconda教程

一、简介 Anaconda 是一个开源的 Python 和 R 语言的分发平台&#xff0c;专为科学计算和数据分析设计。它包含了包管理器 Conda&#xff0c;可以方便地安装和管理库、环境和依赖项。此外&#xff0c;Anaconda 还附带了许多数据科学工具和库&#xff0c;如 Jupyter Notebook 和…

【HTTPS】中间人攻击和证书的验证

中间人攻击 服务器可以创建出一堆公钥和私钥&#xff0c;黑客也可以按照同样的方式&#xff0c;创建一对公钥和私钥&#xff0c;冒充自己是服务器&#xff08;搅屎棍&#xff09; 黑客自己也能生成一对公钥和私钥。生成公钥和私钥的算法是开放的&#xff0c;服务器能生产&…

iOS17找不到developer mode

iOS17找不到开发者模式 developer mode 下载过app之后、弹窗Developer Mode Required之后&#xff0c;这个菜单就出现了&#xff08;之前死活找不到&#xff09;。 背景&#xff1a;用蒲公英分发测试app&#xff0c;有个同事买了新机(iphone 15 pro max)&#xff0c;添加了白名…

双虚拟机部署php项目

前言 经过前面的学习,我们对分布式部署有了一定的了解,这次我们尝试做些东西 准备 我打算用虚拟机部署一个外联网盘 一台虚拟机安装php另一台安装MySQL,但是之前已经安装过 MariaDB 了,就不打算改了。 通常MariaDB与MySQL兼容性很好,可以作为替代使用。彩虹外链网盘项目…

OpenAI的O1模型达到AGI二级,类人推理能力被提示危险,细思极恐!

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 今天让我们一起来聊聊最近科技圈的大新闻—…

Java笔试面试题AI答之设计模式(4)

文章目录 16. 简述什么是观察者模式&#xff1f;基本概念主要特点实现方式应用场景优缺点 17. 请列举观察者模式应用场景 &#xff1f;18. 请用Java代码实现观察者模式的案例 &#xff1f;19. 什么是装饰模式&#xff1f;定义与特点结构与角色工作原理优点应用场景示例 20. 请用…

队列的各种接口的实现(C)

队列的概念 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为队尾 出队列&#xff1a;进行删除操作的一端称为队头 队列的实…

【GlobalMapper精品教程】088:按点线面空间位置选择案例

按点线面空间位置选择的原则为:点线面的排列组合。 文章目录 一、选择线要素附近的点二、选择相交或触碰所选线的区和线三、选择包含点的区要素四、选择选定区域内的点要素一、选择线要素附近的点 启动该工具之前,首先要选择线,例如,选择某一段铁路5km范围之内的县城驻地。…

nacos适配人大金仓的数据库

前言 在微服务架构中&#xff0c;服务发现和配置管理是关键组件。Nacos作为一个动态服务发现和配置管理平台&#xff0c;支持多种数据库作为其后端存储。本文将探讨如何在Nacos中适配人大金仓数据库&#xff0c;以及在此过程中的最佳实践。 Nacos简介 Nacos&#xff08;Nami…

安装 depot_tools 和 Windows 10 SDK 为在Windows下构建基于 chromium 的浏览器(103.0.5060.68 之二)

本文已首发于&#xff1a; 秋码记录 为何要安装depot_tools 虽然我们在上一篇 Windows构建基于 Chromium 的浏览器之环境准备篇安装 Visual Studio&#xff08;103.0.5060.68 之一&#xff09; &#xff0c;已经在Windows系统安装好了Visual Studio 2019 Community版本。 然…

第十二周:机器学习

目录 摘要 Abstract 一、非监督学习 二、word embedding 三、transformer 1、应用 2、encoder 3、decoder 四、各类attention 1、最常见的类别 2、其余种类 3、小结 总结 摘要 本周继续学习机器学习的相关课程&#xff0c;首先了解了监督学习和非监督学习的概…

数据结构与算法——Java实现 9.习题——删除链表倒数节点

目录 19. 删除链表的倒数第 N 个结点 方法1 通过链表长度直接删除 方法2 递归加入哨兵节点 ListNode 方法3 快慢指针法 苦难&#xff0c;区区挫折罢了&#xff0c;而我必定站在幸福的塔尖 —— 24.9.22 19. 删除链表的倒数第 N 个结点 给你一个链表&#xff0c;删除链表的倒数第…