C++中无法生成有效Azure Storage SAS令牌问题排查
Azure Storage SAS令牌生成失败排查与修复
问题详情
无法生成有效的Azure Storage SAS令牌,此前正常运行的代码当前失效,请求返回如下错误:
return (<Error> AuthenticationFailed</Code> <Message> Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. RequestId:b56dca11-101e-0036-08f4-3c47cd000000 Time:2024-11-22T15:41:17.0196662Z </Message> <AuthenticationErrorDetail> Signature did not match. String to sign used was rwl 2024-11-22T15:38:31Z 2024-11-22T17:38:31Z /blob/fotacontainer/fota/testfile.txt https 2022-11-02 b </AuthenticationErrorDetail> </Error> )
生成的访问URL:
https://fotacontainer.blob.core.windows.net/fota/testfile.txt?sv=2022-11-02&sr=b&sp=rwl&st=2024-11-22T15:38:31Z&se=2024-11-22T17:38:31Z&spr=https&sig=EMswjhCIzPCd8OBU48OV2suiJI4HNKl94rB5ctyaGGA%3d
当前代码实现
std::string GenerateSas(const std::string& storageAccountKey, const std::string& input) { std::string decodedKey; StringSource(storageAccountKey, true, new Base64Decoder(new StringSink(decodedKey))); // Create HMAC-SHA256 signature std::string digest; CryptoPP::HMAC<CryptoPP::SHA256> hmac((const byte*)decodedKey.data(), decodedKey.size()); StringSource(input, true, new HashFilter(hmac, new StringSink(digest))); // Compute HMAC digest // Encode the digest to Base64 std::string encodedDigest; StringSource(digest, true, new Base64Encoder(new StringSink(encodedDigest), false)); encodedDigest.erase(std::remove(encodedDigest.begin(), encodedDigest.end(), '\n'), encodedDigest.end()); return encodedDigest; } std::string TimePointToString(const std::chrono::seconds& timePoint) { std::time_t t = timePoint.count(); std::tm tm{}; gmtime_r(&t, &tm); std::ostringstream oss; oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ"); return oss.str(); } std::string crypto_GenerateSasToken( const std::string& permissions, const std::string& blobName, const std::string& accessKey, const std::string& containerName, const std::string& accountName) { // Define SAS token parameters auto currentTime = std::chrono::system_clock::now(); auto currentTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(currentTime.time_since_epoch()); auto expirationTime = currentTimeSeconds + std::chrono::minutes(120); // Convert time to ISO 8601 format std::string signedStart = TimePointToString(currentTimeSeconds); std::string signedExpiry = TimePointToString(expirationTime); // Define other SAS parameters std::string signedPermissions = permissions; //"racwdxl"; std::string signedService = "b"; //(blobName.empty()) ? "c" : "b"; // 'c' for container, 'b' for blob std::string signedProtocol = "https"; std::string signedVersion = "2022-11-02"; // Create canonicalized resource std::string canonicalizedResource; if (blobName.empty()) { // Use container-level resource if blobName is empty canonicalizedResource = "/blob/" + accountName + "/" + containerName; } else { // Use blob-level resource if blobName is not empty canonicalizedResource = "/blob/" + accountName + "/" + containerName + "/" + blobName; } // Construct the string to sign std::ostringstream stringToSign; stringToSign << signedPermissions << " " << signedStart << " " << signedExpiry << " " << canonicalizedResource << " " << " " << " " << signedProtocol << " " << signedVersion << " " << signedService << " " << " " << " " << " " << " " << " " ; // Generate the signature std::string signature = GenerateSas(accessKey, stringToSign.str()); // URL encode the signature std::string urlEncodedSignature = UrlEncode(signature); // Construct the SAS token std::ostringstream sasToken; sasToken << "sv=" << signedVersion << "&sr=" << signedService << "&sp=" << signedPermissions << "&st=" << signedStart << "&se=" << signedExpiry << "&spr=" << signedProtocol << "&sig=" << urlEncodedSignature; std::cout << "Blob URL: https://" << accountName << ".blob.core.windows.net/" << containerName << "/" << blobName << "?" << sasToken.str() << std::endl; return sasToken.str(); } // URL encode function std::string UrlEncode(const std::string& str) { std::ostringstream escaped; for (char c : str) { if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { escaped << c; } else { escaped << "%" << std::setw(2) << std::setfill('0') << std::hex << (int)(unsigned char)c; } } return escaped.str(); };
问题分析与修复
1. 待签名字符串格式错误
从错误信息可见,服务器收到的待签名字符串是空格分隔的,但Azure要求每个参数必须单独占一行,空参数也需保留对应换行符。当前代码构造stringToSign时使用了直接换行(" "),会导致格式错误,必须替换为转义字符"\n"。
2. 待签名字符串参数顺序修正
针对API版本2022-11-02,Blob服务的待签名字符串需严格遵循以下顺序:
- 权限 (
sp) - 起始时间 (
st) - 过期时间 (
se) - 规范化资源
- 签名标识符(空则保留换行)
- IP范围(空则保留换行)
- 协议 (
spr) - API版本 (
sv) - 资源类型 (
sr) - 快照时间(空则保留换行)
- 加密范围(空则保留换行)
- 目录深度(空则保留换行)
- 容器删除权限(空则保留换行)
- 不可变策略条件(空则保留换行)
当前代码顺序基本正确,但需确保空参数对应的换行符数量准确,移除多余空行。
3. SAS令牌参数顺序说明
SAS令牌中的参数顺序不需要与待签名字符串顺序一致,服务器会根据参数名(如sp、st)解析,而非顺序。
修复后的核心代码片段
// 修正后的待签名字符串构造 std::ostringstream stringToSign; stringToSign << signedPermissions << "\n" << signedStart << "\n" << signedExpiry << "\n" << canonicalizedResource << "\n" << "\n" // 签名标识符(空) << "\n" // IP范围(空) << signedProtocol << "\n" << signedVersion << "\n" << signedService << "\n" << "\n" // 快照时间(空) << "\n" // 加密范围(空) << "\n" // 目录深度(空) << "\n" // 容器删除权限(空) << "\n"; // 不可变策略条件(空)
其他检查点
- 恢复
signedService的动态判断逻辑,确保容器级SAS令牌生成正确:std::string signedService = (blobName.empty()) ? "c" : "b"; // 'c' for container, 'b' for blob - 确认
GenerateSas函数中存储账户密钥的Base64解码、HMAC-SHA256签名生成及Base64编码流程正确(当前代码已处理多余换行,符合要求)。 - 确认
UrlEncode函数正确处理Base64签名中的特殊字符(如+、/、=),当前实现符合URL编码规范。
内容的提问来源于stack exchange,提问作者Stefan Gudmundsson
相关产品推荐
相关产品推荐

