1. 场景
Java可以借助javax包接口来运行js,但是运行js时,由于js是属于用户端的输入内容,所以存在很多不确定性,很有安全隐患。
隐患例如:
- js代码存在死循环
- js代码可以操作宿主机上面的功能,删除机器上的文件
- js执行占用过多的java资源
因此,我们应该使用sandbox,具体工具就是利用NashornSandbox来进行沙箱环境运行js。
2. 介绍
2.1 Nashorn的介绍
Nashorn 一个 javascript 引擎。
从JDK 1.8开始,Nashorn取代Rhino(JDK 1.6, JDK1.7)成为Java的嵌入式JavaScript引擎。Nashorn完全支持ECMAScript 5.1规范以及一些扩展。它使用基于JSR 292的新语言特性,其中包含在JDK 7中引入的 invokedynamic,将JavaScript编译成Java字节码。与先前的Rhino实现相比,这带来了2到10倍的性能提升。
2.2 NashornSandbox隔离限制
- 对java类的访问限制
- 对Nashorn引擎的资源限制
3. 实现
3.1 Maven引入
<!-- JS引擎沙盒 START -->
<delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version>
<!-- JS引擎沙盒 END -->
<dependency>
<groupId>org.javadelight</groupId>
<artifactId>delight-nashorn-sandbox</artifactId>
<version>${delight-nashorn-sandbox.version}</version>
</dependency>
3.2 实现类
package wang.fredia.app.api.engine;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import lombok.extern.slf4j.Slf4j;
@SuppressWarnings("restriction")
@Slf4j
public abstract class AbstractNashornJsInvokeService2 extends AbstractJsInvokeService {
private NashornSandbox sandbox;
private ExecutorService monitorExecutorService;
@PostConstruct
public void init() {
sandbox = NashornSandboxes.create();
monitorExecutorService = Executors.newWorkStealingPool(100);
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(100L);
sandbox.allowNoBraces(false);
sandbox.allowLoadFunctions(true);
sandbox.setMaxMemory(100*1024);
sandbox.setMaxPreparedStatements(30);
}
@PreDestroy
public void stop() {
if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow();
}
}
@Override
protected ListenableFuture<UUID> doEval(UUID scriptId, String functionName, String jsScript) {
try {
sandbox.eval(jsScript);
} catch (Exception e) {
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
return Futures.immediateFailedFuture(e);
}
return Futures.immediateFuture(scriptId);
}
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
try {
Object result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
return Futures.immediateFuture(result);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
}
3.3 关键点配置
由于上面代码中包含了过多的细节成分,我们主要关注sandbox的初始化方法,如下
@PostConstruct
public void init() {
sandbox = NashornSandboxes.create();
monitorExecutorService = Executors.newWorkStealingPool(100);
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(100L);
sandbox.allowNoBraces(false);
sandbox.allowLoadFunctions(true);
sandbox.setMaxMemory(100*1024);
sandbox.setMaxPreparedStatements(30);
}
sandbox 初始化时的具体参数使用如下:
- setMaxCPUTime // 设置脚本执行允许的最大CPU时间(以毫秒为单位),超过则会报异常
- setExecutor // 指定执行程序服务,该服务用于在CPU时间运行脚本
- allowNoBraces // 是否允许使用大括号
- allowLoadFunctions // 是否允许nashorn加载全局函数
- setMaxPreparedStatements // LRU初缓存的初始化大小,默认为0
- setMaxMemory // 设置JS执行程序线程可以分配的最大内存(以字节为单位),超过会报ScriptMemoryAbuseException错误
4. 总结
因为当前执行JS的业务场景是在IoT服务内,所以用户在配置规则链时,很容易将规则链里面的Rule(js)写得有问题,这种情况下,没有稳定的隔离机制,一定会发生巨大的生产事故。为确定服务的可用性以及稳定性,必须将可能存在问题都考虑到,尤其是物联网数据监控这样的实时且准确的功能。
评论区