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();
}
}
}
最佳实践建议
- 最小权限原则:只暴露必要的类和方法
- 资源限制:设置合理的内存、CPU、时间限制
- 监控告警:实时监控脚本执行情况
- 熔断降级:异常情况下快速降级
- 安全审计:记录所有脚本执行日志
通过这样的沙箱执行环境,我们可以安全地运行动态规则脚本,保护服务免受恶意代码的影响。
以上就是本期分享的内容,希望对你有所帮助。更多技术干货,请关注服务端技术精选,我们下期再见!
标题:SpringBoot + 规则执行沙箱 + 超时熔断:防止脚本死循环拖垮整个服务
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/05/1770094450866.html
0 评论