使用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); } }
验证步骤
- 运行修复后的代码,对比生成的
signatureBase与Postman的签名基串,确认完全一致 - 检查Authorization头格式,确保无多余空格且
oauth_signature已正确编码 - 确认
baseUrl与请求URL域名完全匹配
内容的提问来源于stack exchange,提问作者anand
相关产品推荐
相关产品推荐

