通用技术 记记一次糟心的 Python 改写 Java 加密

异彩飞天 · June 20, 2025 · Last by YLM replied at June 23, 2025 · 1486 hits

项目背景

首先得感谢 AI 的迅猛发展,让我能够找到些许新鲜感,提供一些成长的养分,最关键的是让我们持续有事可干。本次项目是对一个传统的 BPM 系统接入 AI,对一些关注的流程进行预审。接口虽然不多,但包含加解密功能。一直以来,我对加解密仅停留在知道有那么几种方法的层面,了解并不深入。本项目采用了 AES-CBC+PKCS7Padding 方式加密。接口文档中提供了加解密的示例,一看并不复杂,自己还是有信心解决的,至少还有 AI 帮忙;实在不行的话,还可以求助开发团队。

加密主要方法如下:

public static String encryptHex(String payLoad,String secretKey,String salt) {
        if (StringUtils.isEmpty(payLoad)) {
            return null;
        }
        AES aes = new AES("CBC", "PKCS7Padding",
                // 密钥,可以自定义
                secretKey.getBytes(),
                // iv加盐,按照实际需求添加
                salt.getBytes());
        // 加密为16进制表示
        return  aes.encryptHex(payLoad);
    }

从开始的偏好到最后的扎心

据以往的经验,接口测试通常会有一个工具选择的优先级顺序。我个人更倾向于使用 Python 而非 Java。适合我的工具顺序是:Python > JMeter > Java。

Python

直接编写加密脚本

首先将加解密示例的代码片段截图提问给 AI,请求提供一个等效的 Python 实现。尝试了多种方法后发现,短字符串大多能匹配成功,但一旦涉及到复杂的 JSON 字符串时,验证签名就会失败。于是决定死磕这个问题。

import os
import binascii
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from dotenv import load_dotenv

load_dotenv()

def encrypt_hex(content):
    """
    AES-CBC加密并返回16进制字符串
    :param content: 待加密内容
    :return: 16进制格式的加密结果
    """
    secret_key = os.getenv("secret_key")

    salt = os.getenv("salt")

    if not content:
        return None

    # 确保密钥和IV为16字节(不足补零,超长截断)
    key = secret_key.encode('utf-8')[:16].ljust(16, b'\0')
    iv = salt.encode('utf-8')[:16].ljust(16, b'\0')

    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded_data = pad(content.encode('utf-8'), AES.block_size)
    encrypted = cipher.encrypt(padded_data)
    return binascii.hexlify(encrypted).decode('utf-8')


if __name__ == '__main__':
    content = '123456'
    assert "e966ee85bd3a161164d822e1906db79d" == encrypt_hex(content)

    req = '{"businessType":1,"bmpInstanceId":"ywp75d8c35444224ab8310793d60459b","data":"{\"IncomingUnit\":\"广东公司\",\"IsCooperativeProject\":\"\",\"Remarks\":\"备注信息\",\"Title\":\"成本管理标题\",\"IsLimitExceed\":\"\",\"IncomingDate\":\"2025-05-28\",\"MatterCategory\":\"竣备后新增签证变更\",\"Summary\":\"概要内容\",\"ProjectCompanyAmount\":\"10000万\",\"IsPlanOut\":\"\"}","files":[{"fileData":[{"name":"1.docx","fileId":"6bea5633752a43458a93b649fd55c956","fileType":".docx","fileUrl":""},{"name":"12.txt","fileId":"5616350fb6c6405382abe34ea6a6d741","fileType":".txt","fileUrl":""}],"formType":"Attachments"}]}'
    actual = encrypt_hex(req)
    expect = 'e70fa78982c5bf6d2a894c9285b5ac5279e08d2630eceb3fd66c2b99dea90a23d0203602142b55d46e81a20d15637c09fa4af902cc1b59b91e5c3aee9797f1ef76fb36feda3189443dd1cdd1a6a5c6cafcf93d7a039ab9d68adc8d35483e281a41ff496fb66192ccb3d9b43f8df770b9d7921912a36f6be909db603f47b37cf8ce6153649645e153d7acc34f8ddf013da7b63052462798b303604e384bed464200364be68bbc84f16f03d4146b5391bc23c451dc2227f1365dc0479afd6533825ef598da15e2bd67179864a36f88a3bee9d2b66e519a6cba1724d16d785fc9b60336cdbeda72f694db7093e4d2e5c44da211f786fbdedbba85f1e18a92fad5d2bd3ee73986bd53c0d9194040234ed517e435e1f2b558511b16ef94031659f4c08dc46c0e71d838fcc3914f75c0a63c676f811f99efef8ddda6083021325ccd65e98ede5462ecb5569ebb718dc558c3bcc7ad9ba62bb1bc21f5a80ac4f4b7dd329e929df5c9e6e0069c4ca8651abff6e4d9c6c76a98c7defdf90bda172119f5cf24448a2f8cb4cec1dac34c1a3d5b0f5c320f7de2a19ac4aff1262c1961829fdcdda8d2638d7a0b3ee2036d38b7b51a67f39a7ae65a42e729c7b7586de25f0578a13247da92c40297b47ee7bcf7980d758e7b6fc8bd22399d1be4b9f1c7cd915c256b8ce67aeadd460c9021379b66259577f2fa4f7d921d44348facc0583c22741f5d71ff5faaa77a55d07cf880f161f7a30a96bcca3ea3ed51bb2cec2251402b126835d1fe7a15bfbf89142460f6e349f9a6700da5ffbf8ac4c17dfd8d84e6d15ef76b40ceb3382f8f928bef659f007ee1d814eb6118c4e8f2a03ca920ab0fad51d0f3b33e3a8e1cfda7d98b0f88be91cfe327325a5b738a7f082d54a0067b662b0877ad3b9f59001e1f3114949158a1'
    assert expect == actual

