外观
响应加密
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-Type 为 text/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 结构中。客户端收到响应后:
- 不能直接调用
response.json()或 JSON 解析 - 必须先读取原始文本 → 解密 → 再解析为 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,直接解析
客户端解密
解密步骤
- 判断是否加密:检查响应
Content-Type是否为text/plain。 - 读取原始文本:获取 HTTP 响应体的原始文本内容(即密文字符串)。
- 执行解密:根据算法规则解密密文字符串,得到明文 JSON。
- 解析业务响应:将明文解析为 JSON 对象,从中读取实际的
code、message和result。
通用解密流程(伪代码)
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(字节) |
|---|---|
rc4 | 16 |
aes-cbc | 32 |
des-cbc | 8 |
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 段。
解密流程:
- 用私钥通过 RSA-OAEP (SHA-256) 解密第一段,得到 AES-256 密钥
- 用该 AES 密钥 + IV(第二段)通过 AES-CBC 解密第三段
- 去除 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