做接口测试这些年,我见过太多人在加密接口前卡住。接口文档里多了一个sign字段,返回的不是正常 JSON,而是一长串看不出含义的文本。你按照文档把参数填齐,点击发送,结果永远是一句“签名错误”。我刚遇到这种接口的时候,第一反应也是找开发把加密去掉,得到的答复基本是“签名是安全需求,去不了”。后来我换了个思路,不躲了,正面应对接口测试里的加密解密攻防,反而把测试做深了一层。
这篇不打算堆密码学理论,重点讲清楚三件事:接口到底在防什么、数据是怎么变换的、测试要怎么安全地拿到并还原真实数据。里面所有处理思路、可复制的代码、踩过的坑,都是从实际项目里跑通的经验。内容适合做接口测试的、写接口的,以及对接口安全感兴趣的读者。你不需要懂高深的密码学,只要会看抓包报文、会一点 Python,就能跟着把加密接口测明白。
1. 接口加密为什么会成为测试路上的“拦路虎”
1.1 你看到的“加密接口”到底在防什么
先还原一个最常见的场景。打开抓包工具,看到一个登录接口的请求体长这样:
{ "account": "VGhpcyBpcyBhIG1vY2sgcGFzc3dvcmQ=", "password": "U2VuZmluZSB0ZXN0", "timestamp": "1737269971", "nonce": "8f3a1b7c2d4e9a6f", "sign": "8f6f2b8f8a2c1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e" }如果完全没有防护,任何人打开抓包工具,都能直接看到账号密码明文,还能顺手把金额、用户角色、订单状态这些关键字段改掉再发出去。服务端如果不校验,数据就被人为篡改了;就算校验,攻击者还可以把截获的合法请求原封不动重发一遍,这就叫重放攻击。
所以接口加密并不是开发用来为难测试的,它防的是三类问题:数据泄露、报文篡改、重放攻击。用生活里的例子来类比,签名相当于在信封封口盖上私章,证明这封信没被拆过;加密相当于把信件内容放进保险箱,即使中途被人截走也读不懂。理解了这一层,你在做测试设计的时候就不会只盯着“怎么调通”,而会主动去想“去掉这层防护会不会出事”。
1.2 接口加密的四个层次要分清
很多人一听到“接口加密”就以为只有一个概念,其实站在测试视角,至少要区分四个层次,处理方式完全不同。
| 层次 | 实现方式 | 防护目标 | 测试关注点 |
|---|---|---|---|
| 传输层 | HTTPS/TLS | 防止链路被窃听、报文被截获后直接阅读 | 证书有效性、加密套件、是否强制HTTPS |
| 参数签名 | sign、token、hmac | 防止参数被篡改、保证数据完整性 | 签名算法、规则、字段覆盖范围 |
| 敏感字段加密 | 密码、手机号、身份证等单独加密 | 防止单个敏感字段泄露 | 加密算法、密钥管理、密文格式 |
| 整体报文加密 | 整个请求体/响应体加密 | 防止业务数据整体泄露 | 加密时机、解密链路、明文结构 |
传输层的 HTTPS 是基础,但它解决不了“客户端被控制”之后的场景。攻击者只要拿到了前端代码,完全可以绕过浏览器直接构造请求,所以业务层加密才是接口测试真正要花功夫的地方。
这里有个很容易踩的误区:以为只要用了 HTTPS,业务参数就可以明文传输。实际上很多安全事故都是 HTTPS 之后业务层裸奔导致的,比如登录密码仍然明文出现在抓包工具里。所以我建议你在测试任何接口时,先按上面四个层次把防护情况列出来,缺哪一层就补哪一层的测试用例。
1.3 加密接口难测的根源不是算法,是信息差
算法本身不难,AES、RSA、SHA256 这些都是公开的标准,网上资料一大堆。真正让你卡住的是信息差:不知道密钥,没办法准备合法报文;不知道签名规则,构造不出正确的sign;不知道响应里的密文结构,断言都写不下去。
我见过不少测试同学一遇到加密接口就想着“绕过”,让开发临时开一个不走加密的口子。这样做测试确实省事了,但线上出问题的时候你完全没有排查能力,因为你根本没测过真实链路。正确做法是把信息差补上:
- 从接口文档里找加密说明和密钥获取方式;
- 从前端 JS 代码里看请求是怎么被加密的;
- 从开发维护的环境配置里拿测试密钥;
- 在测试环境约定一套专用的加解密方案。
总结一句话:加密接口能不能测,不取决于你密码学有多强,而取决于你能不能把“密钥、规则、明文结构”这三个信息拿到手。拿到手之后,剩下的就是把算法用代码还原一遍。
2. 加解密算法快速识别与动手实操
2.1 从报文字符串一眼判断算法类型
拿到一串看不明白的文本,第一步不是找代码,而是先用特征判断它属于哪一类。我整理了一个速查表,基本能覆盖日常遇到的情况。
| 文本特征 | 可能的算法 | 处理建议 |
|---|---|---|
以A-Za-z0-9+/组成,结尾有= | Base64 | 先尝试 Base64 解码,看解码后是否可读 |
| 32 位十六进制字符串 | MD5 | 多用于签名摘要,不可逆 |
| 40 位十六进制字符串 | SHA1 | 多用于签名,不可逆 |
| 64 位十六进制字符串 | SHA256 | 多用于签名,不可逆 |
| 长度是 16 的倍数、Base64 或 hex | AES 密文 | 需要密钥和 IV 才能还原 |
| 两段式、长度很长、包含公钥特征 | RSA 密文 | 需要私钥才能解密;公钥通常公开 |
以04开头或带特定前缀的 hex | 国密 SM2 | 需要 SM2 密钥对 |
这个表的作用不是精确定位,而是缩小范围。比如你看到一个 64 位的 hex 字符串,大概率是 SHA256 或者 AES 带 hex 编码后的结果,你就不会傻乎乎拿去做 Base64 解码了。
实际排查的时候,我习惯先用在线工具或者本地脚本做一轮“指纹试探”:Base64 解码失败就试 URL-safe 变体,接着试 hex 转字符串,再判断是不是哈希摘要。把算法类别定位到“对称加密”或“哈希摘要”之后,再去翻代码或者找开发确认,效率会高很多。记住一点:不管最终确定是什么算法,一定要拿到一个“已知明文和已知密文”的对照样本,否则后面自己实现解密时无从验证。
2.2 AES 对称加解密:先写一个能跑通的解密脚本
AES 是接口测试里最常见的对称加密算法,特点是加密和解密用同一个密钥。实际项目里经常能见到AES/CBC/PKCS7Padding这种组合,这里的三个要素缺一不可:算法模式(CBC)、填充方式(PKCS7)、以及 CBC 模式必须用到的初始向量 IV。
我一般会用 Python 的pycryptodome库来跑,安装命令是pip install pycryptodome。下面是一个可以直接用的 AES-CBC 解密脚本:
import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def aes_decrypt(cipher_text_b64: str, key: str, iv: str) -> str: key_bytes = key.encode("utf-8") iv_bytes = iv.encode("utf-8") cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) plain_bytes = unpad( cipher.decrypt(base64.b64decode(cipher_text_b64)), AES.block_size ) return plain_bytes.decode("utf-8") # 示例参数,实际请换成项目里的测试密钥 key = "0123456789abcdef" iv = "abcdef9876543210" cipher_text = "从抓包里复制的密文" print(aes_decrypt(cipher_text, key, iv))用的时候注意几个坑。第一,密钥长度决定 AES 的等级:16 字节是 AES-128,24 字节是 AES-192,32 字节是 AES-256,搞错长度连初始化都会报错。第二,CBC 模式必须用到 IV,如果项目里用了 ECB 模式,那就不需要 IV,写法要相应调整。第三,填充方式报错通常意味着密钥或 IV 不对,不要急着怀疑算法写错了。
还有一点特别容易踩:有些服务端返回的密文不是 Base64,而是十六进制字符串。你拿base64.b64decode去解必然失败,改成bytes.fromhex()就行。我建议把解密函数再做一层适配,遇到无法解码的情况自动尝试另一套编码。
2.3 RSA 与混合加密:私钥在手里才能测
RSA 是典型的非对称加密,公钥加密、私钥解密,或者私钥签名、公钥验签。接口测试里遇到的 RSA 场景大多是登录加密或者报文签名,例如前端用后端下发的公钥把密码加密,后端用私钥解开;或者后端用私钥对响应做签名,前端用公钥验签。
用 Python 的rsa库处理比较直接:
import rsa import base64 # 加载公钥和私钥(pem 格式字符串) pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(pub_pem.encode("utf-8")) pri_key = rsa.PrivateKey.load_pkcs1(pri_pem.encode("utf-8")) # 私钥解密 cipher_bytes = base64.b64decode(cipher_text) plain_bytes = rsa.decrypt(cipher_bytes, pri_key) print(plain_bytes.decode("utf-8")) # 公钥验签 signature = base64.b64decode(sign_text) rsa.verify(message_bytes, signature, pub_key)RSA 有一个无法回避的问题:纯 RSA 加密只能加密比密钥短很多的数据,而且性能差。所以现实项目里大多数是混合加密——业务数据用 AES 加密,AES 的密钥再用 RSA 加密传输。测试这种接口的时候,逻辑上要把两条链路都打通:先用 RSA 解出 AES 密钥,再用 AES 解出真正的业务数据。
在测试环境里,我强烈建议你问开发要一套专用的密钥对,私钥留在服务端、公钥下发到客户端。测试代码里只需要保存解密的私钥或者测试密钥,千万别去碰生产环境的私钥,这不是能力问题,是风险问题。
2.4 容易被忽略的编码细节与加密变体
我刚开始处理加密接口时,经常碰到算法完全正确、参数也一模一样,但解密出来就是乱码或者报错的情况。后来才发现问题全出在“看起来不起眼”的编码细节上。
第一个是 Base64 变体。标准 Base64 会包含+、/、=,但有些系统为了把密文塞进 URL 参数或 JSON 里,会使用 URL-safe Base64,把+换成-,把/换成_,还可能去掉末尾的=。你用标准 Base64 解码自然失败,或者解出错误内容。
第二个是字符集。服务端用的可能是 UTF-8,也可能是 ISO-8859-1,同一个中文字符在不同字符集下的字节表示完全不同,影响解密结果和签名结果。我遇到过整个端到端调不通,最后发现是服务端配置了ISO-8859-1,测试代码却按 UTF-8 去签名,两边摘要永远对不上。
第三个是填充差异。AES 的填充方式除了 PKCS7,还有 ZeroPadding(补零)、NoPadding(不补)。如果服务端用 ZeroPadding、你按 PKCS7 解,要么报错要么得到带零字节的字符串。我每次对接新接口都会先问清楚模式、填充、字符集这三样,比猜快得多。
| 看似无关的细节 | 实际影响 | 处理建议 |
|---|---|---|
| Base64 标准/URL-safe | 解码失败、乱码 | 先尝试replace后再解码 |
| UTF-8 vs 其他字符集 | 解密乱码、签名不一致 | 统一约定 UTF-8,严禁混用 |
| 明文里是否带引号或空格 | 签名拼接不一致 | 严格按服务端规则处理边界符号 |
| 是否对密文再做 URL 编码 | 双重编码后长度变化 | 按实际报文判断是否需要unquote |
3. 实战:从抓包到破解一个签名登录接口
3.1 一个模拟登录接口的抓包分析
假设现在要测某个 App 的用户登录接口,接口文档写得很简单,请求参数有account、password、timestamp、nonce、sign五个字段。文档里只说“签名规则详见前端实现”,没有给密钥。
用抓包工具把 App 登录过程的请求截下来,你会看到密码字段不是明文,而是一串 Base64 文本;sign是一个 64 位的十六进制字符串。这种设计基本可以推断出:密码字段单独加密,sign是完整请求的签名摘要。测试要做的就是搞清楚两件事:密码用什么算法和密钥加密,签名用什么规则生成。
抓包这一步非常关键,一定要把完整的请求头、请求体、响应体都保存下来。它既是后续对比的基线,也是往自动化测试里加解密逻辑的参考依据。不要看完就删,后面排查问题全靠它。
3.2 逆向签名规则:从前端代码和后端文档里确认算法
这里的“逆向”不是去攻击外部服务,而是你负责测试的接口,前端代码就在你手里,需要基于接口文档和前端实现把签名规则确认下来。最常见的方式是静态分析:在浏览器或调试工具里搜索sign、encrypt、secret这些关键词,定位到加密工具函数。
通过这个模拟场景,整理出的签名规则是:
password先使用 AES-CBC 加密并转 Base64,密钥为 16 字节的aesKey;- 排除
sign本身后,把所有参数按照参数名 ASCII 码升序排序; - 按
key=value形式拼接,中间用&连接; - 在拼接字符串末尾追加
&secret=你拿到的测试密钥; - 对最终字符串做 HMAC-SHA256,输出小写十六进制作为
sign。
真实项目里的规则会更多变,有的用 MD5,有的嵌套两层摘要,有的把非空参数过滤一遍再排。这些都不怕,怕的是你拿到规则后不去验证。验证的方法是最笨也最有效的:把从真实抓包里拿到的参数代入你的算法,如果生成的sign和抓包里的完全一致,说明规则搞对了,可以进入下一步。
3.3 用 Python 还原签名生成逻辑
确定规则之后,把它翻译成 Python 代码。下面是完整可运行的模拟实现:
import time import random import string import hmac import hashlib import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad AES_KEY = "0123456789abcdef" AES_IV = "abcdef9876543210" APP_SECRET = "test-app-secret" APP_ID = "test_app_001" def aes_encrypt(plain_text: str, key: str, iv: str) -> str: key_bytes = key.encode("utf-8") iv_bytes = iv.encode("utf-8") cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) encrypted = cipher.encrypt(pad(plain_text.encode("utf-8"), AES.block_size)) return base64.b64encode(encrypted).decode("utf-8") def random_nonce(length: int = 16) -> str: return "".join(random.choices(string.ascii_letters + string.digits, k=length)) def build_sign(params: dict, secret: str) -> str: # 排除 sign 本身,按参数名排序 keys = sorted([k for k in params if k != "sign"]) raw = "&".join(f"{k}={params[k]}" for k in keys) raw = f"{raw}&secret={secret}" return hmac.new(secret.encode("utf-8"), raw.encode("utf-8"), hashlib.sha256).hexdigest() params = { "appId": APP_ID, "account": "test_user", "password": aes_encrypt("Abc@123456", AES_KEY, AES_IV), "timestamp": str(int(time.time())), "nonce": random_nonce(), } params["sign"] = build_sign(params, APP_SECRET) print(params)这段代码有两个细节值得留意。第一,参与签名的是password加密后的密文字符串,不是用户的明文密码。很多人在这里翻车,以为签名应该对明文做摘要,结果怎么算都对不上。第二,时间戳用的是当前时间,如果测试机和服务端时间差太多,签名就算正确也会因为时间窗口过期而被拒绝,后面单独说。
3.4 把签名请求接入你的测试用例
签名规则跑通之后,下一步就是把整个逻辑封装进requests请求流程:
import requests def login(account: str, password: str, base_url: str) -> dict: params = { "appId": APP_ID, "account": account, "password": aes_encrypt(password, AES_KEY, AES_IV), "timestamp": str(int(time.time())), "nonce": random_nonce(), } params["sign"] = build_sign(params, APP_SECRET) resp = requests.post( f"{base_url}/api/login", json=params, headers={"Content-Type": "application/json"} ) data = resp.json() return data result = login("test_user", "Abc@123456", "接口地址按项目实际配置填") print(result)在这个阶段,我建议你做三件事。第一,先不急着写测试用例,而是跑通一条“真实请求”,把请求日志和响应原文完整打印出来,检查sign是否被服务端接受。第二,把timestamp、nonce、password这些动态字段记录到日志里,方便后续排查。第三,把密钥放到配置文件或环境变量里,不要硬编码在测试代码中。
我还吃过一个亏:刚开始为了图方便,把所有请求打到一个公共日志文件里,结果密钥和密文全混在一起,排查时很难看。后来改成每个用例生成一个独立日志子目录,请求参数和响应分开保存,效率高了不少。
4. 站在“攻”的角度设计测试用例
4.1 时间戳与随机数:防重放机制到底怎么测
接口测试里常说的“攻”,并不是真的入侵系统,而是模拟攻击手法来验证服务端的防御是否有效。防重放是其中最典型的场景。攻击者截获一次合法请求后,不需要破解密码,只需要把请求原样再发送一次即可,如果服务端不校验时间戳和随机数,这个请求就可能再次生效。
所以测试用例至少要覆盖这几条:
- 把捕获到的请求原样重放,观察是否被拒绝;
- 把
timestamp改成当前时间前后 5 分钟之外的值,观察是否提示过期; - 把
nonce改成和上次请求相同,观察是否被拦截; - 把请求里的任意业务字段改动后重放,观察签名校验是否拦截。
我实测下来,做得好的服务端会同时校验时间窗口和 nonce 唯一性,三重防护都会生效。做得粗糙的只会看时间戳,不会存 nonce,这意味着同一个 nonce 在有效期内可以反复使用,是有隐患的。
测试时还有一个边界:服务端校验时间窗口用的是哪个时区。如果服务端按 UTC 存储、客户端按北京时间生成时间戳,就会出现“明明刚刚生成的请求,服务端却认为过期”的诡异问题。建议测试前先确认时间基准,再设计边界用例。
4.2 篡改参数与签名校验失效测试
这是加密接口测试里最容易发现漏洞的一种用例。原理很简单:把请求里的某个参数值换掉,但保持sign不变,然后看服务端是否还会正常处理。
具体可以这样设计:
- 登录接口里把
account从test_user改成admin,sign不变; - 删除请求里的
sign字段; - 把
sign改成全0或截断一半; - 把参数名从
account改成Account,值不变; - 把数字字段从
1改成2。
正确的服务端行为应该是对参数重新计算签名,和请求里的sign比对,不一致就返回签名错误或直接拒绝。如果服务端在签名校验失败后仍然返回业务正常,说明它没有把签名校验结果真正应用到业务逻辑里,这就是一个必须提 bug 的高优问题。
我在真实项目里就碰到过一种情况:网关做了签名校验,但业务应用层直接信任了网关传过来的明文参数,前后端之间没有二次校验。测试直接绕过网关调用业务接口,照样能篡改数据成功。所以攻防测试不能只测“入口”,要测“整条链路”。
4.3 密钥安全与异常密文输入测试
很多接口的加密防护实际上建立在一个脆弱的前提下:密钥藏得住。测试人员可以顺手检查几件事:前端代码里是否硬编码了密钥、日志里是否打印过密钥、接口文档里是否出现生产密钥。密钥一旦泄露,所有加密形同虚设。
除了检查密钥本身,还要对解密逻辑做异常输入测试。常见的异常输入包括:
- Base64 字符串里插入非法字符;
- 密文长度不是 16 的倍数;
- 空字符串、null、布尔值等非常规类型;
- 超长密文,比如几百 KB 的随机文本;
- 明文 JSON 结构缺失或字段类型对不上。
这类用例的目标是验证服务端在解密失败时会不会给出友好的错误,而不会堆栈溢出、数据库报错、或者把解密后的异常数据写入业务表。我习惯把这类用例归到“健壮性测试”,数量不用太多,重点覆盖几种最典型的输入就够。
4.4 加解密对性能测试的影响
AES 加解密的性能损耗不算大,但 RSA 这类非对称运算开销高得多,如果把整个请求体都用 RSA 加密,服务端 CPU 压力会非常明显。我在性能测试中做过一次对比:同样一批请求,经过全链路 RSA 加密比不加密的吞吐量低了不少。
建议在性能测试方案里加一组“带加密 vs 不带加密”的对照数据。你可以构造两个版本的测试数据,一个走完整加解密链路,一个走临时放开的明文链路,分别记录 TPS、响应时间和 CPU 占用。这样能直观量化加密对性能的影响,给容量评估提供依据。
还有一个并发环境的坑:很多加密库的加解密对象并不是线程安全的,比如某些库的 AES 对象在并发调用时会出现随机失败。写并发测试脚本时,最好每次请求都新建加解密实例,或者使用线程局部存储,不要在整个工具类里放一个全局共享的加密对象。
5. 自动化测试框架里的加解密集成方案
5.1 统一封装加解密工具模块
手动在用例里写加密逻辑,很快就会失控。更好的做法是把加解密封装成一个独立工具模块,测试用例只关心业务参数,不关心加密细节。
下面是一个简化版的封装:
class CryptoUtil: def __init__(self, config: dict): self.aes_key = config["aes_key"] self.aes_iv = config["aes_iv"] self.app_secret = config["app_secret"] self.app_id = config["app_id"] def encrypt_sensitive(self, plain_text: str) -> str: return aes_encrypt(plain_text, self.aes_key, self.aes_iv) def generate_sign(self, params: dict) -> str: return build_sign(params, self.app_secret) def decrypt_response(self, cipher_text: str) -> dict: plain_text = aes_decrypt(cipher_text, self.aes_key, self.aes_iv) import json return json.loads(plain_text)这样的工具类不仅服务于用例,还可以配合配置文件实现不同环境的切换。测试环境用一套测试密钥,预发环境用另一套密钥,密钥从环境变量或配置中心读取,避免在代码仓库里留下敏感信息。
5.2 在请求与响应链路上自动处理
把加解密逻辑压到更底层,可以让测试代码真正专注于业务。我常用的一种做法是封装一个带加解密能力的 API Client:
class EncryptedApiClient: def __init__(self, base_url: str, crypto: CryptoUtil): self.base_url = base_url self.crypto = crypto self.session = requests.Session() def post(self, path: str, data: dict): # 1. 对敏感字段加密 encrypted_data = { "account": data["account"], "password": self.crypto.encrypt_sensitive(data["password"]), "timestamp": str(int(time.time())), "nonce": random_nonce(), } # 2. 生成签名 encrypted_data["sign"] = self.crypto.generate_sign(encrypted_data) # 3. 发起请求 resp = self.session.post(f"{self.base_url}{path}", json=encrypted_data) # 4. 解密响应 body = resp.json() if body.get("data"): body["data"] = self.crypto.decrypt_response(body["data"]) return body在这个方案里,用例层只需要传明文业务参数,加解密和签名都由客户端自动完成。遇到需要验证签名错误、篡改参数的异常场景,再单独绕过这个封装直接构造原始请求即可。
5.3 测试数据与断言策略怎么设计
对加密接口做断言时,我一般分两层。第一层是“密文层断言”,验证服务端响应是否是合法的密文格式,比如 Base64 可解码、密文长度合理。第二层是“明文层断言”,解密之后再验证业务字段是否符合预期。
常见的误区是只对密文做断言,看到响应体是 Base64 就认为测试通过。实际上密文内容很可能对应着错误信息,比如“用户不存在”,所以在业务测试里必须有解密后的断言。
还有一个小技巧:用同一份明文数据发起两次请求,断言两次密文不同(因为有随机 nonce 或随机 IV),但解码后的明文一致。这能验证加密方案是否真正引入了随机性,防止出现“同一个明文加密后密文固定”的严重安全问题。
测试数据方面,我建议准备一份“标准样本集”:包含正常的账号密码、超长字符串、特殊字符、中文用户名、空值等。每次跑回归都使用同一批样本,方便对比不同版本的加密实现是否发生变化。
6. 实战中的高频问题与排查手册
6.1 高频问题速查表
这些年在接口测试中用加解密,最常见的问题集中在下面这些点,我整理成一个速查表:
| 症状 | 可能原因 | 处理建议 |
|---|---|---|
| 请求报“签名错误” | 拼接顺序、分隔符、secret 不一致 | 用真实抓包样例验证生成结果 |
| 解密出来是乱码 | 字符集不一致 | 统一使用 UTF-8,检查服务端配置 |
| Base64 解码失败 | 不是标准 Base64,可能是 URL-safe 或 hex | 替换-、_,尝试 hex 解码 |
| 提示时间戳过期 | 测试机时间与服务端时钟偏差大 | 同步时钟或使用服务端下发的时间 |
| AES 解密报 padding 错误 | key、iv、填充模式不匹配 | 核对算法参数,找开发确认模式 |
| 本地能调通、线上不行 | 测试环境和线上密钥不同 | 使用专用测试密钥,不要跨环境混用 |
| 并发测试随机失败 | 加密对象线程不安全 | 每次请求新建实例或使用线程局部存储 |
6.2 一条可复用的排查路径
遇到加密接口问题,我习惯用二分法缩小范围。先把问题分为两类:一类是“签名验证失败”,另一类是“解密结果不对”。
如果是签名失败,先别改代码,拿一个真实抓包的请求,把参数复制出来,用你的签名函数重新生成一遍。如果生成的sign和抓包不一致,基本可以断定是你的签名规则或参数拼接有问题。这时候重点检查:排序是否区分大小写、是否排除了sign本身、参与签名的是密文还是明文、末尾是否需要拼接 secret。
如果是解密问题,先确认算法参数的三件套:密钥、IV、填充方式。然后把密文放到解密脚本里跑,看报错类型。如果输出乱码但没报错,检查字符集。如果完全解不出来,建议让开发给一个“明文+密钥+密文对照样例”,用这个样例把你的解密脚本调通,再接入测试框架。
我自己踩过最折腾的一个坑是:开发用的签名规则里把参数值做了trim(),所有值首尾空格都要去掉,但我的脚本没有处理,导致 99% 的 case 都通过、唯独包含空格参数的接口签名失败。这种问题靠日志根本看不出来,只能通过“逐个字符对比拼接结果”才定位到。
6.3 最后说点我的个人经验
关于加密接口测试,我最大的体会是:不要尝试绕过加密,而是尽早参与接口设计。你在接口评审阶段就确认加密方案、密钥管理方式、明文结构,后面测试会顺利很多。等到联调阶段才开始接触加密,大概率会被动。
另外建议把加解密工具类当作被测对象的一部分来维护。它一天不跑,你所有测试用例其实都是废的。我通常会在测试框架的冒烟用例里放一条“加解密自检”用例,专门用已知明文和预置密钥验证解密函数的正确性。
还有一点很重要:密钥过期问题。测试密钥和线上密钥一样会定期轮换,轮换之后所有依赖旧密钥的测试都会挂。可以把密钥的到期时间做个标记,在密钥过期之前自动测试并提醒相关同学更新配置,避免“忽然全红”才去查原因的尴尬。
如果你能把前面这些思路落地到自己的测试项目里,下次再看到sign、timestamp、encrypt这些字段,就不会先想着找开发去掉了,而是直接打开抓包工具,把加密链路梳理清楚,再把它变成一组严谨的攻防测试用例。这不仅是接口测试能力的提升,也是你对接口安全理解的深化。