SpringBoot + 多租户规则沙箱隔离:数据串号、规则越权?彻底杜绝的架构设计!

相信很多小伙伴在开发多租户系统时都遇到过这样的糟心事:租户 A 的数据莫名其妙出现在了租户 B 的屏幕上,或者某个恶意的租户通过构造特殊的规则代码,越权访问了其他租户的数据。这些问题不仅会导致业务逻辑错误,更可能引发严重的数据泄露和安全事故。

特别是在规则引擎场景下,每个租户都有自己的业务规则,如果规则执行时没有做好隔离,就可能出现数据串号、规则越权等问题。曾经某知名 SaaS 平台就因为租户隔离不完善,导致租户 A 可以通过修改参数查看租户 B 的敏感数据,最终造成了不可挽回的损失。

那么,有没有一种方式能从根本上杜绝这些问题?今天我就跟大家分享一套基于 SpringBoot 的多租户规则沙箱隔离方案,从架构层面彻底解决数据串号和规则越权的问题。

为什么需要多租户规则沙箱隔离?

先来说说我们面临的挑战。在多租户系统中,规则引擎面临着严峻的安全考验:

  1. 数据串号风险:规则执行时,如果租户上下文传递不正确,可能导致租户 A 的数据被租户 B 获取
  2. 规则越权访问:恶意租户可能通过构造特殊的规则代码,尝试访问其他租户的数据
  3. 规则注入攻击:租户编写的规则可能包含恶意代码,试图获取系统权限
  4. 资源竞争问题:多个租户的规则同时执行时,可能产生资源竞争,影响系统稳定性
  5. 性能隔离不足:某个租户的高负载规则可能影响其他租户的性能

数据串号和规则越权的危害包括:

  • 隐私数据泄露,严重违反数据安全法规
  • 业务逻辑错误,导致订单、金额等关键数据处理失误
  • 系统安全风险,可能被恶意利用造成更大损失
  • 合规风险,不满足等保、金融监管等要求

整体架构设计

我们的多租户规则沙箱隔离方案由以下几个核心组件构成:

  1. TenantContext 租户上下文:ThreadLocal 实现的租户上下文传递
  2. TenantIsolationRuleEngine 隔离规则引擎:带租户隔离的规则执行引擎
  3. TenantRuleSandbox 租户规则沙箱:安全的规则执行环境,防止越权和注入
  4. TenantDataFilter 租户数据过滤器:自动过滤跨租户数据的拦截器
  5. TenantRulePermissionManager 租户规则权限管理器:管理租户规则执行权限

让我们看看如何在 SpringBoot 中实现这套隔离系统:

1. 创建租户上下文

首先实现租户上下文的传递机制:

public class TenantContext {
    
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
    private static final ThreadLocal<Map<String, Object>> TENANT_ATTRIBUTES = ThreadLocal.withInitial(HashMap::new);
    
    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    
    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }
    
    public static void setAttribute(String key, Object value) {
        TENANT_ATTRIBUTES.get().put(key, value);
    }
    
    public static Object getAttribute(String key) {
        return TENANT_ATTRIBUTES.get().get(key);
    }
    
    public static void clear() {
        CURRENT_TENANT.remove();
        TENANT_ATTRIBUTES.remove();
    }
}

2. 创建租户上下文拦截器

通过拦截器自动设置租户上下文:

@Component
public class TenantContextInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenantId = request.getHeader("X-Tenant-ID");
        if (tenantId != null && !tenantId.isEmpty()) {
            TenantContext.setTenantId(tenantId);
        }
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        TenantContext.clear();
    }
}

注册拦截器:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private TenantContextInterceptor tenantContextInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantContextInterceptor)
            .addPathPatterns("/api/**");
    }
}

3. 创建租户数据过滤器

实现租户数据的自动过滤,防止数据串号:

@Component
@Slf4j
public class TenantDataFilter {
    
