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。

相关推荐
damaoyou几秒前
Cog3DRangeImagePlaneEstimatorTool完全指南
后端
Nturmoils23 分钟前
分页别写太顺手,LIMIT 背后还有排序和边界
数据库·后端
GuWenyue27 分钟前
前端异步请求踩坑?3种方式搞定Ajax数据交互,从XHR到async/await
前端·javascript·设计模式
神奇小汤圆27 分钟前
国产版“Codex”初体验,智谱ZCode很强啊!
后端
站大爷IP28 分钟前
Python里的“赋值”到底是什么意思?
后端
鹅城剑仙1 小时前
Spring Boot 微服务架构设计与最佳实践
spring boot·后端·微服务
大家的林语冰2 小时前
超越 TypeScript,Flow 强势回归,语法高仿 TS,功能更丰富,类型更安全!
前端·javascript·typescript
星空2 小时前
html\css\js入门
javascript·css·html
重生之我是Java开发战士2 小时前
【Java SE】多线程(三):单例模式,阻塞队列,线程池与定时器
java·javascript·单例模式