Springboot管理系统数据权限过滤(四)——mybatis数据权限插件
上一章,Springboot管理系统数据权限过滤(三)——0业务入侵实现部门数据权限过滤数据权限实现的思路和代码实现已经了解。本节在此基础上实现支持mybatis框架的通用数据过滤插件。
实现目标:
- 业务系统引入cr-datapermission.jar插件包;
- 通过默认数据权限规则可实现多种数据权限过滤;(ruoyi的部门权限、项目、租户过滤等)
上一章示例,使用的是mytatisplus数据持久框架,使用是的mybatisplus拦截器,本章是基于mybatis持久化框,所以仅拦截器实现上有一点点差异,但我们也是可以引入mybatisplus的依赖包,但仅使用它的对SQL的处理上,这样可以省很多事。除了这一点,其他的代码都可以沿用上一章的内容。
一、项目结构:
该项目初衷是把数据权限模板做成 自动配置 插件,对springboot版本有要求。但在springboot版本不支持的情况下,也可以通过配置创建相应bean也可以实现权限管理。
二、代码实现
2.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- <version>2.0.0.RELEASE</version>-->
<version>2.7.14</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.chengrui.tool</groupId>
<artifactId>cr-datapermission</artifactId>
<version>1.0.7</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring.boot.version>2.7.14</spring.boot.version>
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<spring.boot.version>2.7.14</spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
<!-- <version>2.0.0.RELEASE</version>-->
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.3</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
<optional>true</optional>
</dependency>
<!-- 实现对 MyBatis 的自动化配置 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2.2 DataPermission.java
没有任何内容的注解定义,可以定义相关字段,以实现更精细的数据过滤,支持更多的可配置性。
package com.luo.chengrui.datapermission.annotation;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import java.lang.annotation.*;
/**
* 数据权限过滤注解
*
* @author luodz
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataPermission
{
}
2.3DataPermissionInterceptorConfiguration.java
这是自动配置,仅配置了注解拦截器。只要引用了该jar包则会自动配置DataPermissionAnnotationAdvisor Bean。即可拦截还有@DataPermission注解的方法了。
package com.luo.chengrui.datapermission.config;
import com.luo.chengrui.datapermission.interceptor.DataPermissionAnnotationAdvisor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* 配置mybatisPlusInterceptor
*/
@AutoConfiguration
public class DataPermissionInterceptorConfiguration {
/**
* 权限注解拦截器。
*
* @return
*/
@Bean
@Role(2)
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
return new DataPermissionAnnotationAdvisor();
}
}
2.4 DataPermissionAnnotationAdvisor.java
spring Advisor用来配置拦截点和拦截器的实现类。
package com.luo.chengrui.datapermission.interceptor;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
/**
* {@link DataPermission} 注解的 Advisor 实现类
*/
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
private final Advice advice;
private final Pointcut pointcut;
@Override
public Advice getAdvice() {
return advice;
}
@Override
public Pointcut getPointcut() {
return pointcut;
}
public DataPermissionAnnotationAdvisor() {
this.advice = new DataScopeAnnotationInterceptor();
this.pointcut = this.buildPointcut();
}
protected Pointcut buildPointcut() {
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
return new ComposablePointcut(classPointcut).union(methodPointcut);
}
}
2.5 DataPermissionAnnotationInterceptor.java
当方法体上有DataPermission注解时,方法执行前先执行该拦截器,解析出DataPermission的对象,并存放到线程变量中DataPermissionContextHolder。当方法执行完时,清除线程变量数据。
package com.luo.chengrui.datapermission.interceptor;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodClassKey;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* {@link DataPermission} 注解的拦截器
* 1. 在执行方法前,将 @DataPermission 注解入栈
* 2. 在执行方法后,将 @DataPermission 注解出栈
*
* @author luodz
*/
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
/**
* DataPermission 空对象,用于方法无 {@link import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;} 注解时,使用 DATA_PERMISSION_NULL 进行占位
*/
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
public Map<MethodClassKey, DataPermission> getDataPermissionCache() {
return dataPermissionCache;
}
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Logger log = LoggerFactory.getLogger(DataPermissionAnnotationInterceptor.class);
log.debug("DataPermissionAnnotationInterceptor 拦截器:" + methodInvocation.getMethod().getName());
// 入栈
DataPermission dataPermission = this.findAnnotation(methodInvocation);
if (dataPermission != null) {
DataPermissionContextHolder.add(dataPermission);
}
try {
// 执行逻辑
return methodInvocation.proceed();
} finally {
// 出栈
if (dataPermission != null) {
DataPermissionContextHolder.remove();
}
}
}
private DataPermission findAnnotation(MethodInvocation methodInvocation) {
// 1. 从缓存中获取
Method method = methodInvocation.getMethod();
Object targetObject = methodInvocation.getThis();
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
if (dataPermission != null) {
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
}
// 2.1 从方法中获取
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
// 2.2 从类上获取
if (dataPermission == null) {
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
}
// 2.3 添加到缓存中
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
return dataPermission;
}
}
2.6 DataPermissionContextHolder.java
用来缓存DataPermission权限注解,注解拦截器将DataPermission对象存入线程变量中,当在执行SQL拦截器时,从线程变量中获取DataPermission对象,如果能获取到,则进行数据过滤,如果没有获取到,则不做数据过滤。
package com.luo.chengrui.datapermission.interceptor;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import java.util.LinkedList;
import java.util.List;
/**
* {@link DataPermission} 注解的 Context 上下文
* 将方法上的注解对象设置到 线程变量里面,在SQL执行拦截器中获取注解对象,根据内容生成相应的权限。
* 告诉SQL执行{DataPermissionSqlParserInterceptor}拦截器,如此方法需要添加权限。
*/
public class DataPermissionContextHolder {
/**
* 使用 List 的原因,可能存在方法的嵌套调用
*/
private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
ThreadLocal.withInitial(LinkedList::new);
/**
* 获得当前的 DataPermission 注解
*
* @return DataPermission 注解
*/
public static DataPermission get() {
return DATA_PERMISSIONS.get().peekLast();
}
/**
* 入栈 DataPermission 注解
*
* @param dataPermission DataPermission 注解
*/
public static void add(DataPermission dataPermission) {
DATA_PERMISSIONS.get().addLast(dataPermission);
}
/**
* 出栈 DataPermission 注解
*
* @return DataPermission 注解
*/
public static DataPermission remove() {
DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
// 无元素时,清空 ThreadLocal
if (DATA_PERMISSIONS.get().isEmpty()) {
DATA_PERMISSIONS.remove();
}
return dataPermission;
}
/**
* 获得所有 DataPermission
*
* @return DataPermission 队列
*/
public static List<DataPermission> getAll() {
return DATA_PERMISSIONS.get();
}
/**
* 清空上下文
* <p>
* 目前仅仅用于单测
*/
public static void clear() {
DATA_PERMISSIONS.remove();
}
}
2.7 DataPermissionSqlParserInterceptor.java
这个是Mybatis拦截器的实现,
- JsqlParserSupport:是mybatisplus的sql解析类,方便sql解析,别无他用;
- Interceptor :是mybatis拦截器接口。
拦截器入口,实现了Interceptor接口中的方法:
@Override
public Object intercept(Invocation invocation) throws Throwable {
}
DataPermissionSqlParserInterceptor.java 的完整代码
package com.luo.chengrui.datapermission.interceptor;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.NotExpression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.sql.Connection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
* 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法
* 主要是在执行SQL前拦截器,在执行之前可重写SQL
*
* @author yudao,luodz
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DataPermissionSqlParserInterceptor extends JsqlParserSupport implements Interceptor {
private Logger logger = LoggerFactory.getLogger(DataPermissionSqlParserInterceptor.class);
private static final String MYSQL_ESCAPE_CHARACTER = "`";
private final List<DataPermissionRule> dataPermissionRule;
public DataPermissionSqlParserInterceptor(List<DataPermissionRule> dataPermissionRule) {
this.dataPermissionRule = dataPermissionRule;
}
private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
@Override
public Object intercept(Invocation invocation) throws Throwable {
//线程变量中获取权限注解。
// 根据业务场景要求,决定是否依赖于方法上权限注解。
DataPermission saDataPermission = DataPermissionContextHolder.get();
if (saDataPermission == null) {
// 方法上未设置权限注解时,不处理SQL
return invocation.proceed();
}
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
if (target instanceof Executor) {
Executor executor = (Executor) target;
Object parameter = args[1];
boolean isUpdate = args.length == 2;
MappedStatement ms = (MappedStatement) args[0];
if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
} else {
boundSql = (BoundSql) args[5];
}
this.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
} else {
StatementHandler sh = (StatementHandler) target;
if (null == args) {
} else {
Connection connections = (Connection) args[0];
Integer transactionTimeout = (Integer) args[1];
this.beforePrepare(sh, connections, transactionTimeout);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return !(target instanceof Executor) && !(target instanceof StatementHandler) ? target : Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 获取配置文件中的属性值
}
// SELECT 场景
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 获得 Mapper 对应的数据权限的规则
if (mappedStatementCache.noRewritable(ms, dataPermissionRule)) { // 如果无需重写,则跳过
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
try {
// 初始化上下文
ContextHolder.init(dataPermissionRule);
// 处理 SQL
mpBs.sql(parserSingle(mpBs.sql(), null));
} finally {
// 添加是否需要重写的缓存
addMappedStatementCache(ms);
// 清空上下文
ContextHolder.clear();
}
}
// 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限)
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
// 获得 Mapper 对应的数据权限的规则
if (mappedStatementCache.noRewritable(ms, dataPermissionRule)) { // 如果无需重写,则跳过
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
try {
// 初始化上下文
ContextHolder.init(dataPermissionRule);
// 处理 SQL
mpBs.sql(parserMulti(mpBs.sql(), null));
} finally {
// 添加是否需要重写的缓存
addMappedStatementCache(ms);
// 清空上下文
ContextHolder.clear();
}
}
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
processSelectBody(select.getSelectBody());
List<WithItem> withItemsList = select.getWithItemsList();
if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(this::processSelectBody);
}
}
/**
* update 语句处理
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
final Table table = update.getTable();
update.setWhere(this.builderExpression(update.getWhere(), table));
}
/**
* delete 语句处理
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
}
// ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========
protected void processSelectBody(SelectBody selectBody) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
processPlainSelect((PlainSelect) selectBody);
} else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
processSelectBody(withItem.getSubSelect().getSelectBody());
} else {
SetOperationList operationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = operationList.getSelects();
if (!CollectionUtils.isEmpty(selectBodyList)) {
selectBodyList.forEach(this::processSelectBody);
}
}
}
/**
* 处理 PlainSelect
*/
protected void processPlainSelect(PlainSelect plainSelect) {
//#3087 github
List<SelectItem> selectItems = plainSelect.getSelectItems();
if (!CollectionUtils.isEmpty(selectItems)) {
selectItems.forEach(this::processSelectItem);
}
// 处理 where 中的子查询
Expression where = plainSelect.getWhere();
processWhereSubSelect(where);
// 处理 fromItem
FromItem fromItem = plainSelect.getFromItem();
List<Table> list = processFromItem(fromItem);
List<Table> mainTables = new ArrayList<>(list);
// 处理 join
List<Join> joins = plainSelect.getJoins();
if (!CollectionUtils.isEmpty(joins)) {
mainTables = processJoins(mainTables, joins);
}
// 当有 mainTable 时,进行 where 条件追加
if (!CollectionUtils.isEmpty(mainTables)) {
plainSelect.setWhere(builderExpression(where, mainTables));
}
}
private List<Table> processFromItem(FromItem fromItem) {
// 处理括号括起来的表达式
while (fromItem instanceof ParenthesisFromItem) {
fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
}
List<Table> mainTables = new ArrayList<>();
// 无 join 时的处理逻辑
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
mainTables.add(fromTable);
} else if (fromItem instanceof SubJoin) {
// SubJoin 类型则还需要添加上 where 条件
List<Table> tables = processSubJoin((SubJoin) fromItem);
mainTables.addAll(tables);
} else {
// 处理下 fromItem
processOtherFromItem(fromItem);
}
return mainTables;
}
/**
* 处理where条件内的子查询
* <p>
* 支持如下:
* 1. in
* 2. =
* 3. >
* 4. <
* 5. >=
* 6. <=
* 7. <>
* 8. EXISTS
* 9. NOT EXISTS
* <p>
* 前提条件:
* 1. 子查询必须放在小括号中
* 2. 子查询一般放在比较操作符的右边
*
* @param where where 条件
*/
protected void processWhereSubSelect(Expression where) {
if (where == null) {
return;
}
if (where instanceof FromItem) {
processOtherFromItem((FromItem) where);
return;
}
if (where.toString().indexOf("SELECT") > 0) {
// 有子查询
if (where instanceof net.sf.jsqlparser.expression.BinaryExpression) {
// 比较符号 , and , or , 等等
net.sf.jsqlparser.expression.BinaryExpression expression = (net.sf.jsqlparser.expression.BinaryExpression) where;
processWhereSubSelect(expression.getLeftExpression());
processWhereSubSelect(expression.getRightExpression());
} else if (where instanceof InExpression) {
// in
InExpression expression = (InExpression) where;
Expression inExpression = expression.getRightExpression();
if (inExpression instanceof SubSelect) {
processSelectBody(((SubSelect) inExpression).getSelectBody());
}
} else if (where instanceof ExistsExpression) {
// exists
ExistsExpression expression = (ExistsExpression) where;
processWhereSubSelect(expression.getRightExpression());
} else if (where instanceof NotExpression) {
// not exists
NotExpression expression = (NotExpression) where;
processWhereSubSelect(expression.getExpression());
} else if (where instanceof Parenthesis) {
Parenthesis expression = (Parenthesis) where;
processWhereSubSelect(expression.getExpression());
}
}
}
protected void processSelectItem(SelectItem selectItem) {
if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
if (selectExpressionItem.getExpression() instanceof SubSelect) {
processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
} else if (selectExpressionItem.getExpression() instanceof Function) {
processFunction((Function) selectExpressionItem.getExpression());
}
}
}
/**
* 处理函数
* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>
* <p> fixed gitee pulls/141</p>
*
* @param function
*/
protected void processFunction(Function function) {
ExpressionList parameters = function.getParameters();
if (parameters != null) {
parameters.getExpressions().forEach(expression -> {
if (expression instanceof SubSelect) {
processSelectBody(((SubSelect) expression).getSelectBody());
} else if (expression instanceof Function) {
processFunction((Function) expression);
}
});
}
}
/**
* 处理子查询等
*/
protected void processOtherFromItem(FromItem fromItem) {
// 去除括号
while (fromItem instanceof ParenthesisFromItem) {
fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
}
if (fromItem instanceof SubSelect) {
SubSelect subSelect = (SubSelect) fromItem;
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
} else if (fromItem instanceof ValuesList) {
logger.debug("Perform a subQuery, if you do not give us feedback");
} else if (fromItem instanceof LateralSubSelect) {
LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
if (lateralSubSelect.getSubSelect() != null) {
SubSelect subSelect = lateralSubSelect.getSubSelect();
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
}
}
}
/**
* 处理 sub join
*
* @param subJoin subJoin
* @return Table subJoin 中的主表
*/
private List<Table> processSubJoin(SubJoin subJoin) {
List<Table> mainTables = new ArrayList<>();
if (subJoin.getJoinList() != null) {
List<Table> list = processFromItem(subJoin.getLeft());
mainTables.addAll(list);
mainTables = processJoins(mainTables, subJoin.getJoinList());
}
return mainTables;
}
/**
* 处理 joins
*
* @param mainTables 可以为 null
* @param joins join 集合
* @return List<Table> 右连接查询的 Table 列表
*/
private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {
// join 表达式中最终的主表
Table mainTable = null;
// 当前 join 的左表
Table leftTable = null;
if (mainTables == null) {
mainTables = new ArrayList<>();
} else if (mainTables.size() == 1) {
mainTable = mainTables.get(0);
leftTable = mainTable;
}
//对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
Deque<List<Table>> onTableDeque = new LinkedList<>();
for (Join join : joins) {
// 处理 on 表达式
FromItem joinItem = join.getRightItem();
// 获取当前 join 的表,subJoint 可以看作是一张表
List<Table> joinTables = null;
if (joinItem instanceof Table) {
joinTables = new ArrayList<>();
joinTables.add((Table) joinItem);
} else if (joinItem instanceof SubJoin) {
joinTables = processSubJoin((SubJoin) joinItem);
}
if (joinTables != null) {
// 如果是隐式内连接
if (join.isSimple()) {
mainTables.addAll(joinTables);
continue;
}
// 当前表是否忽略
Table joinTable = joinTables.get(0);
List<Table> onTables = null;
// 如果不要忽略,且是右连接,则记录下当前表
if (join.isRight()) {
mainTable = joinTable;
if (leftTable != null) {
onTables = Collections.singletonList(leftTable);
}
} else if (join.isLeft()) {
onTables = Collections.singletonList(joinTable);
} else if (join.isInner()) {
if (mainTable == null) {
onTables = Collections.singletonList(joinTable);
} else {
onTables = Arrays.asList(mainTable, joinTable);
}
mainTable = null;
}
mainTables = new ArrayList<>();
if (mainTable != null) {
mainTables.add(mainTable);
}
// 获取 join 尾缀的 on 表达式列表
Collection<Expression> originOnExpressions = join.getOnExpressions();
// 正常 join on 表达式只有一个,立刻处理
if (originOnExpressions.size() == 1 && onTables != null) {
List<Expression> onExpressions = new LinkedList<>();
onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables));
join.setOnExpressions(onExpressions);
leftTable = joinTable;
continue;
}
// 表名压栈,忽略的表压入 null,以便后续不处理
onTableDeque.push(onTables);
// 尾缀多个 on 表达式的时候统一处理
if (originOnExpressions.size() > 1) {
Collection<Expression> onExpressions = new LinkedList<>();
for (Expression originOnExpression : originOnExpressions) {
List<Table> currentTableList = onTableDeque.poll();
if (CollectionUtils.isEmpty(currentTableList)) {
onExpressions.add(originOnExpression);
} else {
onExpressions.add(builderExpression(originOnExpression, currentTableList));
}
}
join.setOnExpressions(onExpressions);
}
leftTable = joinTable;
} else {
processOtherFromItem(joinItem);
leftTable = null;
}
}
return mainTables;
}
// ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ==========
/**
* 处理条件
*
* @param currentExpression 当前 where 条件
* @param table 单个表
*/
protected Expression builderExpression(Expression currentExpression, Table table) {
return this.builderExpression(currentExpression, Collections.singletonList(table));
}
/**
* 处理条件
*
* @param currentExpression 当前 where 条件
* @param tables 多个表
*/
protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
// 没有表需要处理直接返回
if (CollectionUtils.isEmpty(tables)) {
return currentExpression;
}
// 第一步,获得 Table 对应的数据权限条件
Expression dataPermissionExpression = null;
for (Table table : tables) {
// 构建每个表的权限 Expression 条件
Expression expression = buildDataPermissionExpression(table);
if (expression == null) {
continue;
}
// 合并到 dataPermissionExpression 中
dataPermissionExpression = dataPermissionExpression == null ? expression
: new AndExpression(dataPermissionExpression, expression);
}
// 第二步,合并多个 Expression 条件
if (dataPermissionExpression == null) {
return currentExpression;
}
if (currentExpression == null) {
return dataPermissionExpression;
}
// ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
if (currentExpression instanceof OrExpression) {
return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
}
// ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
return new AndExpression(currentExpression, dataPermissionExpression);
}
/**
* 构建指定表的数据权限的 Expression 过滤条件
*
* @param table 表
* @return Expression 过滤条件
*/
private Expression buildDataPermissionExpression(Table table) {
// 生成条件
Expression allExpression = null;
for (DataPermissionRule rule : ContextHolder.getRules()) {
// 判断表名是否匹配
if (!rule.getTableNames().contains(table.getName())) {
continue;
}
// 如果有匹配的规则,说明可重写。
// 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
// 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
ContextHolder.setRewrite(true);
// 单条规则的条件
String tableName = getTableName(table);
Expression oneExpress = rule.getExpression(tableName, table.getAlias());
if (oneExpress == null) {
continue;
}
// 拼接到 allExpression 中
allExpression = allExpression == null ? oneExpress
: new AndExpression(allExpression, oneExpress);
}
return allExpression;
}
/**
* 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中
*
* @param ms MappedStatement
*/
private void addMappedStatementCache(MappedStatement ms) {
if (ContextHolder.getRewrite()) {
return;
}
// 无重写,进行添加
mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules());
}
/**
* SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则
*
* @author yudao
*/
static final class ContextHolder {
/**
* 该 {@link MappedStatement} 对应的规则
*/
private static final ThreadLocal<List<DataPermissionRule>> RULES = ThreadLocal.withInitial(Collections::emptyList);
/**
* SQL 是否进行重写
*/
private static final ThreadLocal<Boolean> REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE);
public static void init(List<DataPermissionRule> rules) {
RULES.set(rules);
REWRITE.set(false);
}
public static void clear() {
RULES.remove();
REWRITE.remove();
}
public static boolean getRewrite() {
return REWRITE.get();
}
public static void setRewrite(boolean rewrite) {
REWRITE.set(rewrite);
}
public static List<DataPermissionRule> getRules() {
return RULES.get();
}
}
/**
* {@link MappedStatement} 缓存
* 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效
* 如果无效,则可以避免 SQL 的解析,加快速度
*
* @author yudao
*/
static final class MappedStatementCache {
/**
* 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存
* <p>
* value:{@link MappedStatement#getId()} 编号
*/
private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>();
public Map<Class<? extends DataPermissionRule>, Set<String>> getNoRewritableMappedStatements() {
return noRewritableMappedStatements;
}
/**
* 判断是否无需重写
* ps:虽然有点中文式英语,但是容易读懂即可
*
* @param ms MappedStatement
* @param rules 数据权限规则数组
* @return 是否无需重写
*/
public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
// 如果规则为空,说明无需重写
if (org.springframework.util.CollectionUtils.isEmpty(rules)) {
return true;
}
// 任一规则不在 noRewritableMap 中,则说明可能需要重写
for (DataPermissionRule rule : rules) {
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
if (mappedStatementIds != null && !mappedStatementIds.stream().anyMatch(item -> item.equals(ms.getId()))) {
return false;
}
return false;
}
return true;
}
/**
* 添加无需重写的 MappedStatement
*
* @param ms MappedStatement
* @param rules 数据权限规则数组
*/
public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
for (DataPermissionRule rule : rules) {
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
if (Objects.nonNull(mappedStatementIds) && CollectionUtils.isEmpty(mappedStatementIds)) {
mappedStatementIds.add(ms.getId());
} else {
noRewritableMappedStatements.put(rule.getClass(), Arrays.stream(new String[]{ms.getId()}).collect(Collectors.toSet()));
}
}
}
/**
* 清空缓存
* 目前主要提供给单元测试
*/
public void clear() {
noRewritableMappedStatements.clear();
}
}
/**
* 获得 Table 对应的表名
* <p>
* 兼容 MySQL 转义表名 `t_xxx`
*
* @param table 表
* @return 去除转移字符后的表名
*/
public static String getTableName(Table table) {
String tableName = table.getName();
if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
tableName = tableName.substring(1, tableName.length() - 1);
}
return tableName;
}
}
2.8 DataPermissionRule.java
数据权限实现规则,业务系统也可以实现该接口,定义出自身的数据过滤规则;
package com.luo.chengrui.datapermission.rules;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import java.util.Set;
/**
* 数据权限规则接口
* 通过实现接口,自定义数据规则。例如说,
*
* @author luodz
*/
public interface DataPermissionRule {
/**
* 获取需要权限过滤的表名,在sql拦截中先按表名进行匹配,匹配上再调用
* @return
*/
Set<String> getTableNames();
/**
* 根据表名和别名,生成对应的 WHERE / OR 过滤条件
*
* @param tableName 表名
* @param tableAlias 别名,可能为空
* @return 过滤条件 Expression 表达式
*/
Expression getExpression(String tableName, Alias tableAlias);
}
2.9 DefaultDataPermissionRule.java
插件实现的默认数据规则处理器,进行数据过滤的关键因素有两个:
- 哪些数据表、字段要做数据过滤;
- 拿到当前用户的数据权限;
以上两个问题不可能提前知道,只能是在业务使用时才知道。既然不知道,那么就把这两个问题交给业务系统或者说程序员去处理吧。
那么我们在实现默认数据规则处理器时,这样去解决上面两个问题:
- 将表和字段的配置提供一个方法
addTableAndColumn(String tableName, String columnName)
; - 把获取用户数据权限定义为一个接口,让用户创建默认规则对象时实现该接口。
综上,就有了默认数据规则处理器,如果就具备了处理任何表、任何权限的能力。
package com.luo.chengrui.datapermission.rules;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import java.util.*;
import java.util.stream.Collectors;
/**
* 默认权限规则处理器
*/
public class DefaultDataPermissionRule<E extends Expression> implements DataPermissionRule {
static final Expression EXPRESSION_NULL = new NullValue();
protected final Map<String, String> tableAndColumns = new HashMap<>();
/**
* 所有表名,需要进行权限过滤的表名
*/
protected final Set<String> TABLE_NAMES = new HashSet<>();
/**
* 获取权限范围的接口
*/
private CustomerPermissionFunction permissionFunction;
public DefaultDataPermissionRule(CustomerPermissionFunction permissionFunction) {
this.permissionFunction = permissionFunction;
}
/**
* 添加需要过滤的表和字段
*
* @param tableName 表名
* @param columnName 字段名
*/
public void addTableAndColumn(String tableName, String columnName) {
tableAndColumns.put(tableName, columnName);
TABLE_NAMES.add(tableName);
}
//定义了用户自定义权限方法,将具体权限获取交给业务去实现。从而让默认规则可以支持任何数据过滤规则。
public interface CustomerPermissionFunction<E extends Expression> {
Set<E> getPermission();
}
/**
* 获取所有需要按部门权限过滤的表
*
* @return
*/
@Override
public Set<String> getTableNames() {
return TABLE_NAMES;
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
// 获取用户的权限
Set<E> deptIds = permissionFunction.getPermission();
if (deptIds == null) {
//表示 不进行权限过滤,即有所有权限。
return null;
}
//进行数据权限过滤
if (Objects.nonNull(tableAndColumns.get(tableName)) && deptIds != null && deptIds.size() > 0) {
return new InExpression(buildColumn(tableName, tableAlias, tableAndColumns.get(tableName)),
new ExpressionList(deptIds.stream().collect(Collectors.toList())));
}
//没有任何数据权限,查询结果将为是空;
return EXPRESSION_NULL;
}
/**
* 构建 Column 对象
*
* @param tableName 表名
* @param tableAlias 别名
* @param column 字段名
* @return Column 对象
*/
public static Column buildColumn(String tableName, Alias tableAlias, String column) {
if (tableAlias != null) {
tableName = tableAlias.getName();
}
return new Column(tableName + "." + column);
}
}
综上,数据权限插件全部代码已经完成。
三、业务系统应用
3.1 pom.xml
如果本身是springboot+mybatis项目,那么仅需要引入:
- mybatis-plus-boot-starter:mybatisplus依赖包
- cr-datapermission:上面的数据权限插件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.chengrui.lab</groupId>
<artifactId>cr-lab-datapermission</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<sprint.boot.version>2.7.14</sprint.boot.version>
</properties>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<!-- 实现对 MyBatis 的自动化配置 -->
<!-- <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>-->
<!-- 引入mybatisplus包,使用sqlpaser功能-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- 我们的数据权限包-->
<dependency>
<groupId>com.chengrui.tool</groupId>
<artifactId>cr-datapermission</artifactId>
<version>1.0.1</version>
</dependency>
<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
</dependencies>
</project>
3.2 MybatisConfig.java——测试版
mybatis框架项目基本都有这个配置类,有这个配置类的时,需要把我们自定义的sql拦截器添加到mybatis拦截器队列中。
1创建规则对象, 2创建SQL拦截器对象, 3将拦截器添加到mybatis中
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
String mapperLocations = env.getProperty("mybatis.mapperLocations");
String configLocation = env.getProperty("mybatis.configLocation");
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
VFS.addImplClass(SpringBootVFS.class);
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
//1开始创建规则
//用默认权限规则创建按部门过滤的
DefaultDataPermissionRule dataPermissionRule = new DefaultDataPermissionRule(() -> {
//获取用户权限。这里获取最好从缓存里面获取,如果实时从数据库获取,会生一些怪事。比如和PageHelper一起使用时,获取权限的时候会被分页。
Set<LongValue> deptId = new HashSet<>();
deptId.add(new LongValue(4L));
return deptId;
});
//注册需要按部门ID过滤的表和字段;
dataPermissionRule.addTableAndColumn("users", "dept_id");
List<DataPermissionRule> dataPermissionRules = new ArrayList<>();
dataPermissionRules.add(dataPermissionRule);
// 规则创建完成
//2创建拦截器对象
DataPermissionSqlParserInterceptor dataPermissionSqlParserInterceptor = new DataPermissionSqlParserInterceptor(dataPermissionRules);
//3将拦截器添加到mybatis中。
sessionFactory.setPlugins(dataPermissionSqlParserInterceptor);
return sessionFactory.getObject();
}
如果你的springboot版本支持自动配置,则注解拦截器你无需注册,如果不支持的话,还需手动注册一下。
运行结果:
3.3默认规则使用示例——正式版
在正式项目中,可能已经有MyBatisConfig类的配置,而且如果权限规则较多的情况下,可能不方便每次都修改MyBatisConfig类。那么也很好处理。
我在一个项目中的处理方式是单独创建一个配置类DefaultDataPermissionConfiguration.java.。
- 首先创建了一个叫deptDataPermissionRule的DataPermissionRule 对象,如果有其他数据类型只需要再创建相关Bean即可,
这里需要特别注意获取数据权限时最好从缓存中拿,否可能与PageHelper产生污染
- 创建DataPermissionSqlParserInterceptor 对象方法,该方法需要两个参数:
SqlSessionFactory
、List<DataPermissionRule>
;一方面创建拦截器,二是将拦截器注入sessionFactory中。
package com.ruoyi.web.core.config;
import cn.hutool.core.collection.CollectionUtil;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.luo.chengrui.datapermission.interceptor.DataPermissionSqlParserInterceptor;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import com.luo.chengrui.datapermission.rules.DefaultDataPermissionRule;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.config.service.ICkStorageService;
import com.ruoyi.framework.datapermissionRule.DeptDatapermissionRule;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.StringValue;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author
* @version 1.0.0
* @description
* @createTime 2023/07/17
*/
@Configuration
public class DefaultDataPermissionConfiguration {
/**
* 初始化部门权限 bean 。
*
* @return
*/
@Bean
public DataPermissionRule deptDataPermissionRule() {
DefaultDataPermissionRule rule = new DefaultDataPermissionRule(() -> {
// 如果是分页,这里需先去掉分页线参数,不然会出线线程污染。待数据权限查询结果完结后,再设置分页参数。
// 当然更好的做法是权限数据从缓存中获取,这样就不需要如此做了。
Page page = PageHelper.getLocalPage();
PageHelper.clearPage();
// 在这里动态获取用户权限;
ICkStorageService ckStorageService = SpringUtils.getBean(ICkStorageService.class);
Set<StringValue> set = null;
//DeptDatapermissionRule是业务自己实现的权限获取方法。
Set<Long> deptIds = DeptDatapermissionRule.getStorageIds();
if (deptIds != null) {
set = new HashSet<>();
if (CollectionUtil.isNotEmpty(deptIds)) {
List<String> storageId = ckStorageService.getSotageIdsByDeptIds(deptIds);
set.addAll(storageId.stream().map(id -> new StringValue(id)).collect(Collectors.toList()));
}
}
if (page != null) {
// 把分页参数设置回线程变量中。
PageHelper.startPage(page.getPageNum(), page.getPageSize(), page.getOrderBy()).setReasonable(page.getReasonable());
}
return set;
});
//当sql中包含以下两个表时,进行数据过滤。
rule.addTableAndColumn("wms_order_info", "storage_id");
rule.addTableAndColumn("wms_sorder_info", "storage_id");
return rule;
}
@Bean
public DataPermissionSqlParserInterceptor dataPermissionSqlParserInterceptor(SqlSessionFactory sqlSessionFactory, List<DataPermissionRule> dataPermissionRuleList) {
DataPermissionSqlParserInterceptor dataPermissionSqlParserInterceptor = new DataPermissionSqlParserInterceptor(dataPermissionRuleList);
if (sqlSessionFactory instanceof SqlSessionFactoryBean) {
((SqlSessionFactoryBean) sqlSessionFactory).setPlugins(dataPermissionSqlParserInterceptor);
}
return dataPermissionSqlParserInterceptor;
}
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!