    /**
     * 校验数据所属租户
     */
    public boolean validateDataOwnership(Object data) {
        if (data == null) {
            return true;
        }
        
        String currentTenantId = TenantContext.getTenantId();
        if (currentTenantId == null) {
            log.warn("数据校验失败:当前租户上下文为空");
            return false;
        }
        
        return validateOwnershipInternal(data, currentTenantId);
    }
    
    private boolean validateOwnershipInternal(Object data, String tenantId) {
        if (data instanceof TenantAware) {
            TenantAware tenantAware = (TenantAware) data;
            if (!tenantId.equals(tenantAware.getTenantId())) {
                log.error("数据租户不匹配,期望: {}, 实际: {}", tenantId, tenantAware.getTenantId());
                return false;
            }
        }
        
        if (data instanceof Collection) {
            for (Object item : (Collection<?>) data) {
                if (!validateOwnershipInternal(item, tenantId)) {
                    return false;
                }
            }
        }
        
        return true;
    }
    
    /**
     * 过滤跨租户数据
     */
    public <T> T filterCrossTenantData(T data) {
        if (data == null) {
            return null;
        }
        
        String currentTenantId = TenantContext.getTenantId();
        if (currentTenantId == null) {
            return data;
        }
        
        return filterDataInternal(data, currentTenantId);
    }
    
    @SuppressWarnings("unchecked")
    private <T> T filterDataInternal(T data, String tenantId) {
        if (data instanceof TenantAware) {
            TenantAware tenantAware = (TenantAware) data;
            if (!tenantId.equals(tenantAware.getTenantId())) {
                log.warn("过滤跨租户数据,当前租户: {}, 数据租户: {}", tenantId, tenantAware.getTenantId());
                return null;
            }
            return data;
        }
        
        if (data instanceof Collection) {
            List<Object> filteredList = new ArrayList<>();
            for (Object item : (Collection<?>) data) {
                Object filtered = filterDataInternal(item, tenantId);
                if (filtered != null) {
                    filteredList.add(filtered);
                }
            }
            return (T) filteredList;
        }
        
        if (data instanceof Map) {
            Map<String, Object> resultMap = new HashMap<>();
            for (Map.Entry<String, Object> entry : ((Map<String, Object>) data).entrySet()) {
                Object filtered = filterDataInternal(entry.getValue(), tenantId);
                if (filtered != null) {
                    resultMap.put(entry.getKey(), filtered);
                }
            }
            return (T) resultMap;
        }
        
        return data;
    }
    
    /**
     * 标记数据所属租户
     */
    public <T extends TenantAware> T markTenantId(T data, String tenantId) {
        if (data != null) {
            data.setTenantId(tenantId);
        }
        return data;
    }
}

4. 创建租户感知接口

定义租户感知的数据接口:

public interface TenantAware {
    String getTenantId();
    void setTenantId(String tenantId);
}

创建租户感知的数据载体:

@Data
public abstract class TenantAwareEntity implements TenantAware {
    private String tenantId;
}

@Data
public class Order extends TenantAwareEntity {
    private Long id;
    private String orderNo;
    private BigDecimal amount;
    private String status;
}

5. 创建规则沙箱

实现安全的规则执行沙箱:

@Slf4j
public class TenantRuleSandbox {
    
    private final String tenantId;
    private final ExpressRunner runner;
    private final Set<String> allowedFunctions;
    private final Set<String> allowedClasses;
    
    public TenantRuleSandbox(String tenantId) {
        this.tenantId = tenantId;
        this.runner = new ExpressRunner();
        this.allowedFunctions = new HashSet<>();
        this.allowedClasses = new HashSet<>();
        configureSandbox();
    }
    
    private void configureSandbox() {
        runner.setTrace(false);
        runner.setCache(true);
        runner.setOptimize(true);
        
        allowedFunctions.add("print");
        allowedFunctions.add("if");
        allowedFunctions.add("while");
        allowedFunctions.add("for");
        
        allowedClasses.add("java.lang.Math");
        allowedClasses.add("java.util.HashMap");
        allowedClasses.add("java.util.ArrayList");
    }
    
