16 changed files with 984 additions and 91 deletions
@ -1,89 +0,0 @@ |
|||||
--测试表 |
|
||||
create table test_enf_demo |
|
||||
( |
|
||||
id bigint not null comment '主键' |
|
||||
primary key, |
|
||||
enf_text varchar(255) null comment '文本', |
|
||||
enf_content longtext null comment '长文本', |
|
||||
tenant_id varchar(20) default '000000' null comment '租户编号', |
|
||||
dept_id bigint null comment '部门id', |
|
||||
user_id bigint null comment '用户id', |
|
||||
order_num int default 0 null comment '排序号', |
|
||||
test_key varchar(255) null comment 'key键', |
|
||||
value varchar(255) null comment '值', |
|
||||
version int default 0 null comment '版本', |
|
||||
create_dept varchar(100) null comment '创建部门', |
|
||||
create_time datetime null comment '创建时间', |
|
||||
create_by bigint null comment '创建人', |
|
||||
update_time datetime null comment '更新时间', |
|
||||
update_by bigint null comment '更新人', |
|
||||
del_flag int default 0 null comment '删除标志' |
|
||||
) |
|
||||
comment 'enf测试单表'; |
|
||||
|
|
||||
|
|
||||
-- 为 test_enf_demo 表生成 10000 条测试数据 |
|
||||
-- 使用存储过程批量插入 |
|
||||
|
|
||||
DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; |
|
||||
|
|
||||
DELIMITER $$ |
|
||||
|
|
||||
CREATE PROCEDURE generate_test_enf_demo_data() |
|
||||
BEGIN |
|
||||
DECLARE i INT DEFAULT 1; |
|
||||
DECLARE batch_size INT DEFAULT 500; |
|
||||
DECLARE current_batch INT DEFAULT 0; |
|
||||
|
|
||||
-- 关闭自动提交,提升插入性能 |
|
||||
SET autocommit = 0; |
|
||||
|
|
||||
WHILE i <= 10000 DO |
|
||||
INSERT INTO test_enf_demo ( |
|
||||
id, enf_text, enf_content, tenant_id, dept_id, user_id, |
|
||||
order_num, test_key, value, version, |
|
||||
create_dept, create_time, create_by, update_time, update_by, del_flag |
|
||||
) VALUES ( |
|
||||
i, |
|
||||
CONCAT('文本_', i), |
|
||||
CONCAT('这是第', i, '条测试长文本内容,用于测试数据填充。'), |
|
||||
'000000', |
|
||||
100 + FLOOR(RAND() * 50), |
|
||||
1000 + FLOOR(RAND() * 500), |
|
||||
i, |
|
||||
CONCAT('KEY_', LPAD(i, 5, '0')), |
|
||||
CONCAT('VAL_', LPAD(i, 5, '0')), |
|
||||
0, |
|
||||
CONCAT('部门_', FLOOR(RAND() * 20)), |
|
||||
DATE_ADD('2024-01-01', INTERVAL FLOOR(RAND() * 730) DAY), |
|
||||
1, |
|
||||
NOW(), |
|
||||
1, |
|
||||
0 |
|
||||
); |
|
||||
|
|
||||
-- 每 500 条提交一次 |
|
||||
SET current_batch = current_batch + 1; |
|
||||
IF current_batch >= batch_size THEN |
|
||||
COMMIT; |
|
||||
SET current_batch = 0; |
|
||||
END IF; |
|
||||
|
|
||||
SET i = i + 1; |
|
||||
END WHILE; |
|
||||
|
|
||||
-- 提交剩余数据 |
|
||||
COMMIT; |
|
||||
SET autocommit = 1; |
|
||||
END$$ |
|
||||
|
|
||||
DELIMITER ; |
|
||||
|
|
||||
-- 执行存储过程 |
|
||||
CALL generate_test_enf_demo_data(); |
|
||||
|
|
||||
-- 清理存储过程 |
|
||||
DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; |
|
||||
|
|
||||
-- 验证数据量 |
|
||||
SELECT COUNT(*) AS total_count FROM test_enf_demo; |
|
||||
@ -0,0 +1,107 @@ |
|||||
|
--测试表 |
||||
|
create table test_enf_demo |
||||
|
( |
||||
|
id bigint not null comment '主键' |
||||
|
primary key, |
||||
|
enf_text varchar(255) null comment '文本', |
||||
|
enf_content longtext null comment '长文本', |
||||
|
tenant_id varchar(20) default '000000' null comment '租户编号', |
||||
|
dept_id bigint null comment '部门id', |
||||
|
user_id bigint null comment '用户id', |
||||
|
order_num int default 0 null comment '排序号', |
||||
|
test_key varchar(255) null comment 'key键', |
||||
|
value varchar(255) null comment '值', |
||||
|
version int default 0 null comment '版本', |
||||
|
create_dept varchar(100) null comment '创建部门', |
||||
|
bureau_dept_id varchar(100) null comment '执法部门ID', |
||||
|
create_time datetime null comment '创建时间', |
||||
|
create_by bigint null comment '创建人', |
||||
|
update_time datetime null comment '更新时间', |
||||
|
update_by bigint null comment '更新人', |
||||
|
del_flag int default 0 null comment '删除标志' |
||||
|
) comment 'enf测试单表'; |
||||
|
|
||||
|
|
||||
|
-- 为 test_enf_demo 表生成 10000 条测试数据 |
||||
|
-- 使用存储过程批量插入 |
||||
|
-- 为 test_enf_demo 表生成 100000 条测试数据 |
||||
|
-- bureau_dept_id 从 efcode_virtual_unit 表的 sys_dept_id 中随机选取 |
||||
|
|
||||
|
DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; |
||||
|
|
||||
|
DELIMITER $$ |
||||
|
|
||||
|
CREATE PROCEDURE generate_test_enf_demo_data() |
||||
|
BEGIN |
||||
|
DECLARE i INT DEFAULT 1; |
||||
|
DECLARE batch_size INT DEFAULT 1000; |
||||
|
DECLARE current_batch INT DEFAULT 0; |
||||
|
DECLARE v_dept_count INT DEFAULT 0; |
||||
|
|
||||
|
-- 创建临时表缓存 sys_dept_id 列表,避免每次子查询 |
||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_dept_ids; |
||||
|
CREATE TEMPORARY TABLE tmp_dept_ids ( |
||||
|
row_num INT AUTO_INCREMENT PRIMARY KEY, |
||||
|
sys_dept_id VARCHAR(100) |
||||
|
); |
||||
|
INSERT INTO tmp_dept_ids (sys_dept_id) |
||||
|
SELECT sys_dept_id FROM efcode_virtual_unit WHERE sys_dept_id IS NOT NULL; |
||||
|
|
||||
|
SELECT COUNT(*) INTO v_dept_count FROM tmp_dept_ids; |
||||
|
|
||||
|
-- 关闭自动提交,提升插入性能 |
||||
|
SET autocommit = 0; |
||||
|
|
||||
|
WHILE i <= 100000 DO |
||||
|
INSERT INTO test_enf_demo ( |
||||
|
id, enf_text, enf_content, tenant_id, dept_id, user_id, |
||||
|
order_num, test_key, value, version, |
||||
|
create_dept, bureau_dept_id, create_time, create_by, update_time, update_by, del_flag |
||||
|
) VALUES ( |
||||
|
i, |
||||
|
CONCAT('文本_', i), |
||||
|
CONCAT('这是第', i, '条测试长文本内容,用于测试数据填充。'), |
||||
|
'000000', |
||||
|
100 + FLOOR(RAND() * 50), |
||||
|
1000 + FLOOR(RAND() * 500), |
||||
|
i, |
||||
|
CONCAT('KEY_', LPAD(i, 5, '0')), |
||||
|
CONCAT('VAL_', LPAD(i, 5, '0')), |
||||
|
0, |
||||
|
CONCAT('部门_', FLOOR(RAND() * 20)), |
||||
|
(SELECT sys_dept_id FROM tmp_dept_ids WHERE row_num = 1 + FLOOR(RAND() * v_dept_count) LIMIT 1), |
||||
|
DATE_ADD('2024-01-01', INTERVAL FLOOR(RAND() * 730) DAY), |
||||
|
1, |
||||
|
NOW(), |
||||
|
1, |
||||
|
0 |
||||
|
); |
||||
|
|
||||
|
-- 每 1000 条提交一次 |
||||
|
SET current_batch = current_batch + 1; |
||||
|
IF current_batch >= batch_size THEN |
||||
|
COMMIT; |
||||
|
SET current_batch = 0; |
||||
|
END IF; |
||||
|
|
||||
|
SET i = i + 1; |
||||
|
END WHILE; |
||||
|
|
||||
|
-- 提交剩余数据 |
||||
|
COMMIT; |
||||
|
SET autocommit = 1; |
||||
|
|
||||
|
-- 清理临时表 |
||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_dept_ids; |
||||
|
END$$ |
||||
|
|
||||
|
DELIMITER ; |
||||
|
|
||||
|
-- 执行存储过程 |
||||
|
CALL generate_test_enf_demo_data(); |
||||
|
|
||||
|
-- 清理存储过程 |
||||
|
DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; |
||||
|
|
||||
|
-- 验证数据量 |
||||
|
SELECT COUNT(*) AS total_count FROM test_enf_demo; |
||||
@ -0,0 +1,49 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import java.lang.annotation.*; |
||||
|
|
||||
|
/** |
||||
|
* 执法码数据权限过滤注解。 |
||||
|
* <p> |
||||
|
* 标注在 Mapper 方法或 Mapper 类上,声明该查询需要按虚拟单位树进行数据过滤。 |
||||
|
* <p> |
||||
|
* 过滤规则: |
||||
|
* <ul> |
||||
|
* <li>超级管理员 → 全部放行,不追加过滤条件</li> |
||||
|
* <li>拥有 ViewAllDataRole 角色 → 全部放行</li> |
||||
|
* <li>拥有 ViewFilterDataRole 角色 → 根据用户所在部门在 efcode_virtual_unit 中的位置, |
||||
|
* 查找该节点及其子孙的 sys_dept_id,生成 IN 条件注入 SQL</li> |
||||
|
* <li>其他用户 → 追加 1=0 条件,无权访问任何数据</li> |
||||
|
* </ul> |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@Target({ElementType.METHOD, ElementType.TYPE}) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@Documented |
||||
|
public @interface EfcodeDataFilter { |
||||
|
|
||||
|
/** |
||||
|
* 是否包含统计节点(efcode_virtual_unit.vir_include_statistics = '是') |
||||
|
*/ |
||||
|
boolean includeStatistics() default true; |
||||
|
|
||||
|
/** |
||||
|
* 是否包含查询节点(efcode_virtual_unit.vir_include_search = '是') |
||||
|
*/ |
||||
|
boolean includeSearch() default true; |
||||
|
|
||||
|
/** |
||||
|
* 业务表的别名(如 SQL 中使用了别名,如 "t"、"e" 等)。 |
||||
|
* <p> |
||||
|
* 为空表示不使用别名,直接使用列名。 |
||||
|
*/ |
||||
|
String tableAlias() default ""; |
||||
|
|
||||
|
/** |
||||
|
* 业务表中用于匹配 sys_dept_id 的列名。 |
||||
|
* <p> |
||||
|
* 默认为 "bureau_dept_id"。 |
||||
|
*/ |
||||
|
String column() default "bureau_dept_id"; |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.aopalliance.intercept.MethodInterceptor; |
||||
|
import org.aopalliance.intercept.MethodInvocation; |
||||
|
|
||||
|
import java.lang.reflect.Method; |
||||
|
import java.lang.reflect.Proxy; |
||||
|
|
||||
|
/** |
||||
|
* EfcodeDataFilter 注解方法拦截器。 |
||||
|
* <p> |
||||
|
* 在 Mapper 方法执行前将注解信息存入 ThreadLocal,执行后清除。 |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class EfcodeDataFilterAdvice implements MethodInterceptor { |
||||
|
|
||||
|
@Override |
||||
|
public Object invoke(MethodInvocation invocation) throws Throwable { |
||||
|
Object target = invocation.getThis(); |
||||
|
Method method = invocation.getMethod(); |
||||
|
// 设置过滤注解
|
||||
|
EfcodeDataFilterHelper.setFilter(getAnnotation(target, method)); |
||||
|
try { |
||||
|
return invocation.proceed(); |
||||
|
} finally { |
||||
|
EfcodeDataFilterHelper.removeFilter(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取 EfcodeDataFilter 注解:优先方法级别,其次类级别 |
||||
|
*/ |
||||
|
private EfcodeDataFilter getAnnotation(Object target, Method method) { |
||||
|
EfcodeDataFilter annotation = method.getAnnotation(EfcodeDataFilter.class); |
||||
|
if (annotation != null) { |
||||
|
return annotation; |
||||
|
} |
||||
|
Class<?> targetClass = target.getClass(); |
||||
|
// JDK 动态代理(Mapper)需要获取接口上的注解
|
||||
|
if (Proxy.isProxyClass(targetClass)) { |
||||
|
targetClass = targetClass.getInterfaces()[0]; |
||||
|
} |
||||
|
return targetClass.getAnnotation(EfcodeDataFilter.class); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.config.BeanDefinition; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import org.springframework.context.annotation.Role; |
||||
|
|
||||
|
/** |
||||
|
* 执法码数据权限过滤配置。 |
||||
|
* <p> |
||||
|
* 注册 {@link EfcodeDataFilterInterceptor} 到 MyBatis-Plus 拦截器链, |
||||
|
* 并注册 {@link EfcodeDataFilterPointcutAdvisor} 用于 AOP 拦截。 |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@Configuration |
||||
|
public class EfcodeDataFilterConfig { |
||||
|
|
||||
|
/** |
||||
|
* 将执法码数据过滤拦截器追加到 MyBatis-Plus 拦截器链中 |
||||
|
*/ |
||||
|
@Autowired |
||||
|
public void configureEfcodeDataFilter(MybatisPlusInterceptor interceptor) { |
||||
|
interceptor.addInnerInterceptor(new EfcodeDataFilterInterceptor()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 执法码数据过滤切面注册器 |
||||
|
*/ |
||||
|
@Bean |
||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
||||
|
public EfcodeDataFilterPointcutAdvisor efcodeDataFilterPointcutAdvisor() { |
||||
|
return new EfcodeDataFilterPointcutAdvisor(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import lombok.AccessLevel; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
/** |
||||
|
* EfcodeDataFilter 注解上下文管理器。 |
||||
|
* <p> |
||||
|
* 通过 ThreadLocal 在 AOP 切面设置注解信息,在 MyBatis 拦截器中读取并使用。 |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE) |
||||
|
public class EfcodeDataFilterHelper { |
||||
|
|
||||
|
private static final ThreadLocal<EfcodeDataFilter> FILTER_CACHE = new ThreadLocal<>(); |
||||
|
|
||||
|
/** |
||||
|
* 设置当前线程的数据过滤注解 |
||||
|
*/ |
||||
|
public static void setFilter(EfcodeDataFilter filter) { |
||||
|
FILTER_CACHE.set(filter); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取当前线程的数据过滤注解 |
||||
|
*/ |
||||
|
public static EfcodeDataFilter getFilter() { |
||||
|
return FILTER_CACHE.get(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除当前线程的数据过滤注解 |
||||
|
*/ |
||||
|
public static void removeFilter() { |
||||
|
FILTER_CACHE.remove(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.baomidou.mybatisplus.core.toolkit.PluginUtils; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; |
||||
|
import com.zdxt.enforcementcode.common.util.DataFilterUtil; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import net.sf.jsqlparser.JSQLParserException; |
||||
|
import net.sf.jsqlparser.expression.Expression; |
||||
|
import net.sf.jsqlparser.expression.operators.conditional.AndExpression; |
||||
|
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; |
||||
|
import net.sf.jsqlparser.parser.CCJSqlParserUtil; |
||||
|
import net.sf.jsqlparser.statement.select.PlainSelect; |
||||
|
import net.sf.jsqlparser.statement.select.Select; |
||||
|
import org.apache.ibatis.executor.Executor; |
||||
|
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.dromara.common.core.utils.StringUtils; |
||||
|
import org.dromara.common.satoken.utils.LoginHelper; |
||||
|
|
||||
|
import java.sql.SQLException; |
||||
|
|
||||
|
/** |
||||
|
* 执法码数据权限 MyBatis 拦截器。 |
||||
|
* <p> |
||||
|
* 在 SQL 执行前,根据 {@link EfcodeDataFilter} 注解配置和用户角色, |
||||
|
* 自动向 SELECT 语句的 WHERE 子句追加基于 bureau_dept_id 的数据过滤条件。 |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class EfcodeDataFilterInterceptor implements InnerInterceptor { |
||||
|
|
||||
|
@Override |
||||
|
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, |
||||
|
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { |
||||
|
// 从 ThreadLocal 获取注解
|
||||
|
EfcodeDataFilter filter = EfcodeDataFilterHelper.getFilter(); |
||||
|
if (filter == null) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 构建过滤 SQL 片段
|
||||
|
String filterSql = buildFilterSql(filter); |
||||
|
if (filterSql == null) { |
||||
|
// null 表示全部放行,不追加条件
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 解析并修改 SQL
|
||||
|
try { |
||||
|
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); |
||||
|
String originalSql = mpBs.sql(); |
||||
|
String newSql = appendWhere(originalSql, filterSql); |
||||
|
mpBs.sql(newSql); |
||||
|
} catch (Exception e) { |
||||
|
log.error("EfcodeDataFilter SQL注入异常: {}", e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据注解配置和用户角色,构建过滤 SQL 片段。 |
||||
|
* <p> |
||||
|
* 使用 SQL 子查询替代 Java 端拼接 IN 列表,避免部门数量过多导致 SQL 超长。 |
||||
|
* |
||||
|
* @param filter 注解 |
||||
|
* @return SQL 片段;null 表示全部放行;非 null 字符串追加到 WHERE |
||||
|
*/ |
||||
|
private String buildFilterSql(EfcodeDataFilter filter) { |
||||
|
// 超管或 ViewAllDataRole → 不过滤
|
||||
|
if (DataFilterUtil.isFullAccess()) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// ViewFilterDataRole → 构建子查询条件
|
||||
|
if (DataFilterUtil.needFilter()) { |
||||
|
String userDeptId = LoginHelper.getDeptId(); |
||||
|
if (StrUtil.isBlank(userDeptId)) { |
||||
|
return "1 = 0"; |
||||
|
} |
||||
|
|
||||
|
// 构建列名(支持别名)
|
||||
|
String columnRef = buildColumnRef(filter.tableAlias(), filter.column()); |
||||
|
String escapedDeptId = escapeSql(userDeptId); |
||||
|
|
||||
|
// 使用子查询:通过 userDeptId 找到 virId,再通过 FIND_IN_SET 查子孙节点的 sys_dept_id
|
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
sb.append(columnRef).append(" IN ("); |
||||
|
sb.append("SELECT eu.sys_dept_id FROM efcode_virtual_unit eu"); |
||||
|
sb.append(" WHERE FIND_IN_SET("); |
||||
|
sb.append("(SELECT eu2.vir_id FROM efcode_virtual_unit eu2 WHERE eu2.sys_dept_id = '").append(escapedDeptId).append("' LIMIT 1)"); |
||||
|
sb.append(", eu.vir_ancestors)"); |
||||
|
sb.append(" AND eu.sys_dept_id IS NOT NULL AND eu.sys_dept_id != ''"); |
||||
|
|
||||
|
// includeSearch / includeStatistics 过滤条件
|
||||
|
if (filter.includeSearch() || filter.includeStatistics()) { |
||||
|
sb.append(" AND (1=0"); |
||||
|
if (filter.includeSearch()) { |
||||
|
sb.append(" OR eu.vir_include_search = '是'"); |
||||
|
} |
||||
|
if (filter.includeStatistics()) { |
||||
|
sb.append(" OR eu.vir_include_statistics = '是'"); |
||||
|
} |
||||
|
sb.append(")"); |
||||
|
} |
||||
|
sb.append(")"); |
||||
|
|
||||
|
// 补充自身部门(子查询通过 FIND_IN_SET 只返回子孙,不含自身节点)
|
||||
|
sb.append(" OR ").append(columnRef).append(" = '").append(escapedDeptId).append("'"); |
||||
|
|
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
// 无权访问
|
||||
|
return "1 = 0"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建列引用(支持表别名) |
||||
|
*/ |
||||
|
private String buildColumnRef(String tableAlias, String column) { |
||||
|
if (StringUtils.isNotBlank(tableAlias)) { |
||||
|
return tableAlias + "." + column; |
||||
|
} |
||||
|
return column; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将过滤条件追加到 SQL 的 WHERE 子句中 |
||||
|
*/ |
||||
|
private String appendWhere(String originalSql, String filterSql) throws JSQLParserException { |
||||
|
Select select = (Select) CCJSqlParserUtil.parse(originalSql); |
||||
|
if (select instanceof PlainSelect plainSelect) { |
||||
|
Expression filterExpression = CCJSqlParserUtil.parseCondExpression(filterSql); |
||||
|
// 用括号包裹过滤条件
|
||||
|
ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(filterExpression); |
||||
|
Expression where = plainSelect.getWhere(); |
||||
|
if (where != null) { |
||||
|
plainSelect.setWhere(new AndExpression(where, parenthesis)); |
||||
|
} else { |
||||
|
plainSelect.setWhere(parenthesis); |
||||
|
} |
||||
|
return plainSelect.toString(); |
||||
|
} |
||||
|
// 非简单 SELECT(如 UNION),尝试在末尾追加
|
||||
|
log.warn("EfcodeDataFilter: 无法解析为PlainSelect,跳过SQL过滤: {}", originalSql); |
||||
|
return originalSql; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 简单的 SQL 注入防护(转义单引号) |
||||
|
*/ |
||||
|
private String escapeSql(String value) { |
||||
|
if (value == null) { |
||||
|
return ""; |
||||
|
} |
||||
|
return value.replace("'", "''"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import org.springframework.aop.support.StaticMethodMatcherPointcut; |
||||
|
|
||||
|
import java.lang.reflect.Method; |
||||
|
import java.lang.reflect.Proxy; |
||||
|
|
||||
|
/** |
||||
|
* EfcodeDataFilter 注解切入点匹配器。 |
||||
|
* <p> |
||||
|
* 匹配方法或类上标注了 {@link EfcodeDataFilter} 注解的目标。 |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@SuppressWarnings("all") |
||||
|
public class EfcodeDataFilterPointcut extends StaticMethodMatcherPointcut { |
||||
|
|
||||
|
@Override |
||||
|
public boolean matches(Method method, Class<?> targetClass) { |
||||
|
// 优先匹配方法上的注解
|
||||
|
if (method.isAnnotationPresent(EfcodeDataFilter.class)) { |
||||
|
return true; |
||||
|
} |
||||
|
// MyBatis 的 Mapper 通过 JDK 动态代理实现,需要检查接口
|
||||
|
Class<?> targetClassRef = targetClass; |
||||
|
if (Proxy.isProxyClass(targetClassRef)) { |
||||
|
targetClassRef = targetClass.getInterfaces()[0]; |
||||
|
} |
||||
|
return targetClassRef.isAnnotationPresent(EfcodeDataFilter.class); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
package com.zdxt.enforcementcode.common.annotation; |
||||
|
|
||||
|
import org.aopalliance.aop.Advice; |
||||
|
import org.springframework.aop.Pointcut; |
||||
|
import org.springframework.aop.support.AbstractPointcutAdvisor; |
||||
|
|
||||
|
/** |
||||
|
* EfcodeDataFilter 切面注册器。 |
||||
|
* <p> |
||||
|
* 将 {@link EfcodeDataFilterPointcut} 和 {@link EfcodeDataFilterAdvice} 组合注册为 Spring AOP Advisor。 |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
public class EfcodeDataFilterPointcutAdvisor extends AbstractPointcutAdvisor { |
||||
|
|
||||
|
private final Advice advice; |
||||
|
private final Pointcut pointcut; |
||||
|
|
||||
|
public EfcodeDataFilterPointcutAdvisor() { |
||||
|
this.advice = new EfcodeDataFilterAdvice(); |
||||
|
this.pointcut = new EfcodeDataFilterPointcut(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Pointcut getPointcut() { |
||||
|
return this.pointcut; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Advice getAdvice() { |
||||
|
return this.advice; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,316 @@ |
|||||
|
package com.zdxt.service.test; |
||||
|
|
||||
|
import cn.dev33.satoken.context.mock.SaTokenContextMockUtil; |
||||
|
import cn.dev33.satoken.stp.StpUtil; |
||||
|
import cn.dev33.satoken.stp.parameter.SaLoginParameter; |
||||
|
import com.zdxt.enforcementcode.domain.bo.TestEnfDemoBo; |
||||
|
import com.zdxt.enforcementcode.domain.vo.TestEnfDemoVo; |
||||
|
import com.zdxt.enforcementcode.service.ITestEnfDemoService; |
||||
|
import org.dromara.ZdxtAdminApplication; |
||||
|
import org.dromara.common.core.domain.model.LoginUser; |
||||
|
import org.dromara.common.mybatis.core.page.PageQuery; |
||||
|
import org.dromara.common.mybatis.core.page.TableDataInfo; |
||||
|
import org.dromara.common.satoken.utils.LoginHelper; |
||||
|
import org.junit.jupiter.api.*; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.mock.web.MockHttpServletRequest; |
||||
|
import org.springframework.test.context.ActiveProfiles; |
||||
|
import org.springframework.web.context.request.RequestContextHolder; |
||||
|
import org.springframework.web.context.request.ServletRequestAttributes; |
||||
|
|
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.*; |
||||
|
|
||||
|
/** |
||||
|
* TestEnfDemoServiceImpl 单元测试 |
||||
|
* <p> |
||||
|
* 验证 @EfcodeDataFilter 注解在 Service 层和 Mapper 层的数据过滤效果。 |
||||
|
* </p> |
||||
|
* |
||||
|
* @author zrb |
||||
|
*/ |
||||
|
@SpringBootTest(classes = ZdxtAdminApplication.class) |
||||
|
@ActiveProfiles({"dev", "dev-cust-zrb"}) |
||||
|
@AutoConfigureMockMvc |
||||
|
@DisplayName("TestEnfDemoService 数据过滤注解测试") |
||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) |
||||
|
public class TestEnfDemoServiceTest { |
||||
|
|
||||
|
@Autowired |
||||
|
private ITestEnfDemoService testEnfDemoService; |
||||
|
|
||||
|
private static final String TEST_CLIENT_ID = "e5cd7e4891bf95d1d19206ce24a7b32e"; |
||||
|
private static final String TENANT_ID = "000000"; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
SaTokenContextMockUtil.setMockContext(); |
||||
|
MockHttpServletRequest mockRequest = new MockHttpServletRequest(); |
||||
|
mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); |
||||
|
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest)); |
||||
|
} |
||||
|
|
||||
|
@AfterEach |
||||
|
public void tearDown() { |
||||
|
try { |
||||
|
StpUtil.logout(); |
||||
|
} catch (Exception ignored) { |
||||
|
} |
||||
|
RequestContextHolder.resetRequestAttributes(); |
||||
|
SaTokenContextMockUtil.clearContext(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 模拟超级管理员登录(全部放行) |
||||
|
*/ |
||||
|
private void loginAsSuperAdmin() { |
||||
|
LoginUser loginUser = new LoginUser(); |
||||
|
loginUser.setUserId(1L); |
||||
|
loginUser.setUserType("sys_user"); |
||||
|
loginUser.setTenantId(TENANT_ID); |
||||
|
loginUser.setUsername("admin"); |
||||
|
loginUser.setNickname("超级管理员"); |
||||
|
loginUser.setDeptId("103"); |
||||
|
loginUser.setDeptName("研发部门"); |
||||
|
loginUser.setRolePermission(new HashSet<>(Set.of("admin"))); |
||||
|
loginUser.setMenuPermission(new HashSet<>(Set.of("*:*:*"))); |
||||
|
|
||||
|
SaLoginParameter param = new SaLoginParameter(); |
||||
|
param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); |
||||
|
LoginHelper.login(loginUser, param); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 模拟 ViewAllDataRole 用户登录(全部放行) |
||||
|
*/ |
||||
|
private void loginAsViewAllDataUser() { |
||||
|
LoginUser loginUser = new LoginUser(); |
||||
|
loginUser.setUserId(2L); |
||||
|
loginUser.setUserType("sys_user"); |
||||
|
loginUser.setTenantId(TENANT_ID); |
||||
|
loginUser.setUsername("view_all_user"); |
||||
|
loginUser.setNickname("全数据查看用户"); |
||||
|
loginUser.setDeptId("103"); |
||||
|
loginUser.setDeptName("研发部门"); |
||||
|
loginUser.setRolePermission(new HashSet<>(Set.of("ViewAllDataRole"))); |
||||
|
loginUser.setMenuPermission(new HashSet<>()); |
||||
|
|
||||
|
SaLoginParameter param = new SaLoginParameter(); |
||||
|
param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); |
||||
|
LoginHelper.login(loginUser, param); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 模拟 ViewFilterDataRole 用户登录(需要数据过滤) |
||||
|
*/ |
||||
|
private void loginAsFilterDataUser(String deptId) { |
||||
|
LoginUser loginUser = new LoginUser(); |
||||
|
loginUser.setUserId(3L); |
||||
|
loginUser.setUserType("sys_user"); |
||||
|
loginUser.setTenantId(TENANT_ID); |
||||
|
loginUser.setUsername("filter_user"); |
||||
|
loginUser.setNickname("过滤数据用户"); |
||||
|
loginUser.setDeptId(deptId); |
||||
|
loginUser.setDeptName("过滤部门"); |
||||
|
loginUser.setRolePermission(new HashSet<>(Set.of("ViewFilterDataRole"))); |
||||
|
loginUser.setMenuPermission(new HashSet<>()); |
||||
|
|
||||
|
SaLoginParameter param = new SaLoginParameter(); |
||||
|
param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); |
||||
|
LoginHelper.login(loginUser, param); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 模拟无任何数据权限角色的用户登录(无权访问) |
||||
|
*/ |
||||
|
private void loginAsNoAccessUser() { |
||||
|
LoginUser loginUser = new LoginUser(); |
||||
|
loginUser.setUserId(4L); |
||||
|
loginUser.setUserType("sys_user"); |
||||
|
loginUser.setTenantId(TENANT_ID); |
||||
|
loginUser.setUsername("no_access_user"); |
||||
|
loginUser.setNickname("无权用户"); |
||||
|
loginUser.setDeptId("103"); |
||||
|
loginUser.setDeptName("研发部门"); |
||||
|
loginUser.setRolePermission(new HashSet<>(Set.of("common"))); |
||||
|
loginUser.setMenuPermission(new HashSet<>()); |
||||
|
|
||||
|
SaLoginParameter param = new SaLoginParameter(); |
||||
|
param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); |
||||
|
LoginHelper.login(loginUser, param); |
||||
|
} |
||||
|
|
||||
|
// ======================== queryPageList 测试(Service层注解,覆盖selectVoPage) ========================
|
||||
|
|
||||
|
@Test |
||||
|
@Order(1) |
||||
|
@DisplayName("超级管理员 - queryPageList - 全部放行,可查询到数据") |
||||
|
public void testQueryPageList_SuperAdmin() { |
||||
|
loginAsSuperAdmin(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.queryPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[超管] queryPageList 总数: " + result.getTotal()); |
||||
|
assertTrue(result.getTotal() > 0, "超级管理员应能查询到数据"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@Order(2) |
||||
|
@DisplayName("ViewAllDataRole用户 - queryPageList - 全部放行") |
||||
|
public void testQueryPageList_ViewAllData() { |
||||
|
loginAsViewAllDataUser(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.queryPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[ViewAllData] queryPageList 总数: " + result.getTotal()); |
||||
|
assertTrue(result.getTotal() > 0, "ViewAllDataRole用户应能查询到数据"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@Order(3) |
||||
|
@DisplayName("ViewFilterDataRole用户 - queryPageList - 数据过滤生效") |
||||
|
public void testQueryPageList_FilterData() { |
||||
|
// 使用一个在虚拟单位树中存在的部门ID
|
||||
|
loginAsFilterDataUser("20210922211852148-9BC0-86CB268FF"); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.queryPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[ViewFilterData] queryPageList 总数: " + result.getTotal()); |
||||
|
// 过滤后数据量应小于等于全量
|
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@Order(4) |
||||
|
@DisplayName("无权用户 - queryPageList - 无法查询到任何数据") |
||||
|
public void testQueryPageList_NoAccess() { |
||||
|
loginAsNoAccessUser(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.queryPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[无权用户] queryPageList 总数: " + result.getTotal()); |
||||
|
assertEquals(0, result.getTotal(), "无权用户不应查询到任何数据"); |
||||
|
} |
||||
|
|
||||
|
// ======================== queryList 测试(Service层注解,覆盖selectVoList) ========================
|
||||
|
|
||||
|
@Test |
||||
|
@Order(5) |
||||
|
@DisplayName("超级管理员 - queryList - 全部放行") |
||||
|
public void testQueryList_SuperAdmin() { |
||||
|
loginAsSuperAdmin(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
|
||||
|
List<TestEnfDemoVo> result = testEnfDemoService.queryList(bo); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[超管] queryList 数量: " + result.size()); |
||||
|
assertFalse(result.isEmpty(), "超级管理员应能查询到数据"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@Order(6) |
||||
|
@DisplayName("无权用户 - queryList - 查询结果为空") |
||||
|
public void testQueryList_NoAccess() { |
||||
|
loginAsNoAccessUser(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
|
||||
|
List<TestEnfDemoVo> result = testEnfDemoService.queryList(bo); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[无权用户] queryList 数量: " + result.size()); |
||||
|
assertTrue(result.isEmpty(), "无权用户不应查询到任何数据"); |
||||
|
} |
||||
|
|
||||
|
// ======================== manualPageList 测试(Service层注解,覆盖多次Mapper调用) ========================
|
||||
|
|
||||
|
@Test |
||||
|
@Order(7) |
||||
|
@DisplayName("超级管理员 - manualPageList - 全部放行") |
||||
|
public void testManualPageList_SuperAdmin() { |
||||
|
loginAsSuperAdmin(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.manualPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[超管] manualPageList 总数: " + result.getTotal()); |
||||
|
assertTrue(result.getTotal() > 0, "超级管理员应能查询到数据"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@Order(8) |
||||
|
@DisplayName("无权用户 - manualPageList - 无法查询到任何数据") |
||||
|
public void testManualPageList_NoAccess() { |
||||
|
loginAsNoAccessUser(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.manualPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[无权用户] manualPageList 总数: " + result.getTotal()); |
||||
|
assertEquals(0, result.getTotal(), "无权用户不应查询到任何数据"); |
||||
|
} |
||||
|
|
||||
|
// ======================== customPageList 测试(Mapper层注解) ========================
|
||||
|
|
||||
|
@Test |
||||
|
@Order(9) |
||||
|
@DisplayName("超级管理员 - customPageList(Mapper注解) - 全部放行") |
||||
|
public void testCustomPageList_SuperAdmin() { |
||||
|
loginAsSuperAdmin(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.customPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[超管] customPageList 总数: " + result.getTotal()); |
||||
|
assertTrue(result.getTotal() > 0, "超级管理员应能查询到数据"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@Order(10) |
||||
|
@DisplayName("无权用户 - customPageList(Mapper注解) - 无法查询到任何数据") |
||||
|
public void testCustomPageList_NoAccess() { |
||||
|
loginAsNoAccessUser(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
PageQuery pageQuery = new PageQuery(10, 1); |
||||
|
|
||||
|
TableDataInfo<TestEnfDemoVo> result = testEnfDemoService.customPageList(bo, pageQuery); |
||||
|
assertNotNull(result); |
||||
|
System.out.println("[无权用户] customPageList 总数: " + result.getTotal()); |
||||
|
assertEquals(0, result.getTotal(), "无权用户不应查询到任何数据"); |
||||
|
} |
||||
|
|
||||
|
// ======================== 对比验证:超管 vs 过滤用户数据量 ========================
|
||||
|
|
||||
|
@Test |
||||
|
@Order(11) |
||||
|
@DisplayName("数据量对比 - 超管全量 vs ViewFilterDataRole过滤量") |
||||
|
public void testDataComparison() { |
||||
|
// 先以超管身份查全量
|
||||
|
loginAsSuperAdmin(); |
||||
|
TestEnfDemoBo bo = new TestEnfDemoBo(); |
||||
|
List<TestEnfDemoVo> allData = testEnfDemoService.queryList(bo); |
||||
|
long allCount = allData.size(); |
||||
|
StpUtil.logout(); |
||||
|
|
||||
|
// 再以过滤用户身份查
|
||||
|
loginAsFilterDataUser("20210922211852148-9BC0-86CB268FF"); |
||||
|
List<TestEnfDemoVo> filteredData = testEnfDemoService.queryList(bo); |
||||
|
long filteredCount = filteredData.size(); |
||||
|
|
||||
|
System.out.println("[对比] 超管全量: " + allCount + ", 过滤用户: " + filteredCount); |
||||
|
assertTrue(filteredCount <= allCount, "过滤后数据量应小于等于全量数据"); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue