mirror of
https://github.com/mfish-qf/mfish-nocode.git
synced 2026-10-28 02:42:09 +08:00
feat: 增加通用租户数据权限控制(#259)
This commit is contained in:
@@ -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 "";
|
||||
}
|
||||
+17
@@ -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();
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+148
@@ -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;
|
||||
}
|
||||
}
|
||||
+75
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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());
|
||||
}
|
||||
}
|
||||
+2
@@ -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)), "角色信息表-查询成功!");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user