activiti脚本任务
最近的Activiti 5.21.0版本的突出特点之一是“安全脚本”。 Activiti用户指南中详细介绍了启用和使用此功能的方法 。 在这篇文章中,我将向您展示我们如何实现其最终实现以及它在幕后所做的事情。 当然,因为这是我通常的签名风格,所以我们还将对性能进行一些了解。
问题
长期以来,Activiti引擎一直支持脚本任务(和任务/执行侦听器)的脚本编写。 所使用的脚本在流程定义中定义,并且可以在部署流程定义后直接执行。 这是很多人喜欢的东西。 这与Java委托类或委托表达式有很大的不同,因为它们通常需要将实际的逻辑放在类路径上。 它本身已经引入了某种“保护”,因为高级用户通常只能这样做。
但是,使用脚本不需要这种“额外步骤”。 如果您将脚本任务的功能提供给最终用户(并且我们从某些用户那里知道某些公司确实拥有此用例),那么所有的赌注都将大打折扣。 您可以通过执行流程实例来关闭JVM或执行恶意操作。
第二个问题是编写一个无限循环且永无止境的脚本非常容易。 第三个问题是,脚本在执行时可以轻松使用大量内存,并占用大量系统资源。
让我们来看入门的第一个问题。 首先,让我们添加最新和最大的Activiti引擎依赖性以及内存数据库库中的H2:
<dependencies><dependency><groupId>org.activiti</groupId><artifactId>activiti-engine</artifactId><version>5.21.0</version></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>1.3.176</version></dependency>
</dependencies>
我们将在这里使用的过程非常简单:只是一个开始事件,脚本任务和结束。 此处的过程并不是真正的重点,而是脚本执行。
我们将尝试的第一个脚本有两件事:它将获取并显示我的机器的当前网络配置(但显然有此想法的更危险的应用程序), 然后关闭整个JVM 。 当然,在适当的设置中,可以通过确保运行逻辑的用户在计算机上没有任何重要权限(但不能解决占用资源的问题)来缓解其中的某些问题。 但是我认为这很好地说明了为什么向几乎任何人提供脚本的功能在安全性方面确实很糟糕。
<scriptTask id="myScriptTask" scriptFormat="javascript"><script>var s = new java.util.Scanner(java.lang.Runtime.getRuntime().exec("ifconfig").getInputStream()).useDelimiter("\\A");var output = s.hasNext() ? s.next() : "";java.lang.System.out.println("--- output = " + output);java.lang.System.exit(1);</script>
</scriptTask>
让我们部署流程定义并执行流程实例:
public class Demo1 {public static void main (String[] args) {// Build engine and deployProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().buildProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();repositoryService.createDeployment().addClasspathResource("process.bpmn20.xml").deploy();// Start process instanceRuntimeService runtimeService = processEngine.getRuntimeService();runtimeService.startProcessInstanceByKey("myProcess");}
}
给出以下输出(此处缩短):
—输出= eth0链接encap:以太网
inet地址:192.168.0.114广播:192.168.0.255掩码:255.255.255.0
… 流程以退出代码1完成
它输出有关我所有网络接口的信息,然后关闭整个JVM。 是的。 太恐怖了
尝试纳斯霍恩
第一个问题的解决方案是,我们需要将要在脚本中公开的内容列入白名单,并且默认情况下将所有内容都列入黑名单。 这样,用户将无法运行任何可以做恶意事情的类或方法。
在Activiti中,当javascript脚本任务是流程定义的一部分时,我们使用JDK中的ScriptEngine类将此脚本提供给JDK中嵌入的javascript引擎。 在JDK 6/7中是Rhino,在JDK 8中是Nashorn。 我首先进行了认真的搜索,以找到Nashorn的解决方案(因为这将更加适用于未来)。 Nashorn确实具有“类过滤器”概念,可以有效地实施白名单。 但是,ScriptEngine抽象没有任何工具可以实际调整或配置Nashorn引擎。 我们必须做一些底层的魔术才能使其正常工作。
代替使用默认的Nashorn脚本引擎,我们自己在“ SecureScriptTask”(这是常规的JavaDelegate)中实例化Nashorn脚本引擎。 注意使用jdk.nashorn。*包的用法–不太好。 我们遵循https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/api.html中的文档,通过在Nashorn引擎中添加“ ClassFilter”来使脚本执行更加安全。 这实际上是可以在脚本中使用的已批准类的白名单。
public class SafeScriptTaskDemo2 implements JavaDelegate {private Expression script;public void execute(DelegateExecution execution) throws Exception {NashornScriptEngineFactory factory = new NashornScriptEngineFactory();ScriptEngine scriptEngine = factory.getScriptEngine(new SafeClassFilter());ScriptingEngines scriptingEngines = Context.getProcessEngineConfiguration().getScriptingEngines();Bindings bindings = scriptingEngines.getScriptBindingsFactory().createBindings(execution, false);scriptEngine.eval((String) script.getValue(execution), bindings);System.out.println("Java delegate done");}public static class SafeClassFilter implements ClassFilter {public boolean exposeToScripts(String s) {return false;}}}
当执行时,上面的脚本将不会执行,将引发一个异常,指出“主线程”中的异常java.lang.RuntimeException:java.lang.ClassNotFoundException:java.lang.System.out.println”。
请注意,ClassFilter仅可从JDK 1.8.0_40获得(相当近期!)。
但是,这不能解决无限循环的第二个问题。 让我们执行一个简单的脚本:
while (true) {print("Hello");
}
您可以猜测会做什么。 这将永远运行。 如果幸运的话,在脚本任务在事务中执行时,事务超时将发生。 但这远不是一个体面的解决方案,因为它浪费了CPU资源一段时间。
使用大量内存的第三个问题也很容易证明:
var array = []
for(var i = 0; i < 2147483647; ++i) {array.push(i);java.lang.System.out.println(array.length);
}
启动流程实例时,内存将快速填满(仅以几个MB开头):
并最终以OutOfMemoryException: 线程“ main” java.lang.OutOfMemoryError:超出GC开销限制结束
切换到犀牛
在以下示例与上一个示例之间,花费了大量时间使Nashorn以某种方式拦截或应对无限循环/内存使用情况。 但是,经过大量搜索和试验后,似乎这些功能在Nashorn中还不是(还?)。 快速搜索将告诉您,我们不是唯一寻求解决方案的人。 通常,人们提到Rhino确实具有解决此问题的功能。
例如,在JDK <8中,Rhino javascript引擎具有'instructionCount'回调机制,Nashorn中不存在。 它基本上为您提供了一种在回叫中执行逻辑的方法,该回叫被每x条指令( 字节码指令!)自动调用。 我首先尝试(并且浪费了很多时间)用Nashorn模仿指令计数的想法,例如,首先美化脚本(因为人们可以将整个脚本写在一行上),然后在脚本中注入一行代码来触发回调。 但是,那是1)做起来不是很简单2)一个人仍然可以在无限运行/使用大量内存的一行上写一条指令。
搜索被困在那里,导致我们找到了Mozilla的Rhino引擎 。 自从很久以前就将其包含在JDK中以来,它实际上已经进一步发展了,而JDK中的版本并未进行这些更改! 阅读了(相当稀疏的)Rhino文档后,很明显Rhino在我们的用例方面似乎具有更丰富的功能。
Nashorn的ClassFilter与Rhino中的“ ClassShutter”概念匹配。 使用Rhino的回调机制解决了cpu和内存问题:您可以定义一个称为x指令的回调。 这意味着一行可能是数百个字节代码指令,并且每x条指令我们都会得到一个回调……。 在执行脚本时,它非常适合监视我们的cpu和内存使用情况。
如果您对我们在代码中实现这些想法感兴趣, 请在此处查看 。
这确实意味着无论您使用什么JDK版本,都不会使用嵌入式javascript引擎,而会一直使用Rhino。
尝试一下
要使用新的安全脚本功能,请添加以下依赖关系:
<dependency><groupId>org.activiti</groupId><artifactId>activiti-secure-javascript</artifactId><version>5.21.0</version>
</dependency>
这将暂时包括Rhino引擎。 这还将启用SecureJavascriptConfigurator ,在创建流程引擎之前需要对其进行配置:
SecureJavascriptConfigurator configurator = new SecureJavascriptConfigurator().setWhiteListedClasses(new HashSet<String>(Arrays.asList("java.util.ArrayList"))).setMaxStackDepth(10).setMaxScriptExecutionTime(3000L).setMaxMemoryUsed(3145728L).setNrOfInstructionsBeforeStateCheckCallback(10);ProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().addConfigurator(configurator).buildProcessEngine();
这会将安全脚本配置为
- 每10条指令,检查一次CPU执行时间和内存使用情况
- 给脚本3秒3MB的执行时间
- 将堆栈深度限制为10(以避免递归)
- 将数组列表公开为可以在脚本中安全使用的类
从上方运行试图读取ifconfig并关闭JVM的脚本会导致:
TypeError:无法在对象[JavaPackage java.lang.Runtime]中调用属性getRuntime。 它不是功能,而是“对象”。
从上面运行无限循环脚本可以得到
线程“主” java.lang.Error中的异常:最大variableScope时间超过了3000 ms
从上面运行内存使用脚本可以
线程“主” java.lang.Error中的异常:内存限制达到3145728字节
和欢呼! 解决了上面定义的问题
性能
我做了一个非常不科学的快速检查……我几乎不敢分享它,因为结果违背了我的设想。
我创建了一个快速主程序,该主程序运行带有脚本任务的流程实例10000次:
public class PerformanceUnsecure {public static void main (String[] args) {ProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().buildProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();repositoryService.createDeployment().addClasspathResource("performance.bpmn20.xml").deploy();Random random = new Random();RuntimeService runtimeService = processEngine.getRuntimeService();int nrOfRuns = 10000;long total = 0;for (int i=0; i<nrOfRuns; i++) {Map<String, Object> variables = new HashMap<String, Object>();variables.put("a", random.nextInt());variables.put("b", random.nextInt());long start = System.currentTimeMillis();runtimeService.startProcessInstanceByKey("myProcess", variables);long end = System.currentTimeMillis();total += (end - start);}System.out.println("Finished process instances : " + processEngine.getHistoryService().createHistoricProcessInstanceQuery().count());System.out.println("Total time = " + total + " ms");System.out.println("Avg time/process instance = " + ((double)total/(double)nrOfRuns) + " ms");}}
流程定义只是一个开始->脚本任务->结束。 脚本任务只是将变量添加到变量中,然后将结果保存在第三个变量中。
<scriptTask id="myScriptTask" scriptFormat="javascript"><script>var c = a + b;execution.setVariable('c', c);</script>
</scriptTask>
我运行了五次,平均每个进程实例为2.57毫秒。 这是在最近的JDK 8(所以是Nashorn)上。
然后,我切换了上面的前两行以使用新的安全脚本,从而切换到Rhino并启用了安全功能:
SecureJavascriptConfigurator configurator = new SecureJavascriptConfigurator().addWhiteListedClass("org.activiti.engine.impl.persistence.entity.ExecutionEntity").setMaxStackDepth(10).setMaxScriptExecutionTime(3000L).setMaxMemoryUsed(3145728L).setNrOfInstructionsBeforeStateCheckCallback(1);ProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().addConfigurator(configurator).buildProcessEngine();
再次进行了五次运行……并获得了1.07毫秒/流程实例。 这是同一件事的两倍以上 。
当然,这不是一个真正的考验。 我以类白名单检查和回调为前提,假设Rhino的执行速度会变慢,但是没有这种事情。 也许这种特殊情况更适合Rhino……如果有人可以解释,请发表评论。 但这仍然是一个有趣的结果。
结论
如果您在流程定义中使用脚本,请仔细阅读引擎中的此新安全脚本功能。 由于这是一项新功能,非常欢迎反馈和改进!
翻译自: https://www.javacodegeeks.com/2016/06/secure-scripting-activiti-works.html
activiti脚本任务