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

使用HMAC-SHA256调用NetSuite WebAPI时OAuth1.0签名无效问题

问题描述

我正在尝试用C#通过OAuth 1.0调用NetSuite API。请求在Postman里能正常运行,把Postman生成的timestamp、nonce、signatureBase值放到C#代码里也能成功调用,但自己代码生成的签名会返回INVALID_LOGIN_ATTEMPT错误。以下是相关代码:


原代码

主方法

string consumerKey = "******";
string TokenId = "******";
string ConsumerSecret = "******";
string TokenSecret = "******";

string realm = "****";
string signatureMethod = "HMAC-SHA256";

string apiUrl = "https://test-test.api.netsuite.com/app/site/hosting/restlet.nl?script=941&deploy=1&FC=SGD&TC=USD&ED=02/11/2022";
string baseUrl = "https://test-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl";

public void run()
{
    var timeStamp = ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString();
    var nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(timeStamp));

    var signatureBase = GetSignatureBaseString(timeStamp, nonce);
    var oauthSignature = encryptSignatureBase(signatureBase, ConsumerSecret, TokenSecret);

    var client = new RestClient(apiUrl);
    var request = new RestRequest("", Method.Get);

    //test with postman values, which is working fine.
    //timeStamp = "1669601987";
    //nonce = "5D1TNfnx5oW";
    //oauthSignature = "C8TXCdaT1If0Od6BU4T8yqukRjFSAz%2BtrO3cZi8lUvw%3D";

    var oauthStr = $"OAuth realm =\"{realm}\",oauth_consumer_key=\"{consumerKey}\",oauth_token=\"{TokenId}\",oauth_signature_method=\"HMAC-SHA256\",oauth_timestamp=\"{timeStamp}\",oauth_nonce=\"{nonce}\",oauth_version=\"1.0\",oauth_signature=\"{oauthSignature}\"";
   
    request.AddHeader("Authorization", oauthStr);
    request.AddHeader("Content-Type", "application/json");
    request.AddHeader("Cookie", "NS_ROUTING_VERSION=LAGGING");
    request.AddParameter("application/json", "{}", ParameterType.RequestBody);

    var response = client.Execute(request);
    Console.WriteLine(response.Content);
}

生成签名基串

string GetSignatureBaseString(string timestamp, string nounce)
{
    //1.Convert the HTTP Method to uppercase and set the output string equal to this value.
    string Signature_Base_String = "GET";
    Signature_Base_String = Signature_Base_String.ToUpper();

    //2.Append the ‘&’ character to the output string.
    Signature_Base_String = Signature_Base_String + "&";

    SortedDictionary<string, string> paramStrings = new SortedDictionary<string, string>();
    paramStrings.Add("script", "941");
    paramStrings.Add("deploy", "1");
    paramStrings.Add("FC", "SGD");
    paramStrings.Add("TC", "USD");
    paramStrings.Add("ED", "02/11/2022");

    //3.Percent encode the URL and append it to the output string.
    string PercentEncodedURL = Uri.EscapeDataString(baseUrl);
    Signature_Base_String = Signature_Base_String + PercentEncodedURL;

    //4.Append the ‘&’ character to the output string.
    Signature_Base_String = Signature_Base_String + "&";

    //5.append OAuth parameter string to the output string.
    var parameters = new SortedDictionary<string, string>
    {
        {"oauth_consumer_key", consumerKey},
        {"oauth_token", TokenId },
        {"oauth_signature_method", signatureMethod},
        {"oauth_timestamp", timestamp},
        {"oauth_nonce", nounce},
        {"oauth_version", "1.0"}
    };

    //6.append parameter string to the output string.
    foreach (KeyValuePair<string, string> elt in paramStrings)
    {
        parameters.Add(elt.Key, elt.Value);
    }

    bool first = true;
    foreach (KeyValuePair<string, string> elt in parameters)
    {
        if (first)
        {
            Signature_Base_String = Signature_Base_String + Uri.EscapeDataString(elt.Key + "=" + elt.Value);
            first = false;
        }
        else
        {
            Signature_Base_String = Signature_Base_String + Uri.EscapeDataString("&" + elt.Key + "=" + elt.Value);
        }
    }

    return Signature_Base_String;
}

HMAC-SHA256加密

public string encryptSignatureBase(string signatureBase, string consumerSecret, string tokenSecret)
{
    HMACSHA256 hmacsha1 = new HMACSHA256();
    hmacsha1.Key = Encoding.ASCII.GetBytes(string.Format("{0}&{1}", UrlEncode(consumerSecret), UrlEncode(tokenSecret)));

    byte[] dataBuffer = Encoding.ASCII.GetBytes(signatureBase);
    byte[] hashBytes = hmacsha1.ComputeHash(dataBuffer);

    return Convert.ToBase64String(hashBytes);
}

string unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

protected string UrlEncode(string value)
{
    StringBuilder result = new StringBuilder();

    foreach (char symbol in value)
    {
        if (unreservedChars.IndexOf(symbol) != -1)
            result.Append(symbol);
        else
            result.Append('%' + String.Format("{0:X2}", (int)symbol));
    }
    return result.ToString();
}

问题排查与修复方案

1. Base URL与请求URL域名不一致

原代码中baseUrl是test-sb1.restlets.api.netsuite.com,但请求的apiUrl是test-test.api.netsuite.com,域名不匹配会直接导致签名基串验证失败。必须保证两者域名完全一致:

string baseUrl = "https://test-test.api.netsuite.com/app/site/hosting/restlet.nl";

2. Nonce生成不符合要求

OAuth 1.0要求nonce是随机不可预测的字符串,原代码用时间戳转Base64生成的nonce可预测,不符合规范。替换为随机字符串生成:

var random = new Random();
var nonce = new string(Enumerable.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 12)
    .Select(s => s[random.Next(s.Length)]).ToArray());

3. 签名基串参数编码逻辑错误

原代码对整个键值对(包括&)统一编码,正确做法是先分别编码键和值,再用=连接,最后用&分隔所有键值对,再整体编码。修改GetSignatureBaseString方法:

string GetSignatureBaseString(string timestamp, string nonce)
{
    var signatureBase = "GET&";
    var encodedBaseUrl = Uri.EscapeDataString(baseUrl);
    signatureBase += encodedBaseUrl + "&";

    var parameters = new SortedDictionary<string, string>
    {
        {"oauth_consumer_key", consumerKey},
        {"oauth_token", TokenId},
        {"oauth_signature_method", signatureMethod},
        {"oauth_timestamp", timestamp},
        {"oauth_nonce", nonce},
        {"oauth_version", "1.0"},
        {"script", "941"},
        {"deploy", "1"},
        {"FC", "SGD"},
        {"TC", "USD"},
        {"ED", "02/11/2022"}
    };

    var paramParts = new List<string>();
    foreach (var kvp in parameters)
    {
        var encodedKey = Uri.EscapeDataString(kvp.Key);
        var encodedValue = Uri.EscapeDataString(kvp.Value);
        paramParts.Add($"{encodedKey}={encodedValue}");
    }
    var encodedParams = Uri.EscapeDataString(string.Join("&", paramParts));
    signatureBase += encodedParams;

    return signatureBase;
}

4. Authorization头格式错误

  • realm后多了空格,需改为realm="xxx"格式
  • oauth_signature需要URL编码(Base64结果中的+、/等字符需转义)

修改Authorization头生成代码:

var oauthStr = $"OAuth realm=\"{realm}\",oauth_consumer_key=\"{consumerKey}\",oauth_token=\"{TokenId}\",oauth_signature_method=\"{signatureMethod}\",oauth_timestamp=\"{timeStamp}\",oauth_nonce=\"{nonce}\",oauth_version=\"1.0\",oauth_signature=\"{Uri.EscapeDataString(oauthSignature)}\"";

5. 编码方法不一致

自定义的UrlEncode和Uri.EscapeDataString规则有差异,OAuth 1.0要求遵循RFC 3986标准,直接用Uri.EscapeDataString替代自定义方法:

public string encryptSignatureBase(string signatureBase, string consumerSecret, string tokenSecret)
{
    var encodedConsumerSecret = Uri.EscapeDataString(consumerSecret);
    var encodedTokenSecret = Uri.EscapeDataString(tokenSecret);
    var key = $"{encodedConsumerSecret}&{encodedTokenSecret}";
    
    using (var hmacsha256 = new HMACSHA256(Encoding.ASCII.GetBytes(key)))
    {
        var dataBuffer = Encoding.ASCII.GetBytes(signatureBase);
        var hashBytes = hmacsha256.ComputeHash(dataBuffer);
        return Convert.ToBase64String(hashBytes);
    }
}

验证步骤

  1. 运行修复后的代码,对比生成的signatureBase与Postman的签名基串,确认完全一致
  2. 检查Authorization头格式,确保无多余空格且oauth_signature已正确编码
  3. 确认baseUrl与请求URL域名完全匹配

内容的提问来源于stack exchange,提问作者anand

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 14:25:18