PHP应用集成JS代码进行的函数计算

缘起

这是在工作中遇到的实际问题。笔者觉得其使用场景和解决的思路,还是具有一定的代表性的,随著文以记之,也作为一个工作的小结和积累。

本司有一个比较老的应用系统,是使用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。

相关推荐
深海的鲸同学 luvi1 小时前
高德地图离线加载解决方案(内网部署)+本地地图瓦片加载
前端·javascript·html5
明明跟你说过1 小时前
【Go语言】从Google实验室走向全球的编程新星
开发语言·后端·go·go1.19
keenx4 小时前
PHP无法读取.env的配置变量原因
php·laravel
GIS好难学4 小时前
《Vue进阶教程》第六课:computed()函数详解(上)
前端·javascript·vue.js
执键行天涯4 小时前
element-plus中的resetFields()方法
前端·javascript·vue.js
Days20505 小时前
uniapp小程序增加加载功能
开发语言·前端·javascript
喵喵酱仔__5 小时前
vue 给div增加title属性
前端·javascript·vue.js
界面开发小八哥5 小时前
LightningChart JS助力德国医疗设备商打造高精度肺功能诊断软件
javascript·信息可视化·数据可视化·lightningchart·图表工具
customer085 小时前
【开源免费】基于SpringBoot+Vue.JS加油站管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·maven
G探险者5 小时前
如何解决垂直越权与水平越权问题
后端