SpringBoot + 规则执行沙箱 + 超时熔断:防止脚本死循环拖垮整个服务

今天咱们聊聊一个在动态规则执行中非常关键的安全话题:如何防止恶意脚本拖垮整个服务。

动态规则执行的风险

在我们的日常开发工作中,经常会遇到这样的场景:

  • 业务规则用Groovy或JavaScript脚本编写,支持动态加载
  • 运营人员配置的规则逻辑中包含死循环
  • 复杂的业务逻辑导致脚本执行时间过长
  • 恶意用户故意构造复杂表达式,消耗服务器资源

这类问题看似简单,但一旦发生,可能导致整个服务不可用,影响所有用户。今天我们就来聊聊如何构建安全的规则执行环境。

沙箱执行的重要性

相比传统的脚本执行方式,沙箱执行有以下显著优势:

  • 资源隔离:限制脚本对系统资源的访问
  • 超时控制:防止长时间运行的脚本
  • 安全防护:阻止恶意代码执行危险操作
  • 稳定性保障:避免单个脚本影响整体服务

核心实现方案

1. 规则执行沙箱

@Component
public class RuleExecutionSandbox {
    
    private final ExecutorService executorService;
    
    public RuleExecutionSandbox() {
        // 创建专用线程池,避免影响主线程
        this.executorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors(),
            r -> {
                Thread t = new Thread(r, "rule-execution-thread");
                t.setDaemon(true); // 设置为守护线程
                return t;
            }
        );
    }
    
    public RuleExecutionResult executeRule(String script, Map<String, Object> context) {
        CompletableFuture<RuleExecutionResult> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 创建安全的脚本执行环境
                ScriptEngine engine = createSecureScriptEngine();
                
                // 设置执行上下文
                Bindings bindings = engine.createBindings();
                bindings.putAll(context);
                
                // 执行脚本
                Object result = engine.eval(script, bindings);
                
                return RuleExecutionResult.success(result);
            } catch (Exception e) {
                return RuleExecutionResult.failure("脚本执行异常: " + e.getMessage());
            }
        }, executorService);
        
        try {
            // 设置超时时间
            return future.get(3, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            // 超时处理
            future.cancel(true);
            return RuleExecutionResult.timeout("规则执行超时");
        } catch (Exception e) {
            return RuleExecutionResult.failure("执行异常: " + e.getMessage());
        }
    }
    
    private ScriptEngine createSecureScriptEngine() {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("groovy");
        
        // 配置安全限制
        if (engine instanceof GroovyScriptEngine) {
            GroovyShell shell = new GroovyShell();
            CompilerConfiguration config = new CompilerConfiguration();
            
            // 限制允许的类
            ImportCustomizer importCustomizer = new ImportCustomizer();
            importCustomizer.addStarImports("java.lang", "java.util");
            config.addCompilationCustomizers(importCustomizer);
            
            shell.setVariable("context", new SecureContext());
            engine.setContext(new SimpleScriptContext());
        }
        
        return engine;
    }
}

2. 脚本安全限制

public class SecureScriptEngine {
    
    public static class RestrictedClassLoader extends ClassLoader {
        private final Set<String> allowedClasses = new HashSet<>();
        
        public RestrictedClassLoader() {
            // 允许的基础类
            allowedClasses.addAll(Arrays.asList(
                "java.lang.String",
                "java.lang.Integer", 
                "java.lang.Long",
                "java.lang.Double",
                "java.util.Map",
                "java.util.HashMap",
                "java.util.List",
                "java.util.ArrayList",
                "java.math.BigDecimal"
            ));
        }
        
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (!isAllowedClass(name)) {
                throw new SecurityException("不允许加载类: " + name);
            }
            return super.loadClass(name, resolve);
        }
        
        private boolean isAllowedClass(String className) {
            return allowedClasses.contains(className) || 
                   className.startsWith("java.lang.") ||
                   className.startsWith("java.util.");
        }
    }
    
    public static class SecureContext {
        // 只暴露安全的方法
        public String formatString(String template, Object... args) {
            return String.format(template, args);
        }
        
        public BigDecimal calculate(BigDecimal a, String operator, BigDecimal b) {
            switch (operator) {
                case "+":
                    return a.add(b);
                case "-":
                    return a.subtract(b);
                case "*":
                    return a.multiply(b);
                case "/":
                    return a.divide(b, 2, RoundingMode.HALF_UP);
                default:
                    throw new IllegalArgumentException("不支持的操作符: " + operator);
            }
        }
    }
}

3. 熔断机制实现

@Component
public class RuleCircuitBreaker {
    
    private final Map<String, CircuitBreaker> circuitBreakers = new ConcurrentHashMap<>();
    
    public RuleExecutionResult executeWithCircuitBreaker(String ruleId, Supplier<RuleExecutionResult> execution) {
        CircuitBreaker breaker = circuitBreakers.computeIfAbsent(ruleId, this::createCircuitBreaker);
        
        try {
            return breaker.executeSupplier(execution);
        } catch (CallNotPermittedException e) {
            return RuleExecutionResult.failure("熔断器开启,暂时跳过规则执行");
        }
    }
    
    private CircuitBreaker createCircuitBreaker(String ruleId) {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50) // 失败率阈值50%
            .waitDurationInOpenState(Duration.ofSeconds(30)) // 开启状态等待时间
            .permittedNumberOfCallsInHalfOpenState(3) // 半开状态允许调用次数
            .slidingWindowSize(10) // 滑动窗口大小
            .build();
        
        return CircuitBreaker.of(ruleId, config);
    }
}

4. 资源监控与限制

@Component
public class RuleResourceMonitor {
    
