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

在业务系统中,控制用户对数据的访问权限是非常重要的。不同用户角色需要根据其关联的维度(如国家、地区等)来访问特定的数据。为实现这一功能,我们可以通过自定义注解和 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 拦截器、自定义注解和用户角色维度表,我们可以实现基于维度的动态数据过滤。这个设计既保证了代码的可维护性,也让系统对不同角色、维度的访问控制更加灵活。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J老熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值