feat:增加租户切换的能力

This commit is contained in:
YunaiV 2025-05-05 17:27:40 +08:00
parent 8500575f44
commit 59234e1eea
11 changed files with 183 additions and 11 deletions

View File

@ -12,6 +12,8 @@ import net.sf.jsqlparser.schema.Table;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
/** /**
* 基于 {@link DataPermissionRule} 的数据权限处理器 * 基于 {@link DataPermissionRule} 的数据权限处理器
* *
@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
@Override @Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
// 特殊跨租户访问
if (skipPermissionCheck()) {
return null;
}
// 获得 Mapper 对应的数据权限的规则 // 获得 Mapper 对应的数据权限的规则
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId); List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
if (CollUtil.isEmpty(rules)) { if (CollUtil.isEmpty(rules)) {

View File

@ -32,13 +32,12 @@ public class DataPermissionUtils {
* @param runnable 逻辑 * @param runnable 逻辑
*/ */
public static void executeIgnore(Runnable runnable) { public static void executeIgnore(Runnable runnable) {
DataPermission dataPermission = getDisableDataPermissionDisable(); addDisableDataPermission();
DataPermissionContextHolder.add(dataPermission);
try { try {
// 执行 runnable // 执行 runnable
runnable.run(); runnable.run();
} finally { } finally {
DataPermissionContextHolder.remove(); removeDataPermission();
} }
} }
@ -50,14 +49,25 @@ public class DataPermissionUtils {
*/ */
@SneakyThrows @SneakyThrows
public static <T> T executeIgnore(Callable<T> callable) { public static <T> T executeIgnore(Callable<T> callable) {
DataPermission dataPermission = getDisableDataPermissionDisable(); addDisableDataPermission();
DataPermissionContextHolder.add(dataPermission);
try { try {
// 执行 callable // 执行 callable
return callable.call(); return callable.call();
} finally { } finally {
DataPermissionContextHolder.remove(); removeDataPermission();
} }
} }
/**
* 添加忽略数据权限
*/
public static void addDisableDataPermission(){
DataPermission dataPermission = getDisableDataPermissionDisable();
DataPermissionContextHolder.add(dataPermission);
}
public static void removeDataPermission(){
DataPermissionContextHolder.remove();
}
} }

View File

