You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:50:55