通过拦截器在 fetch 返回的 promise 链中处理响应,先提取原始数据,再使用 crypto.subtle 进行验签,验证通过后才将数据传递给后续逻辑。具体做法是封装一个自定义的 requestwithverify 函数,该函数接收请求配置和公钥参数,内部调用 fetch 获取响应后,解析 json 并校验签名字段,若校验失败则抛出异常。

如何封装支持验签的请求函数
封装的核心在于利用 Promise 的链式调用特性,在 fetch 完成网络请求后、业务代码消费数据前插入验签步骤。需要确保服务器返回的数据结构中包含用于验签的 sign 字段和原始数据 data。验签过程需要将原始数据序列化为字符串,并使用 Web Crypto API 的 verify 方法比对签名。如果签名无效,直接 reject 错误,阻止脏数据进入应用层。

const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
async function requestWithVerify(url, options = {}, publicKeyPEM) {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
if (!result.sign || !result.data) {
throw new Error('Invalid response structure');
}
const isValid = await verifySignature(result.data, result.sign, publicKeyPEM);
if (!isValid) {
throw new Error('Signature verification failed');
}
return result.data;
}
async function verifySignature(data, signatureBase64, publicKeyPEM) {
const algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
const pubKeyObj = await crypto.subtle.importKey(
'pem', publicKeyPEM, algorithm, false, ['verify']
);
const sigBuffer = Uint8Array.from(atob(signatureBase64), c => c.charCodeAt(0));
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
const dataBuffer = new TextEncoder().encode(dataStr);
return await crypto.subtle.verify(algorithm, pubKeyObj, sigBuffer, dataBuffer);
}
上述代码展示了完整的实现逻辑。requestWithVerify 函数首先发起标准 fetch 请求,随后解析 JSON 响应体。接着调用 verifySignature 辅助函数,该函数负责将 PEM 格式的公钥导入为 CryptoKey 对象,并将 Base64 编码的签名转换为二进制缓冲区。最后,使用 crypto.subtle.verify 对序列化后的数据进行校验。调用方只需传入 URL、请求选项和公钥即可安全获取数据。
立即学习“Java免费学习笔记(深入)”;
如何处理验签失败与重试机制
当验签失败时,通常意味着数据在传输过程中被篡改或服务器私钥泄露,此时不应简单重试,而应记录日志并终止当前操作。但在某些场景下,如临时网络波动导致签名校验异常(极少见),可设计有限次数的重试逻辑。更常见的情况是,验签失败后需要触发特定的错误处理流程,例如刷新 Token 或通知用户。这里重点讨论如何在封装中优雅地处理这些分支,确保主程序不因单一请求失败而崩溃。

async function safeRequest(url, options, publicKeyPEM, maxRetries = 1) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const data = await requestWithVerify(url, options, publicKeyPEM);
return { success: true, data };
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt + 1} failed:`, error.message);
if (error.message.includes('Signature')) {
// 签名错误通常不可重试,直接跳出
break;
}
}
}
return { success: false, error: lastError };
}
这段代码引入了 safeRequest 包装器,它循环调用之前的 requestWithVerify。在 catch 块中,我们检查错误信息是否包含 'Signature'。如果是签名验证失败,说明数据完整性受损,重试无意义,因此立即 break 退出循环。对于其他类型的错误(如网络超时),则根据 maxRetries 配置继续尝试。最终返回一个统一的结果对象,包含 success 状态和对应的数据或错误对象,便于上层组件统一处理成功与失败的路径。
基于特定输入数据的验签结果分析
假设服务器返回如下 JSON 数据:{"data": {"id": 1, "name": "test"}, "sign": "base64_encoded_signature"}。若客户端使用的公钥与服务端私钥不匹配,crypto.subtle.verify 将返回 false。此时 requestWithVerify 会抛出 'Signature verification failed' 异常。若数据被恶意篡改,例如将 name 改为 'hacked' 但签名未更新,验签同样会失败。只有当 data 字段内容与生成签名时的内容完全一致,且公钥正确时,验签才会通过并返回 data 部分。


