@ -33,6 +33,13 @@ public class TenantProperties {
*/ */
private Set<String> ignoreUrls = new HashSet<>(); private Set<String> ignoreUrls = new HashSet<>();
/**
* 需要忽略跨切换租户访问的请求
*
* 原因是某些接口访问的是个人信息在跨租户是获取不到的
*/
private Set<String> ignoreVisitUrls = Collections.emptySet();
/** /**
* 需要忽略多租户的表 * 需要忽略多租户的表
* *

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties; import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
@ -15,6 +16,7 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl; import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor;
import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi; import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
@ -36,6 +38,8 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
@ -115,6 +119,25 @@ public class YudaoTenantAutoConfiguration {
} }
} }
@Bean
public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties,
SecurityFrameworkService securityFrameworkService) {
return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService);
}
@Bean
public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties,
TenantVisitContextInterceptor tenantVisitContextInterceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantVisitContextInterceptor)
.excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0]));
}
};
}
// ========== Security ========== // ========== Security ==========
@Bean @Bean

View File

@ -0,0 +1,65 @@
package cn.iocoder.yudao.framework.tenant.core.web;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
@RequiredArgsConstructor
@Slf4j
public class TenantVisitContextInterceptor implements HandlerInterceptor {
private static final String PERMISSION = "system:tenant:visit";
private final TenantProperties tenantProperties;
private final SecurityFrameworkService securityFrameworkService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 如果和当前租户编号一致则直接跳过
Long visitTenantId = WebFrameworkUtils.getVisitTenantId(request);
if (visitTenantId == null) {
return true;
}
if (ObjUtil.equal(visitTenantId, TenantContextHolder.getTenantId())) {
return true;
}
// 必须是登录用户
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) {
return true;
}
// 校验用户是否可切换租户
if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户");
}
// 重点切换租户编号
loginUser.setVisitTenantId(visitTenantId);
TenantContextHolder.setTenantId(visitTenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 重点清理切换换回原租户编号
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null && loginUser.getTenantId() != null) {
TenantContextHolder.setTenantId(loginUser.getTenantId());
}
}
}

View File

@ -56,6 +56,10 @@ public class LoginUser {
*/ */
@JsonIgnore @JsonIgnore
private Map<String, Object> context; private Map<String, Object> context;
/**
* 访问的租户编号
*/
private Long visitTenantId;
public void setContext(String key, Object value) { public void setContext(String key, Object value) {
if (context == null) { if (context == null) {

View File

@ -9,6 +9,7 @@ import lombok.AllArgsConstructor;
import java.util.Arrays; import java.util.Arrays;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
/** /**
* 默认的 {@link SecurityFrameworkService} 实现类 * 默认的 {@link SecurityFrameworkService} 实现类
@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override @Override
public boolean hasAnyPermissions(String... permissions) { public boolean hasAnyPermissions(String... permissions) {
// 特殊跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
Long userId = getLoginUserId(); Long userId = getLoginUserId();
if (userId == null) { if (userId == null) {
return false; return false;
@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override @Override
public boolean hasAnyRoles(String... roles) { public boolean hasAnyRoles(String... roles) {
// 特殊跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
Long userId = getLoginUserId(); Long userId = getLoginUserId();
if (userId == null) { if (userId == null) {
return false; return false;
@ -55,6 +68,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override @Override
public boolean hasAnyScopes(String... scope) { public boolean hasAnyScopes(String... scope) {
// 特殊跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
LoginUser user = SecurityFrameworkUtils.getLoginUser(); LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user == null) { if (user == null) {
return false; return false;

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.security.core.util; package cn.iocoder.yudao.framework.security.core.util;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@ -137,4 +138,21 @@ public class SecurityFrameworkUtils {
return authenticationToken; return authenticationToken;
} }
/**
* 是否条件跳过权限校验包括数据权限功能权限
*
* @return 是否跳过
*/
public static boolean skipPermissionCheck() {
LoginUser loginUser = getLoginUser();
if (loginUser == null) {
return false;
}
if (loginUser.getVisitTenantId() == null) {
return false;
}
// 重点跨租户访问时无法进行权限校验
return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId());
}
} }

View File

@ -1,19 +1,16 @@
package cn.iocoder.yudao.framework.web.core.util; package cn.iocoder.yudao.framework.web.core.util;
import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.NumberUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.config.WebProperties;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
/** /**
* 专属于 web 包的工具类 * 专属于 web 包的工具类
* *
@ -27,6 +24,7 @@ public class WebFrameworkUtils {
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
public static final String HEADER_TENANT_ID = "tenant-id"; public static final String HEADER_TENANT_ID = "tenant-id";
public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
/** /**
* 终端的 Header * 终端的 Header
@ -53,6 +51,18 @@ public class WebFrameworkUtils {
return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
} }
/**
* 获得访问的租户编号 header
* 考虑到其它 framework 组件也会使用到租户编号所以不得不放在 WebFrameworkUtils 统一提供
*
* @param request 请求
* @return 租户编号
*/
public static Long getVisitTenantId(HttpServletRequest request) {
String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID);
return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null;
}
public static void setLoginUserId(ServletRequest request, Long userId) { public static void setLoginUserId(ServletRequest request, Long userId) {
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
} }

View File

@ -3,3 +3,9 @@ GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
#Authorization: Bearer test100 #Authorization: Bearer test100
tenant-id: {{adminTenantId}} tenant-id: {{adminTenantId}}
### 请求 /system/user/page 接口(测试访问别的租户)
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
visit-tenant-id: 122

View File

@ -274,6 +274,9 @@ yudao:
enable: true enable: true
ignore-urls: ignore-urls:
- /jmreport/* # 积木报表,无法携带租户编号 - /jmreport/* # 积木报表,无法携带租户编号
ignore-visit-urls:
- /admin-api/system/user/profile/**
- /admin-api/system/auth/**
ignore-tables: ignore-tables:
ignore-caches: ignore-caches:
- user_role_ids - user_role_ids