    private final MeterRegistry meterRegistry;
    private final Semaphore executionPermit; // 执行许可信号量
    
    public RuleResourceMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        // 限制并发执行数量
        this.executionPermit = new Semaphore(10);
    }
    
    public RuleExecutionResult executeWithResourceControl(String ruleId, 
                                                         String script, 
                                                         Map<String, Object> context) {
        try {
            // 尝试获取执行许可
            if (!executionPermit.tryAcquire(1, TimeUnit.SECONDS)) {
                return RuleExecutionResult.failure("并发执行数达到上限");
            }
            
            // 记录资源使用情况
            Timer.Sample sample = Timer.start(meterRegistry);
            
            try {
                RuleExecutionResult result = executeInSandbox(script, context);
                
                // 记录执行指标
                sample.stop(Timer.builder("rule.execution.time")
                         .tag("rule_id", ruleId)
                         .tag("success", String.valueOf(result.isSuccess()))
                         .register(meterRegistry));
                
                return result;
            } finally {
                // 释放许可
                executionPermit.release();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return RuleExecutionResult.failure("获取执行许可被中断");
        }
    }
}

高级安全特性

1. AST语法检查

public class AstSecurityChecker {
    
    public boolean validateScript(String script) {
        try {
            // 解析Groovy脚本的AST
            ParserConfiguration config = new ParserConfiguration();
            StaticTypesTransformation staticTypes = new StaticTypesTransformation();
            
            // 检查是否存在危险操作
            return !containsDangerousOperations(script);
        } catch (Exception e) {
            return false;
        }
    }
    
    private boolean containsDangerousOperations(String script) {
        // 检查是否存在系统调用、文件操作等危险代码
        String lowerScript = script.toLowerCase();
        
        return lowerScript.contains("runtime.") ||
               lowerScript.contains("processbuilder") ||
               lowerScript.contains("file.") ||
               lowerScript.contains("system.") ||
               lowerScript.contains(".exec(") ||
               lowerScript.contains(".exit(") ||
               lowerScript.contains("import java.io") ||
               lowerScript.contains("import java.net.socket");
    }
}

2. 内存使用监控

public class MemorySafeExecutor {
    
    private static final long MAX_HEAP_USAGE_PERCENTAGE = 80;
    
    public RuleExecutionResult executeWithMemoryCheck(String script, Map<String, Object> context) {
        // 检查当前堆内存使用率
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapMemory = memoryBean.getHeapMemoryUsage();
        long used = heapMemory.getUsed();
        long max = heapMemory.getMax();
        
        double usagePercentage = (double) used / max * 100;
        
        if (usagePercentage > MAX_HEAP_USAGE_PERCENTAGE) {
            return RuleExecutionResult.failure("系统内存使用率过高,暂停规则执行");
        }
        
        // 强制GC清理
        if (usagePercentage > 70) {
            System.gc();
        }
        
        return executeInSandbox(script, context);
    }
}

3. 完整的规则执行服务

@Service
public class SecureRuleService {
    
    @Autowired
    private RuleExecutionSandbox sandbox;
    
    @Autowired
    private RuleCircuitBreaker circuitBreaker;
    
    @Autowired
    private RuleResourceMonitor resourceMonitor;
    
    @Autowired
    private AstSecurityChecker securityChecker;
    
    public RuleExecutionResult executeRule(String ruleId, String script, Map<String, Object> context) {
        // 1. 语法安全检查
        if (!securityChecker.validateScript(script)) {
            return RuleExecutionResult.failure("脚本包含危险操作,禁止执行");
        }
        
        // 2. 资源控制
        RuleExecutionResult result = resourceMonitor.executeWithResourceControl(
            ruleId, script, context);
        
        if (!result.isSuccess()) {
            return result;
        }
        
        // 3. 熔断保护
        return circuitBreaker.executeWithCircuitBreaker(ruleId, () -> {
            // 4. 沙箱执行
            return sandbox.executeRule(script, context);
        });
    }
}

监控与告警

1. 执行指标监控

@Component
public class RuleExecutionMetrics {
    
    private final MeterRegistry meterRegistry;
    
    public void recordExecution(String ruleId, RuleExecutionResult result, long executionTime) {
        // 记录执行时间
        Timer.builder("rule.execution.time")
             .tag("rule_id", ruleId)
             .tag("result", result.isSuccess() ? "success" : "failure")
             .register(meterRegistry)
             .record(executionTime, TimeUnit.MILLISECONDS);
        
        // 记录执行次数
        Counter.builder("rule.execution.count")
               .tag("rule_id", ruleId)
               .tag("result", result.isSuccess() ? "success" : "failure")
               .register(meterRegistry)
               .increment();
        
        // 记录熔断状态
        if (result.getMessage().contains("熔断器")) {
            Counter.builder("rule.circuitbreaker.tripped")
                   .tag("rule_id", ruleId)
                   .register(meterRegistry)
                   .increment();
        }
    }
}

最佳实践建议

  1. 最小权限原则:只暴露必要的类和方法
  2. 资源限制:设置合理的内存、CPU、时间限制
  3. 监控告警:实时监控脚本执行情况
  4. 熔断降级:异常情况下快速降级
  5. 安全审计:记录所有脚本执行日志

通过这样的沙箱执行环境,我们可以安全地运行动态规则脚本,保护服务免受恶意代码的影响。


以上就是本期分享的内容,希望对你有所帮助。更多技术干货,请关注服务端技术精选,我们下期再见!


标题:SpringBoot + 规则执行沙箱 + 超时熔断:防止脚本死循环拖垮整个服务
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/05/1770094450866.html

    0 评论
avatar