    /**
     * 执行规则
     */
    public Object executeRule(String rule, Map<String, Object> context) throws Exception {
        String currentTenantId = TenantContext.getTenantId();
        if (!tenantId.equals(currentTenantId)) {
            log.error("租户上下文不匹配,执行被拒绝,规则租户: {}, 当前租户: {}", tenantId, currentTenantId);
            throw new SecurityException("租户上下文不匹配,规则执行被拒绝");
        }
        
        validateRule(rule);
        
        Map<String, Object> sandboxContext = new HashMap<>(context);
        sandboxContext.put("__tenantId__", tenantId);
        
        return runner.execute(rule, sandboxContext, null, true, false);
    }
    
    /**
     * 校验规则安全性
     */
    private void validateRule(String rule) {
        String lowerRule = rule.toLowerCase();
        
        if (lowerRule.contains("system") || lowerRule.contains("runtime")) {
            throw new SecurityException("规则包含禁止的关键字");
        }
        
        if (lowerRule.contains("class.forname") || lowerRule.contains("reflection")) {
            throw new SecurityException("规则包含反射操作");
        }
        
        if (lowerRule.contains("thread") || lowerRule.contains("process")) {
            throw new SecurityException("规则包含多线程操作");
        }
        
        log.debug("规则安全校验通过: {}", rule);
    }
    
    /**
     * 执行预编译规则
     */
    public Object executePrecompiledRule(InstructionSet instructionSet, Map<String, Object> context) throws Exception {
        String currentTenantId = TenantContext.getTenantId();
        if (!tenantId.equals(currentTenantId)) {
            log.error("租户上下文不匹配,执行被拒绝");
            throw new SecurityException("租户上下文不匹配,规则执行被拒绝");
        }
        
        Map<String, Object> sandboxContext = new HashMap<>(context);
        sandboxContext.put("__tenantId__", tenantId);
        
        return runner.execute(instructionSet, sandboxContext, null, true, false);
    }
}

6. 创建租户规则服务

实现租户隔离的规则服务:

@Service
@Slf4j
public class TenantRuleService {
    
    private final Map<String, TenantRuleSandbox> sandboxCache = new ConcurrentHashMap<>();
    private final RuleStore ruleStore;
    private final TenantDataFilter tenantDataFilter;
    
    public TenantRuleService(RuleStore ruleStore, TenantDataFilter tenantDataFilter) {
        this.ruleStore = ruleStore;
        this.tenantDataFilter = tenantDataFilter;
    }
    
    /**
     * 执行租户规则
     */
    public Object executeRule(String ruleId, Map<String, Object> context) {
        String tenantId = TenantContext.getTenantId();
        if (tenantId == null) {
            throw new IllegalStateException("租户上下文不能为空");
        }
        
        TenantRuleSandbox sandbox = getSandbox(tenantId);
        StoredRule storedRule = ruleStore.getRule(ruleId);
        
        if (storedRule == null) {
            throw new IllegalArgumentException("规则不存在: " + ruleId);
        }
        
        if (!tenantId.equals(storedRule.getTenantId())) {
            log.error("规则越权访问,规则租户: {}, 当前租户: {}", storedRule.getTenantId(), tenantId);
            throw new SecurityException("规则越权访问被拒绝");
        }
        
        try {
            Object result = sandbox.executeRule(storedRule.getRule(), context);
            
            if (result instanceof TenantAware) {
                tenantDataFilter.validateDataOwnership(result);
            }
            
            return result;
        } catch (Exception e) {
            log.error("规则执行失败: {}", ruleId, e);
            throw new RuntimeException("规则执行失败", e);
        }
    }
    
    /**
     * 获取租户沙箱
     */
    private TenantRuleSandbox getSandbox(String tenantId) {
        return sandboxCache.computeIfAbsent(tenantId, TenantRuleSandbox::new);
    }
    
    /**
     * 注册租户规则
     */
    public void registerRule(String ruleId, String rule, String tenantId) {
        TenantRuleSandbox sandbox = getSandbox(tenantId);
        sandbox.executeRule(rule, new HashMap<>());
        
        ruleStore.addRule(new StoredRule(ruleId, rule, tenantId));
        log.info("租户规则注册成功: {}, 租户: {}", ruleId, tenantId);
    }
    
