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


相信很多小伙伴在开发多租户系统时都遇到过这样的糟心事:租户 A 的数据莫名其妙出现在了租户 B 的屏幕上,或者某个恶意的租户通过构造特殊的规则代码,越权访问了其他租户的数据。这些问题不仅会导致业务逻辑错误,更可能引发严重的数据泄露和安全事故。
特别是在规则引擎场景下,每个租户都有自己的业务规则,如果规则执行时没有做好隔离,就可能出现数据串号、规则越权等问题。曾经某知名 SaaS 平台就因为租户隔离不完善,导致租户 A 可以通过修改参数查看租户 B 的敏感数据,最终造成了不可挽回的损失。
那么,有没有一种方式能从根本上杜绝这些问题?今天我就跟大家分享一套基于 SpringBoot 的多租户规则沙箱隔离方案,从架构层面彻底解决数据串号和规则越权的问题。
为什么需要多租户规则沙箱隔离?
先来说说我们面临的挑战。在多租户系统中,规则引擎面临着严峻的安全考验:
- 数据串号风险:规则执行时,如果租户上下文传递不正确,可能导致租户 A 的数据被租户 B 获取
- 规则越权访问:恶意租户可能通过构造特殊的规则代码,尝试访问其他租户的数据
- 规则注入攻击:租户编写的规则可能包含恶意代码,试图获取系统权限
- 资源竞争问题:多个租户的规则同时执行时,可能产生资源竞争,影响系统稳定性
- 性能隔离不足:某个租户的高负载规则可能影响其他租户的性能
数据串号和规则越权的危害包括:
- 隐私数据泄露,严重违反数据安全法规
- 业务逻辑错误,导致订单、金额等关键数据处理失误
- 系统安全风险,可能被恶意利用造成更大损失
- 合规风险,不满足等保、金融监管等要求
整体架构设计
我们的多租户规则沙箱隔离方案由以下几个核心组件构成:
- TenantContext 租户上下文:ThreadLocal 实现的租户上下文传递
- TenantIsolationRuleEngine 隔离规则引擎:带租户隔离的规则执行引擎
- TenantRuleSandbox 租户规则沙箱:安全的规则执行环境,防止越权和注入
- TenantDataFilter 租户数据过滤器:自动过滤跨租户数据的拦截器
- 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 的数据会被拒绝
- 规则执行时自动注入租户上下文,防止数据串号
- 跨租户的数据访问会被实时拦截和过滤
规则安全效果:
- 每个租户有独立的规则沙箱,防止规则越权
- 规则代码经过安全校验,防止注入攻击
- 租户只能操作自己注册的规则
隔离效果对比:
| 风险类型 | 无隔离方案 | 沙箱隔离方案 |
|---|---|---|
| 数据串号 | 高风险 | 完全杜绝 |
| 规则越权 | 高风险 | 完全杜绝 |
| 规则注入 | 高风险 | 有效防护 |
| 资源竞争 | 存在 | 完全隔离 |
最佳实践建议
-
租户上下文管理:
- 在请求入口处统一设置租户上下文
- 请求结束时务必清理 ThreadLocal,避免内存泄漏
- 使用拦截器自动化处理,降低遗漏风险
-
数据权限校验:
- 所有数据操作必须经过租户校验
- 实现 TenantAware 接口的数据必须校验租户归属
- 批量操作时逐条校验,防止部分数据越权
-
规则安全管理:
- 禁止在规则中使用反射和系统调用
- 限制规则可访问的类和方法
- 对规则进行语法校验和安全扫描
-
监控和审计:
- 记录所有租户切换和权限校验日志
- 监控异常的数据访问和规则执行
- 定期审计租户数据访问记录
-
故障排查:
- 保留租户上下文传递的完整链路日志
- 对权限拒绝事件进行告警
- 定期进行租户隔离的演练和测试
总结
通过 SpringBoot + 租户上下文 + 规则沙箱的组合,我们可以构建一套完善的多租户规则隔离系统。这套方案具有以下优点:
- 彻底杜绝数据串号:通过租户上下文自动注入和数据校验,确保每个租户只能访问自己的数据
- 完全隔离规则执行:每个租户有独立的规则沙箱,防止规则越权和注入攻击
- 架构层面的安全:从系统架构设计上解决多租户隔离问题,而不是在业务代码中层层校验
- 性能影响最小化:使用 ThreadLocal 和缓存机制,对系统性能影响极小
- 易于维护和扩展:集中管理租户隔离逻辑,便于维护和功能扩展
在 SaaS、金融、医疗等对数据安全要求极高的场景中,这套方案可以有效保障多租户数据隔离,满足各种合规要求。
希望这篇文章能对你有所帮助,如果你觉得有用,欢迎关注"服务端技术精选",我会持续分享更多实用的技术干货。
标题:SpringBoot + 多租户规则沙箱隔离:数据串号、规则越权?彻底杜绝的架构设计!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/05/05/1777189229627.html
公众号:服务端技术精选
评论