关注:CodingTechWork
引言
在企业级应用中,数据权限控制是一个常见的需求。本文将通过一个完整的示例,展示如何使用自定义注解和AOP切面在Spring Boot项目中实现数据权限管控,以商品实例列表查询为例,根据用户角色动态过滤数据。同时,我们将提供完整的表结构和数据插入脚本,以便更好地理解和测试。
背景与需求
在一个电商平台中,商品实例数据需要根据用户角色进行权限控制。例如:
- 普通用户只能查看自己购买或收藏的商品实例。
- 供应商只能查看自己供应的商品实例。
- 管理员可以查看所有商品实例。
为了实现这种权限控制,我们需要:
- 定义一个自定义注解,用于标记需要进行数据权限管控的方法。
- 使用AOP切面拦截这些方法,并根据用户角色动态添加过滤条件。
- 在Mapper层动态拼接SQL语句,实现数据过滤。
- 提供完整的表结构和数据插入脚本,以便测试。
实现步骤
定义表结构
用户表(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切面,我们可以非常灵活地实现数据权限管控。这种方式不仅减少了重复代码,还提高了代码的可读性和可维护性。在实际项目中,