    /**
     * 更新租户规则
     */
    public void updateRule(String ruleId, String newRule) {
        String tenantId = TenantContext.getTenantId();
        StoredRule storedRule = ruleStore.getRule(ruleId);
        
        if (storedRule == null) {
            throw new IllegalArgumentException("规则不存在: " + ruleId);
        }
        
        if (!tenantId.equals(storedRule.getTenantId())) {
            throw new SecurityException("规则越权更新被拒绝");
        }
        
        TenantRuleSandbox sandbox = getSandbox(tenantId);
        sandbox.executeRule(newRule, new HashMap<>());
        
        ruleStore.updateRule(new StoredRule(ruleId, newRule, tenantId));
        log.info("租户规则更新成功: {}, 租户: {}", ruleId, tenantId);
    }
    
    /**
     * 删除租户规则
     */
    public void deleteRule(String ruleId) {
        String tenantId = TenantContext.getTenantId();
        StoredRule storedRule = ruleStore.getRule(ruleId);
        
        if (storedRule == null) {
            return;
        }
        
        if (!tenantId.equals(storedRule.getTenantId())) {
            throw new SecurityException("规则越权删除被拒绝");
        }
        
        ruleStore.removeRule(ruleId);
        log.info("租户规则删除成功: {}, 租户: {}", ruleId, tenantId);
    }
}

7. 创建规则存储

实现规则存储:

@Data
@AllArgsConstructor
public class StoredRule {
    private String ruleId;
    private String rule;
    private String tenantId;
}

@Component
public class RuleStore {
    
    private final Map<String, StoredRule> rules = new ConcurrentHashMap<>();
    
    public StoredRule getRule(String ruleId) {
        return rules.get(ruleId);
    }
    
    public void addRule(StoredRule rule) {
        rules.put(rule.getRuleId(), rule);
    }
    
    public void updateRule(StoredRule rule) {
        rules.put(rule.getRuleId(), rule);
    }
    
    public void removeRule(String ruleId) {
        rules.remove(ruleId);
    }
    
    public List<StoredRule> getRulesByTenant(String tenantId) {
        return rules.values().stream()
            .filter(r -> tenantId.equals(r.getTenantId()))
            .collect(Collectors.toList());
    }
}

8. 创建 Controller

提供 REST API 接口:

@RestController
@RequestMapping("/api/tenant/rule")
@Slf4j
public class TenantRuleController {
    
    @Autowired
    private TenantRuleService tenantRuleService;
    
    @PostMapping("/register")
    public ResponseEntity<?> registerRule(@RequestBody TenantRuleRequest request) {
        try {
            String tenantId = TenantContext.getTenantId();
            tenantRuleService.registerRule(request.getRuleId(), request.getRule(), tenantId);
            return ResponseEntity.ok(ApiResponse.success("规则注册成功"));
        } catch (Exception e) {
            log.error("注册规则失败", e);
            return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
        }
    }
    
    @PostMapping("/execute")
    public ResponseEntity<?> executeRule(@RequestBody ExecuteRequest request) {
        try {
            Object result = tenantRuleService.executeRule(request.getRuleId(), request.getContext());
            return ResponseEntity.ok(ExecuteResponse.success(result));
        } catch (Exception e) {
            log.error("执行规则失败", e);
            return ResponseEntity.badRequest().body(ExecuteResponse.error(e.getMessage()));
        }
    }
    
    @PostMapping("/update")
    public ResponseEntity<?> updateRule(@RequestBody TenantRuleRequest request) {
        try {
            tenantRuleService.updateRule(request.getRuleId(), request.getRule());
            return ResponseEntity.ok(ApiResponse.success("规则更新成功"));
        } catch (Exception e) {
            log.error("更新规则失败", e);
            return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
        }
    }
    
    @DeleteMapping("/{ruleId}")
    public ResponseEntity<?> deleteRule(@PathVariable String ruleId) {
        try {
            tenantRuleService.deleteRule(ruleId);
            return ResponseEntity.ok(ApiResponse.success("规则删除成功"));
        } catch (Exception e) {
            log.error("删除规则失败", e);
            return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
        }
    }
    
