Skip to content

响应加密

NullVerify 支持对客户端 API 的响应内容进行加密,防止中间人窃取业务数据。

支持的算法

算法密钥来源密文格式安全性
rc4 (默认)独立加密密钥base64(cipher)低,仅防简单抓包
aes-cbc独立加密密钥base64(iv).base64(cipher)中,推荐对称方案
des-cbc独立加密密钥base64(iv).base64(cipher)低,仅兼容旧系统
rsa开发者上传 RSA 公钥base64(rsaKey).base64(iv).base64(cipher)高,推荐非对称方案

加密算法必选

创建应用时默认使用 RC4 加密,所有应用必须选择一种加密算法,不支持关闭加密。

配置方式

在管理后台 → 应用配置 → 安全配置 中选择响应加密算法。

  • 对称加密(RC4 / AES-CBC / DES-CBC):系统自动生成独立加密密钥,可在安全配置页查看和重新生成。
  • RSA 混合加密:需上传 RSA 公钥(PEM 格式,≥ 2048 位)。

注意

修改加密配置后,所有客户端必须同步更新解密逻辑,否则将无法解析响应。建议先在测试环境验证后再上线。

加密流程

启用响应加密后,HTTP 响应体为纯密文字符串(base64 编码),Content-Typetext/plain; charset=utf-8

// 未加密(原始业务响应,Content-Type: application/json)
{
  "code": 0,
  "message": "ok",
  "result": { "token": "abc123", "expires": "2026-12-31 23:59:59" }
}

// 加密后(Content-Type: text/plain; charset=utf-8)
dGhpcyBpcyBpdi4=.Y2lwaGVydGV4dC4=

重要变更

响应体为纯密文字符串,不再包裹在 JSON 结构中。客户端收到响应后:

  1. 不能直接调用 response.json() 或 JSON 解析
  2. 必须先读取原始文本 → 解密 → 再解析为 JSON

错误处理策略

场景响应行为
业务成功/业务失败(appCtx 已解析)纯密文,Content-Type: text/plain
签名错误/时间戳/nonce 错误(appCtx 已解析)纯密文,Content-Type: text/plain
加密过程本身失败HTTP 500 + 空 body(fail-closed,不泄露明文)
无效 app_key / 缺少 app_key明文 JSON(此时无法获得加密密钥)
限流 429明文 JSON(在签名中间件之前触发)
Panic 500明文 JSON(Recovery 中间件无 appCtx)

判断响应是否加密

检查 Content-Type 响应头:

  • text/plain → 纯密文,需解密
  • application/json → 明文 JSON,直接解析

客户端解密

解密步骤

  1. 判断是否加密:检查响应 Content-Type 是否为 text/plain
  2. 读取原始文本:获取 HTTP 响应体的原始文本内容(即密文字符串)。
  3. 执行解密:根据算法规则解密密文字符串,得到明文 JSON。
  4. 解析业务响应:将明文解析为 JSON 对象,从中读取实际的 codemessageresult

通用解密流程(伪代码)

python
response = http_post(url, data=params)

if response.headers["Content-Type"].startswith("text/plain"):
    # 加密响应:先解密再解析
    ciphertext = response.text
    plaintext = decrypt(ciphertext, encrypt_key)
    result = json.loads(plaintext)
else:
    # 明文响应:直接解析
    result = response.json()

对称加密(RC4 / AES-CBC / DES-CBC)

密钥获取:在管理后台 → 应用配置 → 安全配置 中查看独立加密密钥(64 字符 hex 字符串)。

客户端需将 hex 密钥解码为字节数组,并按算法取指定长度前缀作为实际密钥:

key = hex_decode(encrypt_key)[:keyLen]
算法keyLen(字节)
rc416
aes-cbc32
des-cbc8

RC4 解密

python
import base64

def get_key(encrypt_key_hex: str, length: int) -> bytes:
    return bytes.fromhex(encrypt_key_hex)[:length]

def rc4_decrypt(key: bytes, ciphertext_b64: str) -> str:
    data = base64.b64decode(ciphertext_b64)
    # RC4 加解密逻辑相同
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    out = bytearray(len(data))
    i = j = 0
    for k in range(len(data)):
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out[k] = data[k] ^ S[(S[i] + S[j]) % 256]
    return out.decode('utf-8')

# 使用:encrypt_key 从管理后台安全配置页获取
# ciphertext 从响应的原始文本获取
key = get_key("your_encrypt_key_hex", 16)
plaintext = rc4_decrypt(key, response.text)
result = json.loads(plaintext)

AES-CBC 解密

密文格式:base64(iv).base64(ciphertext),使用 . 分隔。

python
import base64
from Crypto.Cipher import AES

def aes_cbc_decrypt(key: bytes, encrypted: str) -> str:
    parts = encrypted.split(".")
    iv = base64.b64decode(parts[0])
    ciphertext = base64.b64decode(parts[1])
    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded = cipher.decrypt(ciphertext)
    # PKCS#7 去填充
    pad_len = padded[-1]
    return padded[:-pad_len].decode('utf-8')

key = get_key("your_encrypt_key_hex", 32)
# ciphertext 从响应的原始文本获取
plaintext = aes_cbc_decrypt(key, response.text)
result = json.loads(plaintext)

DES-CBC 解密

密文格式与 AES-CBC 相同:base64(iv).base64(ciphertext)

python
from Crypto.Cipher import DES

key = get_key("your_encrypt_key_hex", 8)
# 解密逻辑同 AES-CBC,替换为 DES.new(key, DES.MODE_CBC, iv)

RSA 混合加密解密

密文格式:base64(rsaEncryptedKey).base64(iv).base64(ciphertext),使用 . 分隔为 3 段。

解密流程:

  1. 用私钥通过 RSA-OAEP (SHA-256) 解密第一段,得到 AES-256 密钥
  2. 用该 AES 密钥 + IV(第二段)通过 AES-CBC 解密第三段
  3. 去除 PKCS#7 填充
python
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.Hash import SHA256
import base64

def rsa_hybrid_decrypt(private_key_pem: str, encrypted: str) -> str:
    parts = encrypted.split(".")
    enc_key = base64.b64decode(parts[0])
    iv = base64.b64decode(parts[1])
    ciphertext = base64.b64decode(parts[2])

    # RSA-OAEP 解密 AES key
    rsa_key = RSA.import_key(private_key_pem)
    cipher_rsa = PKCS1_OAEP.new(rsa_key, hashAlgo=SHA256)
    aes_key = cipher_rsa.decrypt(enc_key)

    # AES-CBC 解密数据
    cipher_aes = AES.new(aes_key, AES.MODE_CBC, iv)
    padded = cipher_aes.decrypt(ciphertext)
    pad_len = padded[-1]
    return padded[:-pad_len].decode('utf-8')

RSA 公钥要求

  • 格式:PEM(-----BEGIN PUBLIC KEY-----
  • 最小位数:2048 位
  • 上传后系统自动计算 SHA-256 指纹用于标识

算法选择建议

场景推荐算法原因
快速接入、低安全需求rc4实现简单,性能最好
通用场景aes-cbc安全性与性能平衡
兼容旧系统des-cbc仅在旧系统无法升级时使用
高安全需求rsa密钥不暴露在客户端,最安全
对应实现
  • 加密逻辑: pkg/crypto/response_encrypt.go
  • 响应中间件: internal/platform/httpx/response.go

面向脚本与插件开发者的网络验证系统