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

相关推荐
千叶寻-42 分钟前
正则表达式
前端·javascript·后端·架构·正则表达式·node.js
小咕聊编程2 小时前
【含文档+源码】基于SpringBoot的过滤协同算法之网上服装商城设计与实现
java·spring boot·后端
追逐时光者8 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_8 小时前
敏捷开发流程-精简版
前端·后端
苏打水com9 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧10 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧10 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧10 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧10 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧10 小时前
Spring Cloud Gateway详解与应用实战
后端