    @Data
    public static class TenantRuleRequest {
        private String ruleId;
        private String rule;
    }
    
    @Data
    public static class ExecuteRequest {
        private String ruleId;
        private Map<String, Object> context;
    }
    
    @Data
    @Builder
    public static class ApiResponse {
        private boolean success;
        private String message;
        
        public static ApiResponse success(String message) {
            return ApiResponse.builder().success(true).message(message).build();
        }
        
        public static ApiResponse error(String message) {
            return ApiResponse.builder().success(false).message(message).build();
        }
    }
    
    @Data
    @Builder
    public static class ExecuteResponse {
        private boolean success;
        private Object result;
        private String error;
        
        public static ExecuteResponse success(Object result) {
            return ExecuteResponse.builder().success(true).result(result).build();
        }
        
        public static ExecuteResponse error(String error) {
            return ExecuteResponse.builder().success(false).error(error).build();
        }
    }
}

9. 配置文件

配置应用参数:

tenant:
  isolation:
    enabled: true
    strict-mode: true
    allow-cross-tenant-query: false

spring:
  application:
    name: multi-tenant-rule-engine

logging:
  level:
    com.example.multitenant: DEBUG

server:
  port: 8080

实际应用效果

通过这套方案,我们可以实现:

数据隔离效果

  • 租户 A 只能看到自己的数据,访问租户 B 的数据会被拒绝
  • 规则执行时自动注入租户上下文,防止数据串号
  • 跨租户的数据访问会被实时拦截和过滤

规则安全效果

  • 每个租户有独立的规则沙箱,防止规则越权
  • 规则代码经过安全校验,防止注入攻击
  • 租户只能操作自己注册的规则

隔离效果对比

风险类型无隔离方案沙箱隔离方案
数据串号高风险完全杜绝
规则越权高风险完全杜绝
规则注入高风险有效防护
资源竞争存在完全隔离

最佳实践建议

  1. 租户上下文管理

    • 在请求入口处统一设置租户上下文
    • 请求结束时务必清理 ThreadLocal,避免内存泄漏
    • 使用拦截器自动化处理,降低遗漏风险
  2. 数据权限校验

    • 所有数据操作必须经过租户校验
    • 实现 TenantAware 接口的数据必须校验租户归属
    • 批量操作时逐条校验,防止部分数据越权
  3. 规则安全管理

    • 禁止在规则中使用反射和系统调用
    • 限制规则可访问的类和方法
    • 对规则进行语法校验和安全扫描
  4. 监控和审计

    • 记录所有租户切换和权限校验日志
    • 监控异常的数据访问和规则执行
    • 定期审计租户数据访问记录
  5. 故障排查

    • 保留租户上下文传递的完整链路日志
    • 对权限拒绝事件进行告警
    • 定期进行租户隔离的演练和测试

总结

通过 SpringBoot + 租户上下文 + 规则沙箱的组合,我们可以构建一套完善的多租户规则隔离系统。这套方案具有以下优点:

  • 彻底杜绝数据串号:通过租户上下文自动注入和数据校验,确保每个租户只能访问自己的数据
  • 完全隔离规则执行:每个租户有独立的规则沙箱,防止规则越权和注入攻击
  • 架构层面的安全:从系统架构设计上解决多租户隔离问题,而不是在业务代码中层层校验
  • 性能影响最小化:使用 ThreadLocal 和缓存机制,对系统性能影响极小
  • 易于维护和扩展:集中管理租户隔离逻辑,便于维护和功能扩展

在 SaaS、金融、医疗等对数据安全要求极高的场景中,这套方案可以有效保障多租户数据隔离,满足各种合规要求。

希望这篇文章能对你有所帮助,如果你觉得有用,欢迎关注"服务端技术精选",我会持续分享更多实用的技术干货。


标题:SpringBoot + 多租户规则沙箱隔离:数据串号、规则越权?彻底杜绝的架构设计!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/05/05/1777189229627.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消