本周闭关写代码,用Java通过TBA方式访问NetSuite REST Webservices。由于是手生,卡在InvalidSignature报错上,在这个问题上被卡了一整天。
直到终于到来的Aha时刻。
在NetSuite中的样例代码是PHP的, 我平移到Java后,代码逻辑丝毫未变。反复核对代码后,发现代码没有任何的问题。按照NetSuite系统的指引,签名流程如下:
1. 构建Base String。这时,请注意字符串的格式,有三种格式。我采用的是REST Webservices,所以按照相应指引进行了构建。
NetSuite Applications Suite - The Signature for Web Services and RESTletshttps://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534941088.html#The-Signature-for-Web-Services-and-RESTlets
2. 构建Secret。
3. 用Secret对Base String进行加密,形成Signature。加密方法为HMAC-SHA256。
4. 把Signature结合其他字串,形成Authorization字串,赋给Header。
反复检查上述逻辑,但是一直报InvalidSignature错。相同的参数,在Postman中是毫无问题的。我们在对比了Header中的Authorization字符串后,发现只有最后的oauth_signature的值是有差别的。
Signature的差别,只会有两种可能性,一是加密方法出错,二是Base String出错。在确定了HmacSHA256没有问题后,我们把问题聚焦在了Base String。
最后,虫子找到了!
原来在OAuth1.0的规范中,host必须是小写的。例如,123456-SB1必须格式化为123456-sb1。
但是,在构建的Header中,Host是要大写的。这就是大坑所在。
以下代码调试通过,可参考。
//访问NetSuite REST Webservices,请注意Base String中Host的小写格式。
//Rick Mao 2023-6-26import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.net.URLEncoder;
import java.util.*;public class MainAppOAuth1okhttp3 {private static final String NETSUITE_ACCOUNT = "你的账户"; //对字母大写private static final String NETSUITE_CONSUMER_KEY = "你的consumer key";private static final String NETSUITE_CONSUMER_SECRET = "你的consumer secret";private static final String NETSUITE_TOKEN_ID = "你的token id";private static final String NETSUITE_TOKEN_SECRET = "你的token secret";// Generate the timestamp and nonceprivate static final String timestamp = Long.toString(System.currentTimeMillis() / 1000L);private static final String nonce = UUID.randomUUID().toString();public static void main(String[] args) throws Exception {// Create OkHttpClient with logging interceptor for debuggingHttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(loggingInterceptor).build();// Create the request URLHttpUrl requestUrl = HttpUrl.parse("https://" + NETSUITE_ACCOUNT + ".suitetalk.api.netsuite.com/services/rest/record/v1/customer");// Generate the Authorization header valueString authorizationHeader = generateAuthorizationHeader(requestUrl.toString());// Create the requestRequest request = new Request.Builder().url(requestUrl).header("Authorization", authorizationHeader).get().build();// Execute the requesttry (Response response = httpClient.newCall(request).execute()) {// Process the responseString responseBody = response.body().string();System.out.println(responseBody);}}private static String generateAuthorizationHeader(String url) throws Exception {// Generate the base string,这是Rest Webservice格式的,SOAP和Restlet则不同String baseString = baseStringConcat();// Generate the signatureString signature = generateSignature(baseString, NETSUITE_CONSUMER_SECRET, NETSUITE_TOKEN_SECRET);// Construct the Authorization header valueString AuthString = "OAuth " +"realm=\"" + NETSUITE_ACCOUNT + "\"," +"oauth_consumer_key=\"" + NETSUITE_CONSUMER_KEY + "\"," +"oauth_token=\"" + NETSUITE_TOKEN_ID + "\"," +"oauth_signature_method=\"HMAC-SHA256\"," +"oauth_timestamp=\"" + timestamp + "\"," +"oauth_nonce=\"" + nonce + "\"," +"oauth_version=\"1.0\"," +"oauth_signature=\"" + URLEncoder.encode(signature, StandardCharsets.UTF_8) + "\"";return AuthString;}private static String generateSignature(String baseString, String consumerSecret, String tokenSecret) throws NoSuchAlgorithmException, InvalidKeyException {String EMPTY_STRING = "";String CARRIAGE_RETURN = "\r\n";String key = URLEncoder.encode(consumerSecret, StandardCharsets.UTF_8) + "&" + URLEncoder.encode(tokenSecret, StandardCharsets.UTF_8);SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");Mac sha256Hmac = Mac.getInstance("HmacSHA256");sha256Hmac.init(secretKey);byte[] signatureBytes = sha256Hmac.doFinal(baseString.getBytes(StandardCharsets.UTF_8));String resultSignature = new String(java.util.Base64.getEncoder().encode(signatureBytes));return resultSignature.replace(CARRIAGE_RETURN, EMPTY_STRING);}public static String generateSignatureBaseString(String httpMethod, String url, Map<String, String> parameters) throws Exception {StringBuilder baseString = new StringBuilder();// URL-encode the components of the URLString encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);// Sort and encode the parametersMap<String, String> sortedParameters = new HashMap<>(parameters);sortedParameters.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEachOrdered(entry -> {try {String key = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8);String value = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8);baseString.append(key).append("=").append(value).append("&");} catch (Exception e) {e.printStackTrace();}});// Remove the trailing '&' characterif (baseString.length() > 0) {baseString.setLength(baseString.length() - 1);}// Construct the signature base stringString signatureBaseString = httpMethod.toUpperCase() + "&" + encodedUrl + "&" + URLEncoder.encode(baseString.toString(), "UTF-8");return signatureBaseString;}private static String baseStringConcat() throws Exception {String httpMethod = "GET";//NETSUITE_ACCOUNT 需要转为小写,否则服务端报InvalidSignature错。String url = "https://"+ NETSUITE_ACCOUNT.toLowerCase() + ".suitetalk.api.netsuite.com/services/rest/record/v1/customer";Map<String, String> parameters = new HashMap<>();parameters.put("oauth_consumer_key", NETSUITE_CONSUMER_KEY);parameters.put("oauth_nonce", nonce);parameters.put("oauth_signature_method", "HMAC-SHA256");parameters.put("oauth_timestamp", timestamp);parameters.put("oauth_token", NETSUITE_TOKEN_ID);parameters.put("oauth_version", "1.0");String signatureBaseString = generateSignatureBaseString(httpMethod, url, parameters);System.out.println(signatureBaseString);return signatureBaseString;}
}
如果有任何关于NetSuite的问题,欢迎来谈。我的邮箱:rick.mao@truston.group