偏方,把 java 代码打包成 jar 供 python 调用

注意打包的时候要把依赖也要打进去

python 调用 java 包有三种方法:

  • jpype, ✅,推荐这种,通义千问给了可行代码
  • jnius , ❌,不知道怎么搞,也挺折腾人
  • subprocess,❌,太原始粗暴不推荐,传参都不知道怎么传
import os
import jpype
from dotenv import load_dotenv

load_dotenv()


secret_key = os.getenv("secret_key")
salt = os.getenv("salt")

_jvm_initialized = False

def init_jvm():
    global _jvm_initialized
    if _jvm_initialized:
        return

    lib_dir = r"D:\project\py\bpm_ai"
    jars = [os.path.join(lib_dir, f) for f in os.listdir(lib_dir) if f.endswith(".jar")]
    classpath = "." + os.pathsep + os.pathsep.join(jars)

    try:
        jpype.startJVM(
            jpype.getDefaultJVMPath(),
            "-Djdk.security.allowNonCaAnchor=true",
            # "-Djava.security.debug=all",
            f"-Djava.class.path={classpath}"
        )

        security = jpype.JClass("java.security.Security")
        bc_class = jpype.JClass("org.bouncycastle.jce.provider.BouncyCastleProvider")
        security.addProvider(bc_class())

        _jvm_initialized = True
    except Exception as e:
        print("❌ 启动 JVM 失败:", str(e))
        raise

def encrypt_hex(content):
    result = ''
    init_jvm()

    try:

        util = jpype.JClass("qa.polyic.BPMUtils")
        result = util.toAnalysisReqSign(content)
        return result
    except Exception as e:
        print("❌ 调用 Java 方法失败:", str(e))
        raise

