使用WinInet.h发送请求始终返回"HMAC签名不匹配"的问题
核心问题分析
Python和CURL使用相同签名逻辑可正常请求,但WinInet实现返回"HMAC签名不匹配",且已确认请求头值一致但顺序被WinInet调整。结合WinInet的特性,问题大概率出在WinInet自动添加的额外请求头或头部处理细节差异上。
具体排查步骤
1. 检查WinInet自动添加的隐藏请求头
WinInet会自动为请求添加默认头(如Accept-Encoding、Connection、Accept等),这些头部可能被服务器纳入HMAC签名计算范围,但你的Python代码未包含这些头部,导致签名不匹配。
可通过HttpQueryInfo获取实际发送的所有请求头:
char receivedHeaders[8192]; DWORD headersLen = sizeof(receivedHeaders); if (HttpQueryInfoA(Connect, HTTP_QUERY_RAW_HEADERS_CRLF, receivedHeaders, &headersLen, NULL)) { cout << "实际发送的请求头:\n" << receivedHeaders << endl; }
将获取到的所有头部(包括WinInet自动添加的)加入签名计算的头部列表,重新生成X-Ca-Signature。
2. 改用分步请求构建流程
InternetOpenUrl会自动处理头部和请求细节,建议改用手动分步构建请求的方式,避免WinInet的自动行为干扰:
- 使用
InternetConnect建立连接 - 使用
HttpOpenRequest创建请求句柄 - 使用
HttpAddRequestHeaders手动添加所有请求头 - 使用
HttpSendRequest发送请求
示例代码:
void send_request(const string& url, const string& headers, string* htmltext) { HINTERNET hSession = InternetOpenA("Mozilla/5.0 (Windows NT 10.0; Win64; x64)", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); if (!hSession) { *htmltext = "InternetOpenA Error"; return; } // 解析URL的主机名和路径(可自行实现或调用UrlSplit等系统API) string host = "your-domain.com"; string path = "/your-api-path"; HINTERNET hConnect = InternetConnectA(hSession, host.c_str(), INTERNET_DEFAULT_HTTPS_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); if (!hConnect) { *htmltext = "InternetConnectA Error"; InternetCloseHandle(hSession); return; } HINTERNET hRequest = HttpOpenRequestA(hConnect, "GET", path.c_str(), NULL, NULL, NULL, INTERNET_FLAG_SECURE | INTERNET_FLAG_RELOAD, 0); if (!hRequest) { *htmltext = "HttpOpenRequestA Error"; InternetCloseHandle(hConnect); InternetCloseHandle(hSession); return; } // 手动添加请求头,禁用WinInet自动合并或替换 BOOL addHeadersOk = HttpAddRequestHeadersA(hRequest, headers.c_str(), headers.length(), HTTP_ADDREQ_FLAG_REPLACE | HTTP_ADDREQ_FLAG_COALESCE); if (!addHeadersOk) { *htmltext = "HttpAddRequestHeadersA Error"; InternetCloseHandle(hRequest); InternetCloseHandle(hConnect); InternetCloseHandle(hSession); return; } BOOL sendOk = HttpSendRequestA(hRequest, NULL, 0, NULL, 0); if (!sendOk) { *htmltext = "HttpSendRequestA Error"; InternetCloseHandle(hRequest); InternetCloseHandle(hConnect); InternetCloseHandle(hSession); return; } // 读取响应内容 DWORD BytesRead; string response; char buffer[4096]; while (InternetReadFile(hRequest, buffer, sizeof(buffer), &BytesRead) && BytesRead > 0) { response.append(buffer, BytesRead); } *htmltext = response; // 释放句柄 InternetCloseHandle(hRequest); InternetCloseHandle(hConnect); InternetCloseHandle(hSession); }
3. 验证请求头的大小写一致性
虽然HTTP标准规定头部名称大小写不敏感,但部分签名算法会严格要求头部名称大小写匹配(比如X-Ca-Signature-Headers指定小写x-ca-key,x-ca-nonce,但WinInet可能发送X-Ca-Key,X-Ca-Nonce)。
确保签名计算时使用的头部名称与WinInet实际发送的完全一致,或调整X-Ca-Signature-Headers的值为WinInet发送的头部名称大小写。
4. 检查URL路径的处理差异
WinInet可能会对URL路径做自动调整(如规范化路径、转换大小写),如果服务器签名计算包含URL路径部分,会导致签名不匹配。可通过HttpQueryInfo获取实际发送的路径:
char actualPath[1024]; DWORD pathLen = sizeof(actualPath); if (HttpQueryInfoA(hRequest, HTTP_QUERY_PATH_INFO, actualPath, &pathLen, NULL)) { cout << "实际发送的路径:\n" << actualPath << endl; }
用实际路径重新计算签名,或确保WinInet发送的路径与Python一致。
5. 禁用WinInet的自动压缩
WinInet默认会添加Accept-Encoding: gzip, deflate,如果服务器在签名计算时包含该头部,而Python代码未发送,会导致不匹配。可通过InternetSetOption禁用自动压缩:
DWORD disableCompression = 1; InternetSetOptionA(hRequest, INTERNET_OPTION_HTTP_DECODING, &disableCompression, sizeof(disableCompression));
总结
优先排查WinInet自动添加的额外请求头,改用分步请求构建方式精确控制头部内容,确保签名计算时包含所有实际发送的头部和URL路径细节,基本可以解决签名不匹配的问题。
内容的提问来源于stack exchange,提问作者Scott Paang

