缘起
这是在工作中遇到的实际问题。笔者觉得其使用场景和解决的思路,还是具有一定的代表性的,随著文以记之,也作为一个工作的小结和积累。
本司有一个比较老的应用系统,是使用PHP编写的。其中有用到一个基于ECC的信息加密通信机制。基本过程就是这个应用系统作为另一个应用系统的客户端,需要在请求前,对实际请求的业务信息进行加密和编码,然后对响应的结果进行解密。
加解密用到了基于ECC的密钥协商和使用。简单而言,就是使用本方的ECC私钥,和对方的ECC公钥,来协商生成一个密钥,然后使用这个密钥和AES算法来进行实际的加解密。同时,基于安全的考虑和要求,还使用另外一对密钥,来进行信息的签名和验证。
这个算法具体实现,并不是本文要探讨的重点。这里的主要问题是,可能是由于考虑到密码学实现的方便或者限制,原来的开发者并没有直接使用PHP来编写,而是使用了一个Nodejs实现(可能因为可以共享使用服务端实现的代码)。那么作为PHP的主应用,如何来调用执行这个代码呢? 答案是为其配套的搭建了一个本地的Web服务,然后通过HTTP接口,来提供信息加解密的服务,而且为了管理这个应用,还使用了PM2作为应用管理系统。
显然,这并不是一个高效和优雅的解决方案。基于一个不是特别复杂的操作,需要引入一个外部程序也是可以理解的,但在上面又叠加了一个应用系统和其支撑系统,无论从架构的复杂性,部署和运维的简便,性能和资源的使用,应用安全等方面考虑,都是更加负面的影响。
改造思路
在理解了基本情况之后,笔者考虑可以基于现有的情况和条件,构思相关的系统改造。改造的目标就是简化系统架构,提高执行的性能。
首先需要明确,虽然理论上最好的方式,是基于PHP在应用内部来实现这个密码学算法和操作,但考虑到公司的主要技术栈并不是PHP系统,在这方面的研究和积累比较薄弱,笔者觉得在短期内使用PHP完美高效的复刻现有算法的难度还是不小的,因为要考虑很多的问题如ECC库,密钥协商,AES加密解密,ECC的签名和验证等。所以基本思路还是保留原有的JS代码和算法,但可以考虑服务和构造模式上加以改进。
笔者对于Nodejs系统和JS语言还是比较熟练的,经过简单的研究发现,其实这里这个Nodejs应用所完成的工作其实非常简单,就是提供了两个操作:编码和解码。将PHP要使用的数据,使用协商出来的密钥进行加密,生成签名后,就返回给PHP程序来进行后续的处理的。实质就是将这个Nodejs应用作为了一个"计算函数"来使用,而且专门为这个PHP应用来服务,不涉及到其他如数据库、网络、Web服务等方面的内容。非常简单的"输入"-"处理"-"输出"模式,而且可以在内部完成。
基于对Nodejs程序的了解,笔者知道,其实js代码是可以基于Nodejs环境来独立运行的。这样我们就可以在一个虚拟的Shell(命令行)环境中,使用node程序调用并执行这个js代码。输入就是命令行执行的执行参数;而输出,就是命令行输出stdout,在js中可以使用console.log来实现。
同时作为PHP程序,也提供了OS命令的执行函数,可以执行对应的操作系统Shell命令行。同时以文本的方式,获取命令执行的结果。
总结一下相关的改造思路如下:
- 将原有的nodejs Web应用程序,改造成为命令行程序,可以在命令行中执行
- 命名行命令的参数作为输入
- 命令行执行的结果作为输出
- 输出的目标是stdout,可以使用js程序中的console.log来输出
- 在php程序中,可以通过shell执行的方式,调用js程序
- php程序捕获命令行执行结果,作为处理结果,以进行后续操作
实现示例
笔者基于上面的构想和思路,在实际应用中实现了相关的操作。当然,虽然思路比较明确也不难理解,但在实际实现过程中,还是遇到和解决了一些工程方面的问题的。我们先简单的分享一下相关的实现示例,然后再总结和分析在这个过程中遇到的问题和解决的方式。
nodejs程序
首先就是需要对原有的nodejs应用进行改造。可以理解,基于方便管理和移植的考虑,笔者使用了单个js文件作为应用主体,同时改造成为完全无外部依赖的形式。因为这个程序比较简单,所有的密码学操作,都是crypto库提供的,不需要外部依赖。其他的如一些相关的配置信息,也同时写在了同一个代码文件当中,最终目的就是保证这个程序,是一个"单一文件"的程序文件,唯一的外部依赖就是nodejs环境,方便管理和部署。
下面就是这个程序的部分结构和代码:
js
'use strict'
// 业务和密钥配置信息
const PAY_CFG = {
// url and
pay_page : "pay.com",
pay_code : "",
// sign key
sign_private : xxx,
sign_public : yyy,
...
}
// 算法配置
const
crypto=require('crypto'),
CURVE='secp256k1',
ALGORITHM='SHA256',
...
// ECC类和对象
class ECC{
constructor ( config ){
//
...
// 程序入口
let CUR_ECC = null;
const start = async ()=>{
const args = process.argv.slice(2);
let r, param = null;
if (args[1] && args[1].length > 0) {
try {
param = JSON.parse(Buffer.from(args[1], "base64"));
} catch (error) {
return console.log({R: 500, C: "ERROR_PARAMS" });
}
}
// init ecc
CUR_ECC = new ECC({
privateKey : PAY_CFG.client_private,
publicKey : PAY_CFG.server_public,
signKey : PAY_CFG.sign_private,
verifyKey : PAY_CFG.verify_public, // should by verify_public
});
switch(args[0]) {
case "encode": // generate order
r= await ulencode(param);
break;
case "decode": // decode result from pay platform
r= await uldecode(param);
break;
default: r = { R: 500, C: "ERROR_PARAMS" };
}
console.log(JSON.stringify(r));
};
// 业务和命令实现
const ulencode = (param) {
....
};
// 程序启动
start()
笔者需要提醒一下,基于一些安全和知识产权的限制,上面的代码经过裁剪和修改,并不能直接执行,笔者列举在此,主要是为了讨论程序的架构和思路。这里的要点在于:
- 由于需要限制为单一文件,文件中包含和很多内容,也一般的模块化程序组织有所不同
- 集成编写了相关的业务配置信息,可以基于业务环境进行调整
- 加密方法也是可以配置的
- 核心类是一个ECC对象,封装了相关的加密解密和签名验证算法
- 通过执行时的命令行参数和处理来进行功能的扩展
- 程序作为一个函数(非服务)方式来执行,所以是实时同步的
- 程序的入口负责参数的解析,加密库的初始,根据参数调用相关的程序,和标准化输出
- 输出就是简单单一的console.log,而且必须保证有输出和严格的JSON格式文本
此外还有一些相关的技术细节,在后面会结合后续操作进行详细探讨。
命令行执行
js主程序文件写好之后,就可以使用标准的nodejs程序的执行方式来执行了。和普通的nodejs web应用不同的是,它不是服务程序或者守护程序,而是作为"函数程序"立即执行和处理的,这在和PHP程序集成中非常重要。
下面就是这个程序在命令行环境中执行的方式:
shell
node ../js/ulpay.js decode eyJkYXRhIjoiQWhDdXVlUk...=
// 输出
{"R":200,"C":{"mnum":"xxx","status":1}}
这个标准命令程序包括以下几个要素:
- node nodejs主程序,必须保证在系统中可以找到并执行
- .js 函数主程序文件
- decode 这里程序的第一个参数,为一个指令,其实就是程序的一个功能,此次为解码
- 参数,这里使用base64编码的参数,原始形式是json
- 这种形式保证程序只有两个命令行参数, 指令和参数
- 输入是JSON形式的文本,在调用它的PHP程序中进行后续处理
PHP调用
在保证了js程序能够在命令行中正确执行之后,就可以考虑将其集成到PHP主应用程序当中了,下面是相关示例代码:
php
// node 命令
const NODE_CMD = "node ../js/ulpay.js ";
// 原始参数
$param = array(
"order" => $this->getOrder($price,$stuname),
"stuid" => $stuid,
"price" => $price
);
// JSON和base64编码
$b64 = json_encode($param,JSON_UNESCAPED_UNICODE);
$b64 = base64_encode($b64);
// nodej 完整命令
$ncommand = NODE_CMD." encode ".$b64;
// 运行结果和JSON解码
$output = shell_exec($ncommand);
$json = json_decode($output);
...
这部分代码的要点如下:
- 要处理的参数,要先编码成为base64,来匹配js程序处理
- 原始命令和文件,需要匹配js文件的位置
- 使用shell_exec函数,配合完整命令来执行
- shell_exec的执行结果,就是console.log的输出
- 使用json_decode来解码输出文本
相关问题和优势
看到这里,可能有一些细心的读者,会发现一些问题,并提出相关的疑问,这其实就是笔者在实现过程中遇到的问题和处理的方式。
- 为什么要编码成为BASE64
因为笔者在实际环境中,遇到了一些需要转义的参数,比如混合的双引号和单引号内容,非常不好处理,所以索性就使用base64进行统一的编码。就避免了很多处理错误的情况
- 为什么js文件的位置是 ../js/...
这是笔者使用的CI框架的一个特点。笔者是将这个JS文件,部署在PHP应用项目的js文件夹当中的。这个应用项目是CI框架项目,其默认程序入口在 www/index.php当中,所有的相对路径,都是从这个文件出发的。
当然也可以写成绝对路径,不会影响执行。但那样就不方便部署和移植了。
- 为什么要使用 JSON_UNESCAPED_UNICODE
这是笔者使用的PHP版本和CI框架的一个限制,默认情况下,它会编码程序 \u... 形式的字符,虽然实际上不影响处理,但在开发和调试过程中,检查相关信息和文本非常不方便。
- 另外,需要特别注意nodejs的执行环境,可以在PHP相同的环境中执行,并且没有外部依赖
显然,和原来的解决方案相比,新的技术方案具备的好处如下:
- JS程序文件,可以成为PHP项目中的一个组成部分,方便开发、管理和部署
- 简化了原有PHP程序的配置,无需考虑外部连接和配置的问题
- 简化了额外的Nodejs Web应用的管理
- JS程序以函数而非服务的形式运行,减少了运算资源的占用
小结
本文记录了笔者在工作过程中遇到的一个使用单一js文件,集成到PHP程序中进行处理的实现和案例。探讨了原始应用架构和问题,新的解决思路和方案,以及将JS程序集成到PHP项目中的基本过程和需要注意的问题,希望对类似情况和需求的开发者有所启示和帮助。
写到这里,笔者突然想起,这种模式还暗合Unix之道: Text In, Text Out; Everything is CLI。