if __name__ == '__main__':
    import json
    content = {"businessType":1,"bmpInstanceId":"ywp75d8c35444224ab8310793d60459b","data":"{\"ProjectCompanyAmount\":\"10000万\",\"IsPlanOut\":\"\",\"IncomingUnit\":\"广东公司\",\"MatterCategory\":\"竣备后新增签证变更\",\"IsLimitExceed\":\"\",\"Remarks\":\"备注信息\",\"Summary\":\"概要内容\",\"Title\":\"成本管理标题\",\"IsCooperativeProject\":\"\",\"IncomingDate\":\"2025-05-28\"}","files":[{"fileData":[{"name":"1.docx","fileId":"6bea5633752a43458a93b649fd55c956","fileType":".docx","fileUrl":""},{"name":"12.txt","fileId":"5616350fb6c6405382abe34ea6a6d741","fileType":".txt","fileUrl":""}],"formType":"Attachments"}]}
    content = json.dumps(content, ensure_ascii=False, separators=(',', ':')).replace(': ', ':').replace(', ', ',')
    encrypted_result = encrypt_hex(content)
    print(f"加密后的十六进制数据: {encrypted_result}")
    print("e70fa78982c5bf6d2a894c9285b5ac5279e08d2630eceb3fd66c2b99dea90a23d0203602142b55d46e81a20d15637c09fa4af902cc1b59b91e5c3aee9797f1ef76fb36feda3189443dd1cdd1a6a5c6cafcf93d7a039ab9d68adc8d35483e281a41ff496fb66192ccb3d9b43f8df770b9d7921912a36f6be909db603f47b37cf8ce6153649645e153d7acc34f8ddf013da7b63052462798b303604e384bed464200364be68bbc84f16f03d4146b5391bc23c451dc2227f1365dc0479afd6533825ef598da15e2bd67179864a36f88a3bee9d2b66e519a6cba1724d16d785fc9b60336cdbeda72f694db7093e4d2e5c44da211f786fbdedbba85f1e18a92fad5d2bd3ee73986bd53c0d9194040234ed517e435e1f2b558511b16ef94031659f4c08dc46c0e71d838fcc3914f75c0a63c676f811f99efef8ddda6083021325ccd65e98ede5462ecb5569ebb718dc558c3bcc7ad9ba62bb1bc21f5a80ac4f4b7dd329e929df5c9e6e0069c4ca8651abff6e4d9c6c76a98c7defdf90bda172119f5cf24448a2f8cb4cec1dac34c1a3d5b0f5c320f7de2a19ac4aff1262c1961829fdcdda8d2638d7a0b3ee2036d38b7b51a67f39a7ae65a42e729c7b7586de25f0578a13247da92c40297b47ee7bcf7980d758e7b6fc8bd22399d1be4b9f1c7cd915c256b8ce67aeadd460c9021379b66259577f2fa4f7d921d44348facc0583c22741f5d71ff5faaa77a55d07cf880f161f7a30a96bcca3ea3ed51bb2cec2251402b126835d1fe7a15bfbf89142460f6e349f9a6700da5ffbf8ac4c17dfd8d84e6d15ef76b40ceb3382f8f928bef659f007ee1d814eb6118c4e8f2a03ca920ab0fad51d0f3b33e3a8e1cfda7d98b0f88be91cfe327325a5b738a7f082d54a0067b662b0877ad3b9f59001e1f3114949158a1" == encrypted_result)

最后不得不干脆点出狠招,直接调用 java post 请求的方法,可行。就是好奇为何加密方法、调用 jar 包 返回的签名不一样。如果有同行者碰到相同问题,可以在下面告知我,让我学习一下,十分感谢

Jmeter

也是使用相同办法,把加解密示例的代码片段截图提问 AI,让 AI 提供一个 BeanShell 的等价方法。这里尝试了 N 种办法,BeanShell 的调试也是各种闹心,还有很多 Java 的语法不支持。但最后也是通了,发现 Json 字符串需要压缩,然后再通过 Map 构造,fastjson 转对象后再转一下字符串,就能用了。缺点就是每每要新增采样,或者禁用旧的采样,不然一条测试报文就没能保存到。接口测试的价值除了快,就是能够重复跑,所以时不时还是转到 Java 项目去写单元测试。

Java

最后还是跟开发兄弟拿来了 Java 的几个关键 java 文件,很快速的就把测试项目支棱起来了,发现比以往称手了不少。虽然至少有一年没有敲过 java 代码了。果然日常的点滴积累,一法通万法通。一顿操作猛如虎,很顺手的把一些工具类、单元测试搞起来。java 环境很少搞,本渣刚好是个更新狂,爱折腾,时不时都更新 idea 工具。
被 maven 仓库的配置这个老坑坑过好几次,工具自带 还有自己安装的 maven 仓库存储路径不一样。
单元测试是越写越多,发现不方便管理,每个单元测试方法还要维护注释,这个方法的目的,报告之类的也不好展示,于是想起了自己很喜欢的 com.thoughtworks.gauge,这个用 markdown 表格方式维护的测试数据很是直观,报告还美观 。于是又 搭建个新项目,用 Gauge 去组织测试用例,又开始折腾了,通过 gauge init java_maven 命令创建好项目。但编译那啥的又报错了,这搞不了啊。于是又想到了 Python。

共收到 3 条回复 时间 点赞

就是好奇为何加密方法、调用 jar 包 返回的签名不一样。如果有同行者碰到相同问题,可以在下面告知我,让我学习一下,十分感谢

编码、json 里字典的顺序这些都会影响,若确定使用都没问题,那就要看两边底层库的加解密实现了。

异彩飞天 回复

AES 加密时会约定在前面加上 16 个随机的字节,服务解密的时候会先去掉 16 个随机字节,然后解密。因为每次都有会随机字节,所以加密的结果不同,但解密的结果是相同的。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up