分类:计算机 / 泛微Ecology
- OA系统加解密代码块
公司网络结构调整,我们需要批量迁移OA系统的代码块中的JS文件地址。代码块在数据库中是以密文形式存储的,22年我们曾尝试过手动编辑和前端自动化脚本的办法,但总归是效率低下,治标不治本。现在我们有了逆向工程手段,可以从根本上解决问题。 分析OA系统的反编译代码,得知其使用的加解密库为org.bouncycastle:bcprov-jdk15on:1.52,所有类型的代码块(流程布局、建模布局、建模查询)的加解密过程完全相同。提取加解密过程,封装为工具类: package com.company.project.util; import org.bouncycastle.crypto.engines.AESFastEngine; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import org.bouncycastle.util.encoders.Hex; /** 代码块工具类 */ public class CodeBlockUtils { private static final byte[] BYTES1 = "WEAVER E-DESIGN.".getBytes(); private static final byte[] BYTES2 = "weaver e-design.".getBytes(); /** * 将明文编码成密文 * @param source 明文 * @return 密文 */ public static String encode(String source) { try { if (source.isEmpty()) return ""; PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); cipher.init(true, new ParametersWithIV(new KeyParameter(BYTES1), BYTES2)); byte[] outputBytes = new byte[cipher.getOutputSize(source.getBytes("GBK").length)]; int length1 = cipher.processBytes(source.getBytes("GBK"), 0, source.getBytes("GBK").length, outputBytes, 0); int length2 = cipher.doFinal(outputBytes, length1); byte[] resultBytes = new byte[length1 + length2]; System.arraycopy(outputBytes, 0, resultBytes, 0, resultBytes.length); return new String(Hex.encode(resultBytes)); } catch (Exception e) { throw new RuntimeException(e); } } /** * 将密文解码为明文 * @param encoded 密文 * @return 明文 */ public static String decode(String encoded) { try { if (encoded.isEmpty()) return ""; PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); cipher.init(true, new ParametersWithIV(new KeyParameter(BYTES1), BYTES2)); byte[] sourceBytes = Hex.decode(encoded); cipher.init(false, new ParametersWithIV(new KeyParameter(BYTES1), BYTES2)); byte[] outputBytes = new byte[cipher.getOutputSize(sourceBytes.length)]; int length1 = cipher.processBytes(sourceBytes, 0, sourceBytes.length, outputBytes, 0); int length2 = cipher.doFinal(outputBytes, length1); byte[] resultBytes = new byte[length1 + length2]; System.arraycopy(outputBytes, 0, resultBytes, 0, resultBytes.length); return new String(resultBytes, "GBK"); } catch (Exception e) { throw new RuntimeException(e); } } } 使用OA系统中存储的所有代码块对工具类做全量测试,确认加解密结果一致。 那么,我们就可以在此基础上,进一步实现代码块内容的查找与批量替换。
- OA系统实现复制建模布局
做一个复杂的需求,要求对于同一份建模数据,不同的用户在新建、编辑、查看时使用不同的布局。实现时,先做一个完整的母版布局,然后复制出用户布局并修改。Ecology 9没有提供复制建模布局的功能,因此我们自己实现。 为了避免使OA系统出现未知的问题,我们限定,只允许在同一个模块内,复制一个已存在的布局到另一个已存在的布局。 从接口/formmode/exceldesign/excelLayoutSave.jsp?operation=saveExcel出发,逆向分析OA系统保存建模布局的机制,得出结论,只需复制以下表的数据即可实现复制建模布局: 表 数据内容 复制方式 modehtmllayout 布局自身的信息,例如布局设计、代码块等 update,注意保留布局名称 modeformfield 布局的字段信息,例如是否必填等 先delete再insert,注意复制到显示布局时字段全部变为只读 modefieldattr 布局的字段属性信息,例如计算日期差等 先delete再insert modeformgroup 布局的明细表信息,例如是否允许增行等 先delete再insert,注意复制到显示布局时关闭修改的相关配置 mode_layout_querysql 布局的明细表的固定查询条件 先delete再insert mode_layout_sortfield 布局的明细表的字段排序信息 先delete再insert
- OA系统前端逆向工程
泛微OA系统Ecology 9的前端程序是经过编译、压缩后的JavaScript文件,为了深入研究其机制细节,我们对其做逆向工程,将压缩的JS代码还原为非压缩的JS代码,并对其进行分析、理解。 建立一个项目oa-frontend-reverse-engineering,将线上的JS文件下载下来,按相同的目录结构放在项目中。然后在本地用nginx做反向代理:如果一个JS文件在项目中,则取项目中的,否则取线上的。nginx配置如下: server { listen 80; listen [::]:80; server_name oa-frontend-reverse-engineering; root "<项目目录>"; location / { # 如果本地存在文件,则取本地文件 # 否则,通过反向代理,取OA系统上的文件 try_files $uri $uri/ @reverse_proxy; } location @reverse_proxy { proxy_pass http://<OA系统的IP地址>$request_uri; } } 浏览器通过http://oa-frontend-reverse-engineering/可以正常访问OA系统,其中部分JS文件取自本地,于是,我们不仅可以阅读代码,还可以通过实际执行、断点调试来深入理解细节。 注:后来Chrome系浏览器在DevTools的“源代码”功能中增加了“替换”功能,可以直接用本地JS文件替换指定的线上JS文件,这比做nginx反向代理更简便。以下是阅读、还原压缩的JavaScript代码的方法和经验: 第一步:用代码格式化工具(例如VSCode)将压缩的JS代码格式化为缩进良好的版本。 写注释。 利用编辑器、IDE的重构功能,将变量改为有意义的名字。 利用编辑器、IDE的代码跟踪功能,跳转到相关的上下文。但JS代码由于动态性高,通常难以跳转,转而利用搜索功能,用函数名、特殊字符串等关键信息查找上下文。 在分析过程中,熟悉!0、&&、||、模块化等常见的编译结果模式,理解JS语法原理,还原为对应的正常写法。 断点调试,跟踪执行。 临时改写代码,将某些对象、数据、函数暴露到window上,在控制台中查看、修改、执行之,这对状态存储数据尤其有用。 注意:还原出来的JS文件只允许用于分析,不允许替换线上的JS文件。
- OA系统后端逆向工程
泛微OA系统Ecology 9的后端程序使用Java语言开发,为了深入研究其机制细节,我们对其做逆向工程,将class文件反编译为Java源代码,并对源代码进行分析、理解。 反编译 Ecology 9的class文件都存放在classbean目录中。从服务器上将classbean目录整体下载到本地,然后使用JetBrains IntelliJ IDEA附带的反编译器FernFlower,将classbean目录整体反编译到src目录: java -cp '<IDEA安装目录>\plugins\java-decompiler\lib\java-decompiler.jar' org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true .\classbean\ .\src\ 代码分析 反编译出来的源代码并不是原始代码,它的类名、属性名、方法名、字符串等都保留了意义,但所有的方法参数、局部变量都是形如var1、var2的无意义形式,也没有任何注释。 我们建立一个Git仓库oa-backend-reverse-engineering,将反编译出来的源代码放入其中管理,因为我们会在阅读的过程中修改它。我们同时将WEB-INF/lib目录(OA系统的三方库)放入其中,使类库整体完整,以便IDE识别。 以下是阅读、分析反编译出来的Java源代码的方法和经验: 写注释,包括文档注释、行注释等。 利用IDE的重构功能,将局部变量改为有意义的名字。 利用IDE的代码跟踪功能,跳转到相关的上下文。 在浏览器DevTools中提取API路径的关键词,在IDE中搜索,定位到起始执行点。 对最常见的类,如BaseBean、RecordSet,做完整分析,加深理解。 对最常见的架构设计,如命令(command)设计模式,做架构分析,加深理解。 注意:反编译出来的Java源代码只允许用于分析,不允许重新编译后替换线上的class文件。
- OA系统热更新Java类
我们的Java类在OA系统中运行时,如果发现bug,因为白天有大量用户在使用,无法重启JVM,就只能半夜更新代码并重启,故障处理时间很长。经研究,部分bug完全可以在白天通过热更新手段迅速修复。 工具:Arthas(阿里巴巴开源的JVM热更新工具)。 热更新要求:类不能增减字段、方法,只能修改方法的执行过程。 热更新过程: 本地修改Java代码,编译成class文件,上传到OA系统中,覆盖问题class文件(避免下次重启仍加载问题class文件)。 启动Arthas,选择名称为Resin的Java进程(OA系统的Web容器)。Resin进程可能有多个,在Arthas连接到进程后,通过jad com.company.<Tab键>来判断进程中是否有我们的Java类,从而确定是OA系统的进程。 在Arthas中执行命令retransform <class文件的路径>,尝试热更新。如果看到输出success,则成功,否则失败。注意不要使用redefine命令。
- 清除OA系统的表缓存
OA系统在读写数据库时,会对各表维护缓存,以提高性能。实际业务中,我们经常要在OA系统之外写库,此时OA系统中的表缓存可能是陈旧的,这会导致数据不一致。因此,我们需要清除OA系统中指定表的表缓存的功能。OA系统未提供这种功能,我们要自己想办法开发出来。 原理 通过逆向工程分析OA系统的表缓存机制。 RecordSet的executeQuery()和executeUpdate()最终都是执行executeSql()。在executeSql()中,以"select"开头的SQL会先尝试从缓存获取结果,非"select"开头的SQL不会。在未命中缓存的情况下,SQL执行成功后,会调用CacheFactory的refreshCache()更新缓存。 在CacheFactory的refreshCache()中,如果SQL以"select"开头,则调用putCache()更新指定表的缓存的一部分;如果SQL非"select"开头,则调用removeCacheForSql()清除指定表的所有缓存。 那么,想要清除指定表的缓存,就只需要执行一条非"select"开头的、包含指定表名的SQL语句,比如写库SQL,但更安全的办法是执行desc 表名。 实现 实现APIclearTableCache,接收参数tableNames。遍历表名,对每个表名通过RecordSet的executeQuery()执行SQLdesc 表名。