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切面,我们可以非常灵活地实现数据权限管控。这种方式不仅减少了重复代码,还提高了代码的可读性和可维护性。在实际项目中,

相关推荐
热爱学习的路人甲6 分钟前
GoKV
后端
陈哥聊测试7 分钟前
开发认为测试不及时,测试吐槽工作量太大?
后端·测试·devops
写bug写bug7 分钟前
为什么 LIMIT 0, 10 快,而 LIMIT 1000000, 10 慢?
数据库·后端·mysql
飞鱼荷兰猪26 分钟前
LLM大语言模型简述
后端·aigc
小爷毛毛_卓寿杰32 分钟前
【Dify(v1.x) 核心源码深入解析】errors、extension 和 external_data_tool 模块
人工智能·后端·python
溪饱鱼38 分钟前
秒杀传统数据库!Cloudflare D1 + Drizzle组合拳,高并发高可用,让我们的成本爆降10倍 - D1
前端·后端
廖广杰44 分钟前
java虚拟机-为什么TLAB能提升对象分配效率?如何配置TLAB大小
后端
终身学习基地2 小时前
第一篇:Django简介
后端·python·django
Apifox3 小时前
Apifox 4月更新|Apifox在线文档支持LLMs.txt、评论支持使用@提及成员、支持为团队配置「IP 允许访问名单」
前端·后端·ai编程
我家领养了个白胖胖3 小时前
#和$符号使用场景 注意事项
java·后端·mybatis