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 12  
13 13 import java.util.List;
14 14  
  15 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
  16 +
15 17 /**
16 18 * 基于 {@link DataPermissionRule} 的数据权限处理器
17 19 *
... ... @@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
27 29  
28 30 @Override
29 31 public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
  32 + // 特殊:跨租户访问
  33 + if (skipPermissionCheck()) {
  34 + return null;
  35 + }
  36 +
30 37 // 获得 Mapper 对应的数据权限的规则
31 38 List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
32 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 32 * @param runnable 逻辑
33 33 */
34 34 public static void executeIgnore(Runnable runnable) {
35   - DataPermission dataPermission = getDisableDataPermissionDisable();
36   - DataPermissionContextHolder.add(dataPermission);
  35 + addDisableDataPermission();
37 36 try {
38 37 // 执行 runnable
39 38 runnable.run();
40 39 } finally {
41   - DataPermissionContextHolder.remove();
  40 + removeDataPermission();
42 41 }
43 42 }
44 43  
... ... @@ -50,14 +49,25 @@ public class DataPermissionUtils {
50 49 */
51 50 @SneakyThrows
52 51 public static <T> T executeIgnore(Callable<T> callable) {
53   - DataPermission dataPermission = getDisableDataPermissionDisable();
54   - DataPermissionContextHolder.add(dataPermission);
  52 + addDisableDataPermission();
55 53 try {
56 54 // 执行 callable
57 55 return callable.call();
58 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 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 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 3 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
5 5 import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
  6 +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
6 7 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
7 8 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
8 9 import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
... ... @@ -15,6 +16,7 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
15 16 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
16 17 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
17 18 import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
  19 +import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor;
18 20 import cn.iocoder.yudao.framework.web.config.WebProperties;
19 21 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
20 22 import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
... ... @@ -36,6 +38,8 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
36 38 import org.springframework.data.redis.connection.RedisConnectionFactory;
37 39 import org.springframework.data.redis.core.RedisTemplate;
38 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 43 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
40 44 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
41 45 import org.springframework.web.util.pattern.PathPattern;
... ... @@ -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 141 // ========== Security ==========
119 142  
120 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 56 */
57 57 @JsonIgnore
58 58 private Map<String, Object> context;
  59 + /**
  60 + * 访问的租户编号
  61 + */
  62 + private Long visitTenantId;
59 63  
60 64 public void setContext(String key, Object value) {
61 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 9 import java.util.Arrays;
10 10  
11 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 15 * 默认的 {@link SecurityFrameworkService} 实现类
... ... @@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
27 28  
28 29 @Override
29 30 public boolean hasAnyPermissions(String... permissions) {
  31 + // 特殊:跨租户访问
  32 + if (skipPermissionCheck()) {
  33 + return true;
  34 + }
  35 +
  36 + // 权限校验
30 37 Long userId = getLoginUserId();
31 38 if (userId == null) {
32 39 return false;
... ... @@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
41 48  
42 49 @Override
43 50 public boolean hasAnyRoles(String... roles) {
  51 + // 特殊:跨租户访问
  52 + if (skipPermissionCheck()) {
  53 + return true;
  54 + }
  55 +
  56 + // 权限校验
44 57 Long userId = getLoginUserId();
45 58 if (userId == null) {
46 59 return false;
... ... @@ -55,6 +68,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
55 68  
56 69 @Override
57 70 public boolean hasAnyScopes(String... scope) {
  71 + // 特殊:跨租户访问
  72 + if (skipPermissionCheck()) {
  73 + return true;
  74 + }
  75 +
  76 + // 权限校验
58 77 LoginUser user = SecurityFrameworkUtils.getLoginUser();
59 78 if (user == null) {
60 79 return false;
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
1 1 package cn.iocoder.yudao.framework.security.core.util;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.ObjUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.framework.security.core.LoginUser;
6 7 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
... ... @@ -137,4 +138,21 @@ public class SecurityFrameworkUtils {
137 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 1 package cn.iocoder.yudao.framework.web.core.util;
2 2  
3 3 import cn.hutool.core.util.NumberUtil;
4   -import cn.hutool.extra.servlet.ServletUtil;
5 4 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
6 5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
7 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8   -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
9 7 import cn.iocoder.yudao.framework.web.config.WebProperties;
  8 +import jakarta.servlet.ServletRequest;
  9 +import jakarta.servlet.http.HttpServletRequest;
10 10 import org.springframework.web.context.request.RequestAttributes;
11 11 import org.springframework.web.context.request.RequestContextHolder;
12 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 15 * 专属于 web 包的工具类
19 16 *
... ... @@ -27,6 +24,7 @@ public class WebFrameworkUtils {
27 24 private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
28 25  
29 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 30 * 终端的 Header
... ... @@ -53,6 +51,18 @@ public class WebFrameworkUtils {
53 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 66 public static void setLoginUserId(ServletRequest request, Long userId) {
57 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 Authorization: Bearer {{token}}
4 4 #Authorization: Bearer test100
5 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 12 \ No newline at end of file
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -274,6 +274,9 @@ yudao:
274 274 enable: true
275 275 ignore-urls:
276 276 - /jmreport/* # 积木报表,无法携带租户编号
  277 + ignore-visit-urls:
  278 + - /admin-api/system/user/profile/**
  279 + - /admin-api/system/auth/**
277 280 ignore-tables:
278 281 ignore-caches:
279 282 - user_role_ids
... ...