Commit 59234e1eeade300a68adc8183d58f616c14e90f1

Authored by YunaiV
1 parent 8500575f

feat:增加租户切换的能力

Showing 11 changed files with 183 additions and 11 deletions
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java
@@ -12,6 +12,8 @@ import net.sf.jsqlparser.schema.Table; @@ -12,6 +12,8 @@ import net.sf.jsqlparser.schema.Table;
12 12
13 import java.util.List; 13 import java.util.List;
14 14
  15 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
  16 +
15 /** 17 /**
16 * 基于 {@link DataPermissionRule} 的数据权限处理器 18 * 基于 {@link DataPermissionRule} 的数据权限处理器
17 * 19 *
@@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler { @@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
27 29
28 @Override 30 @Override
29 public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { 31 public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
  32 + // 特殊:跨租户访问
  33 + if (skipPermissionCheck()) {
  34 + return null;
  35 + }
  36 +
30 // 获得 Mapper 对应的数据权限的规则 37 // 获得 Mapper 对应的数据权限的规则
31 List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId); 38 List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
32 if (CollUtil.isEmpty(rules)) { 39 if (CollUtil.isEmpty(rules)) {
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java
@@ -32,13 +32,12 @@ public class DataPermissionUtils { @@ -32,13 +32,12 @@ public class DataPermissionUtils {
32 * @param runnable 逻辑 32 * @param runnable 逻辑
33 */ 33 */
34 public static void executeIgnore(Runnable runnable) { 34 public static void executeIgnore(Runnable runnable) {
35 - DataPermission dataPermission = getDisableDataPermissionDisable();  
36 - DataPermissionContextHolder.add(dataPermission); 35 + addDisableDataPermission();
37 try { 36 try {
38 // 执行 runnable 37 // 执行 runnable
39 runnable.run(); 38 runnable.run();
40 } finally { 39 } finally {
41 - DataPermissionContextHolder.remove(); 40 + removeDataPermission();
42 } 41 }
43 } 42 }
44 43
@@ -50,14 +49,25 @@ public class DataPermissionUtils { @@ -50,14 +49,25 @@ public class DataPermissionUtils {
50 */ 49 */
51 @SneakyThrows 50 @SneakyThrows
52 public static <T> T executeIgnore(Callable<T> callable) { 51 public static <T> T executeIgnore(Callable<T> callable) {
53 - DataPermission dataPermission = getDisableDataPermissionDisable();  
54 - DataPermissionContextHolder.add(dataPermission); 52 + addDisableDataPermission();
55 try { 53 try {
56 // 执行 callable 54 // 执行 callable
57 return callable.call(); 55 return callable.call();
58 } finally { 56 } finally {
59 - DataPermissionContextHolder.remove(); 57 + removeDataPermission();
60 } 58 }
61 } 59 }
62 60
  61 + /**
  62 + * 添加忽略数据权限
  63 + */
  64 + public static void addDisableDataPermission(){
  65 + DataPermission dataPermission = getDisableDataPermissionDisable();
  66 + DataPermissionContextHolder.add(dataPermission);
  67 + }
  68 +
  69 + public static void removeDataPermission(){
  70 + DataPermissionContextHolder.remove();
  71 + }
  72 +
63 } 73 }
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java
@@ -34,6 +34,13 @@ public class TenantProperties { @@ -34,6 +34,13 @@ public class TenantProperties {
34 private Set<String> ignoreUrls = new HashSet<>(); 34 private Set<String> ignoreUrls = new HashSet<>();
35 35
36 /** 36 /**
  37 + * 需要忽略跨(切换)租户访问的请求
  38 + *
  39 + * 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的!
  40 + */
  41 + private Set<String> ignoreVisitUrls = Collections.emptySet();
  42 +
  43 + /**
37 * 需要忽略多租户的表 44 * 需要忽略多租户的表
38 * 45 *
39 * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 46 * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.config; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.config;
3 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; 3 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; 4 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
5 import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties; 5 import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
  6 +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
6 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 7 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
7 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; 8 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
8 import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; 9 import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
@@ -15,6 +16,7 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; @@ -15,6 +16,7 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
15 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; 16 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
16 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl; 17 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
17 import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; 18 import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
  19 +import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor;
18 import cn.iocoder.yudao.framework.web.config.WebProperties; 20 import cn.iocoder.yudao.framework.web.config.WebProperties;
19 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; 21 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
20 import cn.iocoder.yudao.module.system.api.tenant.TenantApi; 22 import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
@@ -36,6 +38,8 @@ import org.springframework.data.redis.cache.RedisCacheWriter; @@ -36,6 +38,8 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
36 import org.springframework.data.redis.connection.RedisConnectionFactory; 38 import org.springframework.data.redis.connection.RedisConnectionFactory;
37 import org.springframework.data.redis.core.RedisTemplate; 39 import org.springframework.data.redis.core.RedisTemplate;
38 import org.springframework.web.method.HandlerMethod; 40 import org.springframework.web.method.HandlerMethod;
  41 +import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  42 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
39 import org.springframework.web.servlet.mvc.method.RequestMappingInfo; 43 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
40 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; 44 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
41 import org.springframework.web.util.pattern.PathPattern; 45 import org.springframework.web.util.pattern.PathPattern;
@@ -115,6 +119,25 @@ public class YudaoTenantAutoConfiguration { @@ -115,6 +119,25 @@ public class YudaoTenantAutoConfiguration {
115 } 119 }
116 } 120 }
117 121
  122 + @Bean
  123 + public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties,
  124 + SecurityFrameworkService securityFrameworkService) {
  125 + return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService);
  126 + }
  127 +
  128 + @Bean
  129 + public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties,
  130 + TenantVisitContextInterceptor tenantVisitContextInterceptor) {
  131 + return new WebMvcConfigurer() {
  132 +
  133 + @Override
  134 + public void addInterceptors(InterceptorRegistry registry) {
  135 + registry.addInterceptor(tenantVisitContextInterceptor)
  136 + .excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0]));
  137 + }
  138 + };
  139 + }
  140 +
118 // ========== Security ========== 141 // ========== Security ==========
119 142
120 @Bean 143 @Bean
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.web;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  6 +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
  7 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  8 +import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
  9 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  10 +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  11 +import jakarta.servlet.http.HttpServletRequest;
  12 +import jakarta.servlet.http.HttpServletResponse;
  13 +import lombok.RequiredArgsConstructor;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.springframework.web.servlet.HandlerInterceptor;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
  18 +
  19 +@RequiredArgsConstructor
  20 +@Slf4j
  21 +public class TenantVisitContextInterceptor implements HandlerInterceptor {
  22 +
  23 + private static final String PERMISSION = "system:tenant:visit";
  24 +
  25 + private final TenantProperties tenantProperties;
  26 +
  27 + private final SecurityFrameworkService securityFrameworkService;
  28 +
  29 + @Override
  30 + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
  31 + // 如果和当前租户编号一致,则直接跳过
  32 + Long visitTenantId = WebFrameworkUtils.getVisitTenantId(request);
  33 + if (visitTenantId == null) {
  34 + return true;
  35 + }
  36 + if (ObjUtil.equal(visitTenantId, TenantContextHolder.getTenantId())) {
  37 + return true;
  38 + }
  39 + // 必须是登录用户
  40 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
  41 + if (loginUser == null) {
  42 + return true;
  43 + }
  44 +
  45 + // 校验用户是否可切换租户
  46 + if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
  47 + throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户");
  48 + }
  49 +
  50 + // 【重点】切换租户编号
  51 + loginUser.setVisitTenantId(visitTenantId);
  52 + TenantContextHolder.setTenantId(visitTenantId);
  53 + return true;
  54 + }
  55 +
  56 + @Override
  57 + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
  58 + // 【重点】清理切换,换回原租户编号
  59 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
  60 + if (loginUser != null && loginUser.getTenantId() != null) {
  61 + TenantContextHolder.setTenantId(loginUser.getTenantId());
  62 + }
  63 + }
  64 +
  65 +}
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
@@ -56,6 +56,10 @@ public class LoginUser { @@ -56,6 +56,10 @@ public class LoginUser {
56 */ 56 */
57 @JsonIgnore 57 @JsonIgnore
58 private Map<String, Object> context; 58 private Map<String, Object> context;
  59 + /**
  60 + * 访问的租户编号
  61 + */
  62 + private Long visitTenantId;
59 63
60 public void setContext(String key, Object value) { 64 public void setContext(String key, Object value) {
61 if (context == null) { 65 if (context == null) {
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java
@@ -9,6 +9,7 @@ import lombok.AllArgsConstructor; @@ -9,6 +9,7 @@ import lombok.AllArgsConstructor;
9 import java.util.Arrays; 9 import java.util.Arrays;
10 10
11 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 11 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  12 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
12 13
13 /** 14 /**
14 * 默认的 {@link SecurityFrameworkService} 实现类 15 * 默认的 {@link SecurityFrameworkService} 实现类
@@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
27 28
28 @Override 29 @Override
29 public boolean hasAnyPermissions(String... permissions) { 30 public boolean hasAnyPermissions(String... permissions) {
  31 + // 特殊:跨租户访问
  32 + if (skipPermissionCheck()) {
  33 + return true;
  34 + }
  35 +
  36 + // 权限校验
30 Long userId = getLoginUserId(); 37 Long userId = getLoginUserId();
31 if (userId == null) { 38 if (userId == null) {
32 return false; 39 return false;
@@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
41 48
42 @Override 49 @Override
43 public boolean hasAnyRoles(String... roles) { 50 public boolean hasAnyRoles(String... roles) {
  51 + // 特殊:跨租户访问
  52 + if (skipPermissionCheck()) {
  53 + return true;
  54 + }
  55 +
  56 + // 权限校验
44 Long userId = getLoginUserId(); 57 Long userId = getLoginUserId();
45 if (userId == null) { 58 if (userId == null) {
46 return false; 59 return false;
@@ -55,6 +68,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @@ -55,6 +68,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
55 68
56 @Override 69 @Override
57 public boolean hasAnyScopes(String... scope) { 70 public boolean hasAnyScopes(String... scope) {
  71 + // 特殊:跨租户访问
  72 + if (skipPermissionCheck()) {
  73 + return true;
  74 + }
  75 +
  76 + // 权限校验
58 LoginUser user = SecurityFrameworkUtils.getLoginUser(); 77 LoginUser user = SecurityFrameworkUtils.getLoginUser();
59 if (user == null) { 78 if (user == null) {
60 return false; 79 return false;
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
1 package cn.iocoder.yudao.framework.security.core.util; 1 package cn.iocoder.yudao.framework.security.core.util;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.ObjUtil;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.security.core.LoginUser; 6 import cn.iocoder.yudao.framework.security.core.LoginUser;
6 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; 7 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@@ -137,4 +138,21 @@ public class SecurityFrameworkUtils { @@ -137,4 +138,21 @@ public class SecurityFrameworkUtils {
137 return authenticationToken; 138 return authenticationToken;
138 } 139 }
139 140
  141 + /**
  142 + * 是否条件跳过权限校验,包括数据权限、功能权限
  143 + *
  144 + * @return 是否跳过
  145 + */
  146 + public static boolean skipPermissionCheck() {
  147 + LoginUser loginUser = getLoginUser();
  148 + if (loginUser == null) {
  149 + return false;
  150 + }
  151 + if (loginUser.getVisitTenantId() == null) {
  152 + return false;
  153 + }
  154 + // 重点:跨租户访问时,无法进行权限校验
  155 + return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId());
  156 + }
  157 +
140 } 158 }
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
1 package cn.iocoder.yudao.framework.web.core.util; 1 package cn.iocoder.yudao.framework.web.core.util;
2 2
3 import cn.hutool.core.util.NumberUtil; 3 import cn.hutool.core.util.NumberUtil;
4 -import cn.hutool.extra.servlet.ServletUtil;  
5 import cn.iocoder.yudao.framework.common.enums.TerminalEnum; 4 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
6 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
7 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;  
9 import cn.iocoder.yudao.framework.web.config.WebProperties; 7 import cn.iocoder.yudao.framework.web.config.WebProperties;
  8 +import jakarta.servlet.ServletRequest;
  9 +import jakarta.servlet.http.HttpServletRequest;
10 import org.springframework.web.context.request.RequestAttributes; 10 import org.springframework.web.context.request.RequestAttributes;
11 import org.springframework.web.context.request.RequestContextHolder; 11 import org.springframework.web.context.request.RequestContextHolder;
12 import org.springframework.web.context.request.ServletRequestAttributes; 12 import org.springframework.web.context.request.ServletRequestAttributes;
13 13
14 -import jakarta.servlet.ServletRequest;  
15 -import jakarta.servlet.http.HttpServletRequest;  
16 -  
17 /** 14 /**
18 * 专属于 web 包的工具类 15 * 专属于 web 包的工具类
19 * 16 *
@@ -27,6 +24,7 @@ public class WebFrameworkUtils { @@ -27,6 +24,7 @@ public class WebFrameworkUtils {
27 private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; 24 private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
28 25
29 public static final String HEADER_TENANT_ID = "tenant-id"; 26 public static final String HEADER_TENANT_ID = "tenant-id";
  27 + public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
30 28
31 /** 29 /**
32 * 终端的 Header 30 * 终端的 Header
@@ -53,6 +51,18 @@ public class WebFrameworkUtils { @@ -53,6 +51,18 @@ public class WebFrameworkUtils {
53 return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; 51 return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
54 } 52 }
55 53
  54 + /**
  55 + * 获得访问的租户编号,从 header 中
  56 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
  57 + *
  58 + * @param request 请求
  59 + * @return 租户编号
  60 + */
  61 + public static Long getVisitTenantId(HttpServletRequest request) {
  62 + String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID);
  63 + return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null;
  64 + }
  65 +
56 public static void setLoginUserId(ServletRequest request, Long userId) { 66 public static void setLoginUserId(ServletRequest request, Long userId) {
57 request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); 67 request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
58 } 68 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http
@@ -3,3 +3,9 @@ GET {{baseUrl}}/system/user/page?pageNo=1&amp;pageSize=10 @@ -3,3 +3,9 @@ GET {{baseUrl}}/system/user/page?pageNo=1&amp;pageSize=10
3 Authorization: Bearer {{token}} 3 Authorization: Bearer {{token}}
4 #Authorization: Bearer test100 4 #Authorization: Bearer test100
5 tenant-id: {{adminTenantId}} 5 tenant-id: {{adminTenantId}}
  6 +
  7 +### 请求 /system/user/page 接口(测试访问别的租户)
  8 +GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
  9 +Authorization: Bearer {{token}}
  10 +tenant-id: {{adminTenantId}}
  11 +visit-tenant-id: 122
6 \ No newline at end of file 12 \ No newline at end of file
yudao-server/src/main/resources/application.yaml
@@ -274,6 +274,9 @@ yudao: @@ -274,6 +274,9 @@ yudao:
274 enable: true 274 enable: true
275 ignore-urls: 275 ignore-urls:
276 - /jmreport/* # 积木报表,无法携带租户编号 276 - /jmreport/* # 积木报表,无法携带租户编号
  277 + ignore-visit-urls:
  278 + - /admin-api/system/user/profile/**
  279 + - /admin-api/system/auth/**
277 ignore-tables: 280 ignore-tables:
278 ignore-caches: 281 ignore-caches:
279 - user_role_ids 282 - user_role_ids