Java | 基于自定义注解与AOP切面实现数据权限管控的思路和实践

关注:CodingTechWork

引言

在企业级应用中,数据权限控制是一个常见的需求。本文将通过一个完整的示例,展示如何使用自定义注解和AOP切面在Spring Boot项目中实现数据权限管控,以商品实例列表查询为例,根据用户角色动态过滤数据。同时,我们将提供完整的表结构和数据插入脚本,以便更好地理解和测试。

背景与需求

在一个电商平台中,商品实例数据需要根据用户角色进行权限控制。例如:

  • 普通用户只能查看自己购买或收藏的商品实例。
  • 供应商只能查看自己供应的商品实例。
  • 管理员可以查看所有商品实例。

为了实现这种权限控制,我们需要:

  1. 定义一个自定义注解,用于标记需要进行数据权限管控的方法。
  2. 使用AOP切面拦截这些方法,并根据用户角色动态添加过滤条件。
  3. 在Mapper层动态拼接SQL语句,实现数据过滤。
  4. 提供完整的表结构和数据插入脚本,以便测试。

实现步骤

定义表结构

用户表(t_user

sql 复制代码
CREATE TABLE t_user (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    role VARCHAR(20) NOT NULL,
    instance_ids VARCHAR(255) -- 以逗号分隔的商品实例ID,用于普通用户和供应商
);

商品实例表(t_product

sql 复制代码
CREATE TABLE t_product (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    user_id INT, -- 关联用户ID,用于普通用户和供应商
    FOREIGN KEY (user_id) REFERENCES t_user(id)
);

插入测试数据

插入用户数据

sql 复制代码
-- 管理员
INSERT INTO t_user (username, password, role) VALUES ('admin', 'admin123', 'ADMIN');

-- 供应商
INSERT INTO t_user (username, password, role, instance_ids) VALUES ('supplier', 'supplier123', 'SUPPLIER', '1,2,3');

-- 普通用户
INSERT INTO t_user (username, password, role, instance_ids) VALUES ('user', 'user123', 'USER', '1');

插入商品实例数据

sql 复制代码
-- 商品1(普通用户关联)
INSERT INTO t_product (name, description, user_id) VALUES ('商品1', '这是商品1的描述', 3);

-- 商品2(供应商关联)
INSERT INTO t_product (name, description, user_id) VALUES ('商品2', '这是商品2的描述', 2);

-- 商品3(供应商关联)
INSERT INTO t_product (name, description, user_id) VALUES ('商品3', '这是商品3的描述', 2);

-- 商品4(无关联,管理员可见)
INSERT INTO t_product (name, description) VALUES ('商品4', '这是商品4的描述');

定义自定义注解

定义一个自定义注解@DataScope,用于标记需要进行数据权限管控的方法。

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
    /**
     * 需要过滤数据的字段名,默认为user_id
     */
    String field() default "user_id";

    /**
     * 数据表的别名,默认为t
     */
    String tableAlias() default "t";
}

创建AOP切面类

创建一个AOP切面类DataScopeAspect,用于拦截被@DataScope注解标记的方法,并在方法执行前插入数据权限过滤逻辑。

java 复制代码
@Aspect
@Component
public class DataScopeAspect {
    @Before("@annotation(dataScope)")
    public void doBefore(JoinPoint joinPoint, DataScope dataScope) throws Throwable {
        // 获取当前登录用户
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String username = userDetails.getUsername();

            // 假设我们有一个方法可以根据用户名获取用户角色和关联的实例ID
            UserRole userRole = getUserRole(username);

            // 获取方法的参数
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                if (arg instanceof Map) {
                    Map<String, Object> paramMap = (Map<String, Object>) arg;
                    if (userRole.getRole().equals("ADMIN")) {
                        // 管理员可以查看所有数据,无需过滤
                        continue;
                    }
                    // 构造过滤条件
                    String dataScopeCondition = " AND " + dataScope.tableAlias() + "." + dataScope.field() + " IN (" + String.join(",", userRole.getInstanceIds()) + ")";
                    paramMap.put("dataScope", dataScopeCondition);
                }
            }
        }
    }

    private UserRole getUserRole(String username) {
        // 模拟从数据库获取用户角色和关联的实例ID
        if ("admin".equals(username)) {
            return new UserRole("ADMIN", Collections.emptyList());
        } else if ("supplier".equals(username)) {
            return new UserRole("SUPPLIER", Arrays.asList("1", "2", "3")); // 假设供应商关联的商品实例ID为1, 2, 3
        } else {
            return new UserRole("USER", Collections.singletonList("1")); // 普通用户只能查看实例ID为1的商品
        }
    }

    @Data
    @AllArgsConstructor
    private static class UserRole {
        private String role;
        private List<String> instanceIds;
    }
}

