Commit 8795a4cdeb6961824d785afe6c0423289d51b914

Authored by YunaiV
1 parent df9b0684

增加 Tenant Security 的实现

Showing 13 changed files with 129 additions and 14 deletions
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.http
... ... @@ -18,7 +18,7 @@ tenant-id: 1
18 18 ### 请求 /list-menus 接口 => 成功
19 19 GET {{baseUrl}}/list-menus
20 20 Authorization: Bearer {{token}}
21   -#Authorization: Bearer 0d161f69c9ac4c7f836e1b850715a7b0
  21 +#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a
22 22 tenant-id: 1
23 23  
24 24 ### 请求 /druid/xxx 接口 => 失败 TODO 临时测试
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
... ... @@ -17,13 +17,15 @@ public interface WebFilterOrderEnum {
17 17  
18 18 // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
19 19  
20   - int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
  20 + int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
21 21  
22   - int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面
  22 + int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
23 23  
24   - int XSS_FILTER = -80; // 需要保证在 RequestBodyCacheFilter 后面
  24 + int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面
25 25  
26   - // Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类
  26 + // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
  27 +
  28 + int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后
27 29  
28 30 int DEMO_FILTER = Integer.MAX_VALUE;
29 31  
... ...
yudao-framework/yudao-spring-boot-starter-tenant/pom.xml
... ... @@ -27,6 +27,11 @@
27 27 <artifactId>spring-boot-starter-web</artifactId>
28 28 </dependency>
29 29  
  30 + <dependency>
  31 + <groupId>cn.iocoder.boot</groupId>
  32 + <artifactId>yudao-spring-boot-starter-security</artifactId>
  33 + </dependency>
  34 +
30 35 <!-- DB 相关 -->
31 36 <dependency>
32 37 <groupId>cn.iocoder.boot</groupId>
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java
... ... @@ -14,6 +14,15 @@ import java.util.Set;
14 14 @Data
15 15 public class TenantProperties {
16 16  
  17 +// /**
  18 +// * 租户是否开启
  19 +// */
  20 +// private static final Boolean ENABLE_DEFAULT = true;
  21 +//
  22 +// /**
  23 +// * 是否开启
  24 +// */
  25 +// private Boolean enable = ENABLE_DEFAULT;
17 26 /**
18 27 * 需要多租户的表
19 28 *
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java
... ... @@ -8,12 +8,14 @@ import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
8 8 import org.springframework.beans.BeansException;
9 9 import org.springframework.beans.factory.config.BeanPostProcessor;
10 10 import org.springframework.context.annotation.Bean;
  11 +import org.springframework.context.annotation.Configuration;
11 12  
12 13 /**
13 14 * 多租户针对 Job 的自动配置
14 15 *
15 16 * @author 芋道源码
16 17 */
  18 +@Configuration
17 19 public class YudaoTenantJobAutoConfiguration {
18 20  
19 21 @Bean
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.config;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
  4 +import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
  5 +import org.springframework.boot.web.servlet.FilterRegistrationBean;
  6 +import org.springframework.context.annotation.Bean;
  7 +import org.springframework.context.annotation.Configuration;
  8 +
  9 +/**
  10 + * 多租户针对 Web 的自动配置
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Configuration
  15 +public class YudaoTenantSecurityAutoConfiguration {
  16 +
  17 + @Bean
  18 + public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter() {
  19 + FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
  20 + registrationBean.setFilter(new TenantSecurityWebFilter());
  21 + registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
  22 + return registrationBean;
  23 + }
  24 +
  25 +}
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java
... ... @@ -4,12 +4,14 @@ import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
4 4 import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
5 5 import org.springframework.boot.web.servlet.FilterRegistrationBean;
6 6 import org.springframework.context.annotation.Bean;
  7 +import org.springframework.context.annotation.Configuration;
7 8  
8 9 /**
9 10 * 多租户针对 Web 的自动配置
10 11 *
11 12 * @author 芋道源码
12 13 */
  14 +@Configuration
13 15 public class YudaoTenantWebAutoConfiguration {
14 16  
15 17 @Bean
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java
... ... @@ -11,10 +11,28 @@ public class TenantContextHolder {
11 11  
12 12 private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
13 13  
  14 + /**
  15 + * 获得租户编号。
  16 + *
  17 + * @return 租户编号
  18 + */
14 19 public static Long getTenantId() {
15 20 return TENANT_ID.get();
16 21 }
17 22  
  23 + /**
  24 + * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
  25 + *
  26 + * @return 租户编号
  27 + */
  28 + public static Long getRequiredTenantId() {
  29 + Long tenantId = getTenantId();
  30 + if (tenantId == null) {
  31 + throw new NullPointerException("TenantContextHolder 不存在租户编号");
  32 + }
  33 + return tenantId;
  34 + }
  35 +
18 36 public static void setTenantId(Long tenantId) {
19 37 TENANT_ID.set(tenantId);
20 38 }
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java
... ... @@ -20,8 +20,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
20 20  
21 21 @Override
22 22 public Expression getTenantId() {
23   - // TODO 芋艿:暂时不考虑获取不到的情况。此时,会存在 NPE 的报错
24   - return new StringValue(TenantContextHolder.getTenantId().toString());
  23 + return new StringValue(TenantContextHolder.getRequiredTenantId().toString());
25 24 }
26 25  
27 26 @Override
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java
... ... @@ -40,7 +40,7 @@ public class TenantRedisKeyDefine extends RedisKeyDefine {
40 40  
41 41 @Override
42 42 public String formatKey(Object... args) {
43   - args = ArrayUtil.append(args, TenantContextHolder.getTenantId());
  43 + args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
44 44 return super.formatKey(args);
45 45 }
46 46  
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.security;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
  6 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  7 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  8 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.web.filter.OncePerRequestFilter;
  11 +
  12 +import javax.servlet.FilterChain;
  13 +import javax.servlet.ServletException;
  14 +import javax.servlet.http.HttpServletRequest;
  15 +import javax.servlet.http.HttpServletResponse;
  16 +import java.io.IOException;
  17 +import java.util.Objects;
  18 +
  19 +/**
  20 + * 多租户 Security Web 过滤器
  21 + * 校验用户访问的租户,是否是其所在的租户,避免越权问题
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@Slf4j
  26 +public class TenantSecurityWebFilter extends OncePerRequestFilter {
  27 +
  28 + @Override
  29 + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
  30 + throws ServletException, IOException {
  31 + LoginUser user = SecurityFrameworkUtils.getLoginUser();
  32 + assert user != null; // shouldNotFilter 已经校验
  33 + if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
  34 + log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
  35 + user.getTenantId(), user.getId(), user.getUserType(),
  36 + TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
  37 + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
  38 + "您无权访问该租户的数据"));
  39 + return;
  40 + }
  41 + // 继续过滤
  42 + chain.doFilter(request, response);
  43 + }
  44 +
  45 + @Override
  46 + protected boolean shouldNotFilter(HttpServletRequest request) {
  47 + return SecurityFrameworkUtils.getLoginUser() == null;
  48 + }
  49 +
  50 +}
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java
1 1 /**
2 2 * 多租户,支持如下层面:
3 3 * 1. DB:基于 MyBatis Plus 多租户的功能实现。
4   - * 2. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
5   - * 3. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
6   - * 4. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
7   - * 5. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
  4 + * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
  5 + * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
  6 + * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。
  7 + * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
  8 + * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
  9 + * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
8 10 * 1)Spring Async:
9 11 * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
10 12 * 2)Spring Security:
11 13 * TransmittableThreadLocalSecurityContextHolderStrategy
12 14 * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
13   - * 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
  15 + *
14 16 */
15 17 package cn.iocoder.yudao.framework.tenant;
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories
... ... @@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 2 cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\
3 3 cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\
4 4 cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\
5   - cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration
  5 + cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration,\
  6 + cn.iocoder.yudao.framework.tenant.config.YudaoTenantSecurityAutoConfiguration
... ...