feat: 增加通用租户数据权限控制(#259)

This commit is contained in:
qiufeng
2024-04-28 23:01:39 +08:00
parent f88456e324
commit 1694c045fa
11 changed files with 429 additions and 5 deletions
@@ -0,0 +1,41 @@
package cn.com.mfish.common.ds.annotation;
import cn.com.mfish.common.ds.common.DataScopeType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @description: 数据范围
* @author: mfish
* @date: 2024/4/25
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
/**
* 需要进行权限控制的表
*
* @return
*/
String table() default "";
/**
* 进行权限控制的类型
*
* @return
*/
DataScopeType type() default DataScopeType.Tenant;
/**
* 表进行过滤的字段名称
* 注意:fieldNames与tables数组值一一对应
*
* @return
*/
String fieldName() default "";
String value() default "";
}
@@ -0,0 +1,17 @@
package cn.com.mfish.common.ds.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @description: 数据范围组
* @author: mfish
* @date: 2024/4/28
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScopes {
DataScope[] value();
}
@@ -0,0 +1,56 @@
package cn.com.mfish.common.ds.aspect;
import cn.com.mfish.common.core.annotation.GlobalException;
import cn.com.mfish.common.ds.annotation.DataScope;
import cn.com.mfish.common.ds.annotation.DataScopes;
import cn.com.mfish.common.ds.common.DataScopeUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @description: 数据范围切面
* @author: mfish
* @date: 2024/4/25
*/
@Aspect
@Component
@GlobalException
@Slf4j
public class DataScopeAspect {
@Before("@annotation(cn.com.mfish.common.ds.annotation.DataScopes)||@annotation(cn.com.mfish.common.ds.annotation.DataScope)")
public void doBefore(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
List<DataScope> list = new ArrayList<>();
if (method.isAnnotationPresent(DataScopes.class)) {
DataScopes dataScopes = method.getAnnotation(DataScopes.class);
list.addAll(Arrays.stream(dataScopes.value()).toList());
} else {
list.add(method.getAnnotation(DataScope.class));
}
DataScopeUtils.context.set(list);
}
@AfterReturning("@annotation(cn.com.mfish.common.ds.annotation.DataScopes)||@annotation(cn.com.mfish.common.ds.annotation.DataScope)")
public void doAfterReturning() {
DataScopeUtils.context.remove();
}
@AfterThrowing("@annotation(cn.com.mfish.common.ds.annotation.DataScopes)||@annotation(cn.com.mfish.common.ds.annotation.DataScope)")
public void doAfterThrowing() {
DataScopeUtils.context.remove();
}
}
@@ -0,0 +1,25 @@
package cn.com.mfish.common.ds.common;
import cn.com.mfish.common.ds.scope.DataScopeHandle;
import cn.com.mfish.common.ds.scope.RoleDataScopeHandle;
import cn.com.mfish.common.ds.scope.TenantDataScopeHandle;
import lombok.Getter;
/**
* @description: 数据范围
* @author: mfish
* @date: 2024/4/26
*/
@Getter
public enum DataScopeType {
Tenant(0, new TenantDataScopeHandle()),
Role(1, new RoleDataScopeHandle());
private final int value;
private final DataScopeHandle handle;
DataScopeType(int value, DataScopeHandle handle) {
this.value = value;
this.handle = handle;
}
}
@@ -0,0 +1,148 @@
package cn.com.mfish.common.ds.common;
import cn.com.mfish.common.ds.annotation.DataScope;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @description: 数据范围上下文
* @author: mfish
* @date: 2024/4/26
*/
public class DataScopeUtils {
public static final ThreadLocal<List<DataScope>> context = new ThreadLocal<>();
public static Set<String> FOLLOW_KEYWORD = new HashSet<>();
public static Set<Character> START_KEYWORD = new HashSet<>();
public static Set<Character> END_KEYWORD = new HashSet<>();
static {
START_KEYWORD.add('.');
START_KEYWORD.add('(');
START_KEYWORD.add(' ');
START_KEYWORD.add('\n');
START_KEYWORD.add('\r');
START_KEYWORD.add('\t');
//结束标示
END_KEYWORD.add(' ');
END_KEYWORD.add('\n');
END_KEYWORD.add('\r');
END_KEYWORD.add('\t');
END_KEYWORD.add(')');
//表名后允许的关键字
FOLLOW_KEYWORD.add("as");
FOLLOW_KEYWORD.add("where");
FOLLOW_KEYWORD.add("inner");
FOLLOW_KEYWORD.add("left");
FOLLOW_KEYWORD.add("right");
FOLLOW_KEYWORD.add("full");
FOLLOW_KEYWORD.add("join");
FOLLOW_KEYWORD.add("group");
FOLLOW_KEYWORD.add("limit");
FOLLOW_KEYWORD.add("order");
}
/**
* 分解SQL
*
* @param sql
* @param tableName
* @return
*/
public static List<SqlSplit> splitSql(String sql, String tableName) {
List<SqlSplit> list = new ArrayList<>();
findTable(sql, tableName, list);
return list;
}
/**
* 找出SQL中指定表
* #############并不严谨,适用与一般SQL。奇葩SQL请自行解决#############
*
* @param sql 原SQL
* @param tableName 表名
* @param list 列表
*/
private static void findTable(String sql, String tableName, List<SqlSplit> list) {
int index = sql.toLowerCase().indexOf(tableName.toLowerCase());
if (index <= 0) {
//未找到表名直接返回
list.add(new SqlSplit().setStatement(sql).setHaveTable(false));
return;
}
SqlSplit sqlSplit = new SqlSplit().setStatement(sql.substring(0, index + tableName.length()));
boolean isTable = START_KEYWORD.contains(sql.toCharArray()[index - 1]);
String leaveSql = sql.substring(index + tableName.length());
//表名已结尾说明当前SQL存在表名,直接返回
if (leaveSql.trim().isEmpty()) {
list.add(sqlSplit.setHaveTable(isTable));
return;
}
//不符合表开始字符或结束字符说明当前字符并非表名,继续判断后续SQL
if (!isTable || !END_KEYWORD.contains(leaveSql.charAt(0))) {
list.add(sqlSplit);
findTable(leaveSql, tableName, list);
return;
}
//表名后非括回结尾可能存在别名,进行别名判断
if (leaveSql.trim().charAt(0) == ')') {
list.add(sqlSplit.setHaveTable(isTable));
findTable(leaveSql, tableName, list);
return;
}
String keyword = followKeyword(leaveSql);
int keywordIndex = leaveSql.indexOf(keyword) + keyword.length();
//不在表后面跟随的关键字中,说明当前字符为别名
if (!FOLLOW_KEYWORD.contains(keyword.toLowerCase())) {
list.add(sqlSplit.setHaveTable(true).setHaveAlias(true));
} else if (keyword.equalsIgnoreCase("as")) {
//当前关键字为as,说明后面跟随字符为别名
list.add(sqlSplit.setHaveTable(true).setHaveAlias(true));
String nextKeyword = followKeyword(leaveSql.trim().substring(keyword.length()));
keyword += " " + nextKeyword;
keywordIndex = leaveSql.indexOf(nextKeyword) + nextKeyword.length();
} else {
//不是别名直接返回
list.add(sqlSplit.setHaveTable(true));
findTable(leaveSql, tableName, list);
return;
}
list.add(new SqlSplit().setStatement(" " + keyword));
leaveSql = leaveSql.substring(keywordIndex);
findTable(leaveSql, tableName, list);
}
/**
* 获取表后面的关键字
*
* @param sql 剩余SQL语句
* @return 关键字
*/
private static String followKeyword(String sql) {
StringBuilder sb = new StringBuilder();
for (char c : sql.trim().toCharArray()) {
if (END_KEYWORD.contains(c)) {
break;
}
sb.append(c);
}
return sb.toString();
}
@Data
@Accessors(chain = true)
public static class SqlSplit {
@Schema(name = "语句类型")
private boolean haveTable = false;
@Schema(name = "语句")
private String statement;
@Schema(name = "是否存在别名")
private boolean haveAlias = false;
}
}
@@ -0,0 +1,75 @@
package cn.com.mfish.common.ds.config;
import cn.com.mfish.common.core.exception.MyRuntimeException;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.ds.annotation.DataScope;
import cn.com.mfish.common.ds.common.DataScopeUtils;
import lombok.extern.slf4j.Slf4j;
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.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.List;
/**
* @description: 查询拦截器
* @author: mfish
* @date: 2024/4/25
*/
@Component
@Intercepts(
{@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}
)
@Slf4j
public class SelectInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<DataScope> scopes = DataScopeUtils.context.get();
if (scopes == null) {
return invocation.proceed();
}
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = MetaObject
.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
new DefaultReflectorFactory());
// 获取mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
SqlCommandType sqlType = mappedStatement.getSqlCommandType();
if (!SqlCommandType.SELECT.equals(sqlType)) {
return invocation.proceed();
}
BoundSql boundSql = statementHandler.getBoundSql();
String oriSql = boundSql.getSql();
try {
for (DataScope scope : scopes) {
String tableName = scope.table();
if (!oriSql.toLowerCase().contains(tableName.toLowerCase())) {
continue;
}
if (!StringUtils.isEmpty(scope.fieldName())) {
oriSql = scope.type().getHandle().sqlChange(oriSql, tableName, scope.fieldName());
} else {
oriSql = scope.type().getHandle().sqlChange(oriSql, tableName, "");
}
}
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, oriSql);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new MyRuntimeException("异常:数据权限过滤出错");
}
return invocation.proceed();
}
}
@@ -0,0 +1,11 @@
package cn.com.mfish.common.ds.scope;
/**
* @description: 数据范围处理
* @author: mfish
* @date: 2024/4/26
*/
public interface DataScopeHandle {
String sqlChange(String sql, String table, String fieldName);
}
@@ -0,0 +1,15 @@
package cn.com.mfish.common.ds.scope;
/**
* @description: 租户数据处理
* @author: mfish
* @date: 2024/4/26
*/
public class RoleDataScopeHandle implements DataScopeHandle {
@Override
public String sqlChange(String sql, String table, String fieldName) {
//todo 补充逻辑
return sql;
}
}
@@ -0,0 +1,32 @@
package cn.com.mfish.common.ds.scope;
import cn.com.mfish.common.core.utils.AuthInfoUtils;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.ds.common.DataScopeUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* @description: 租户数据处理
* @author: mfish
* @date: 2024/4/26
*/
public class TenantDataScopeHandle implements DataScopeHandle {
@Override
public String sqlChange(String sql, String table, String fieldName) {
fieldName = StringUtils.isEmpty(fieldName) ? "tenant_id" : fieldName;
List<DataScopeUtils.SqlSplit> list = DataScopeUtils.splitSql(sql, table);
for (DataScopeUtils.SqlSplit split : list) {
if (split.isHaveTable()) {
split.setStatement(split.getStatement().replaceAll(table
, "(select * from " + table + " where " + fieldName + "='" + AuthInfoUtils.getCurrentTenantId() + "')"));
if (split.isHaveAlias()) {
continue;
}
split.setStatement(split.getStatement() + " " + table);
}
}
return list.stream().map(DataScopeUtils.SqlSplit::getStatement).collect(Collectors.joining());
}
}
@@ -1,3 +1,5 @@
cn.com.mfish.common.ds.config.MybatisInterceptor
cn.com.mfish.common.ds.config.SelectInterceptor
cn.com.mfish.common.ds.aspect.DataScopeAspect
cn.com.mfish.common.ds.config.BatchSqlInjector
com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure
@@ -9,8 +9,11 @@ import cn.com.mfish.common.core.utils.excel.ExcelUtils;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.ds.annotation.DataScope;
import cn.com.mfish.common.ds.common.DataScopeType;
import cn.com.mfish.common.log.annotation.Log;
import cn.com.mfish.common.oauth.annotation.RequiresPermissions;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.common.oauth.api.entity.SsoOrg;
import cn.com.mfish.common.oauth.api.entity.SsoTenant;
import cn.com.mfish.common.oauth.api.entity.UserInfo;
@@ -18,18 +21,17 @@ import cn.com.mfish.common.oauth.api.vo.TenantVo;
import cn.com.mfish.common.oauth.common.OauthUtils;
import cn.com.mfish.common.oauth.entity.RedisAccessToken;
import cn.com.mfish.common.oauth.entity.WeChatToken;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.common.oauth.req.ReqSsoOrg;
import cn.com.mfish.common.oauth.req.ReqSsoUser;
import cn.com.mfish.common.oauth.service.SsoMenuService;
import cn.com.mfish.common.oauth.service.SsoOrgService;
import cn.com.mfish.common.oauth.service.SsoUserService;
import cn.com.mfish.oauth.cache.common.ClearCache;
import cn.com.mfish.common.oauth.api.entity.SsoMenu;
import cn.com.mfish.oauth.entity.SsoRole;
import cn.com.mfish.oauth.entity.UserOrg;
import cn.com.mfish.common.oauth.req.ReqSsoMenu;
import cn.com.mfish.oauth.req.ReqSsoRole;
import cn.com.mfish.oauth.req.ReqSsoTenant;
import cn.com.mfish.common.oauth.service.SsoMenuService;
import cn.com.mfish.oauth.service.SsoRoleService;
import cn.com.mfish.oauth.service.SsoTenantService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -357,17 +359,17 @@ public class SsoTenantController {
@Operation(summary = "租户角色信息-分页列表查询", description = "租户角色信息-分页列表查询")
@GetMapping("/role")
@RequiresPermissions("sys:tenantRole:query")
@DataScope(table = "sso_role", type = DataScopeType.Tenant)
public Result<PageResult<SsoRole>> queryRolePageList(ReqSsoRole reqSsoRole, ReqPage reqPage) {
PageHelper.startPage(reqPage.getPageNum(), reqPage.getPageSize());
reqSsoRole.setTenantId(AuthInfoUtils.getCurrentTenantId());
return Result.ok(new PageResult<>(ssoRoleService.list(SsoRoleController.buildCondition(reqSsoRole))), "角色信息表-查询成功!");
}
@Operation(summary = "角色信息表-列表查询", description = "角色信息表-列表查询")
@GetMapping("/role/all")
@RequiresPermissions("sys:tenantRole:query")
@DataScope(table = "sso_role", type = DataScopeType.Tenant)
public Result<List<SsoRole>> queryRoleList(ReqSsoRole reqSsoRole) {
reqSsoRole.setTenantId(AuthInfoUtils.getCurrentTenantId());
return Result.ok(ssoRoleService.list(SsoRoleController.buildCondition(reqSsoRole)), "角色信息表-查询成功!");
}