配置Mapper层

在Mapper层,我们需要根据传递的参数动态拼接SQL语句,以实现数据权限过滤。

java 复制代码
@Mapper
public interface ProductMapper {
    List<ProductEntity> listProducts(@Param("param") Map<String, Object> param);
}

对应的Mapper XML文件如下:

xml 复制代码
<select id="listProducts" parameterType="Map" resultType="ProductEntity">
    SELECT * FROM t_product
    <where>
        <if test="param.keyword != null and param.keyword != ''">
            AND name LIKE CONCAT('%', #{param.keyword}, '%')
        </if>
        <if test="param.dataScope != null">
            ${param.dataScope}
        </if>
    </where>
</select>

在Service层使用注解

在Service层的方法上使用@DataScope注解,标记需要进行数据权限管控的方法。

java 复制代码
@Service
public class ProductService {
    @Autowired
    private ProductMapper productMapper;

    @DataScope
    public List<ProductEntity> listProducts(Map<String, Object> param) {
        return productMapper.listProducts(param);
    }
}

Controller层

在Controller层,提供一个接口供前端调用。

java 复制代码
@RestController
@RequestMapping("/products")
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping
    public ResponseEntity<List<ProductEntity>> getProducts(@RequestParam Map<String, Object> param) {
        List<ProductEntity> products = productService.listProducts(param);
        return ResponseEntity.ok(products);
    }
}

测试与验证

假设我们有以下用户:

  • admin(管理员)
  • supplier(供应商,关联商品实例ID为1, 2, 3)
  • user(普通用户,关联商品实例ID为1)

测试用例 1:管理员查询商品列表

请求:

bash 复制代码
GET /products

用户:admin

响应:

json 复制代码
[
    {"id": 1, "name": "商品1"},
    {"id": 2, "name": "商品2"},
    {"id": 3, "name": "商品3"},
    {"id": 4, "name": "商品4"}
]

管理员可以查看所有商品实例。

测试用例 2:供应商查询商品列表

请求:

bash 复制代码
GET /products

用户:supplier

响应:

json 复制代码
[
    {"id": 1, "name": "商品1"},
    {"id": 2, "name": "商品2"},
    {"id": 3, "name": "商品3"}
]

供应商只能查看自己供应的商品实例(ID为1, 2, 3)。

测试用例 3:普通用户查询商品列表

请求:

bash 复制代码
GET /products

用户:user

响应:

json 复制代码
[
    {"id": 1, "name": "商品1"}
]

普通用户只能查看自己关联的商品实例(ID为1)。

四、总结

通过自定义注解和AOP切面,我们可以非常灵活地实现数据权限管控。这种方式不仅减少了重复代码,还提高了代码的可读性和可维护性。在实际项目中,

相关推荐
Piper蛋窝3 小时前
深入 Go 语言垃圾回收:从原理到内建类型 Slice、Map 的陷阱以及为何需要 strings.Builder
后端·go
六毛的毛5 小时前
Springboot开发常见注解一览
java·spring boot·后端
AntBlack5 小时前
拖了五个月 ,不当韭菜体验版算是正式发布了
前端·后端·python
31535669135 小时前
一个简单的脚本,让pdf开启夜间模式
前端·后端
uzong6 小时前
curl案例讲解
后端
一只叫煤球的猫6 小时前
真实事故复盘:Redis分布式锁居然失效了?公司十年老程序员踩的坑
java·redis·后端
大鸡腿同学7 小时前
身弱武修法:玄之又玄,奇妙之门
后端
轻语呢喃9 小时前
JavaScript :字符串模板——优雅编程的基石
前端·javascript·后端
MikeWe9 小时前
Paddle张量操作全解析:从基础创建到高级应用
后端
岫珩9 小时前
Ubuntu系统关闭防火墙的正确方式
后端