[PHP] NetSuite SuiteScript 中請求 RESTlet API 使用的 OAuth 1.0a 方法

本篇文章更新時間:2024/09/04
如有資訊過時或語誤之處,歡迎使用 Contact 功能通知。
一介資男的 LINE 社群開站囉!歡迎入群聊聊~
如果本站內容對你有幫助,歡迎使用 BFX Pay 加密貨幣新台幣 贊助支持。


因工作需求,需要寫到 Oracle ERP NetSuite 的程式。這套的強大,從他也支援的開發串接方式就可以知道。

不過在用 PHP 實作串接 OAuth 1.0a 以及驗證方法更新到 SHA256 的開源工具...

沒有啊~ 於是就是找到一套彈性的其他語言(NodeJS)寫的工具來改寫成 PHP 版本。

class OAuth {
    private $consumer;
    private $nonce_length;
    private $version;
    private $parameter_seperator;
    private $realm;
    private $last_ampersand;
    private $signature_method;
    private $hash_function;
    private $body_hash_function;

    public function __construct($opts) {
        if (!isset($opts['consumer'])) {
            throw new Exception('consumer option is required');
        }

        $this->consumer = $opts['consumer'];
        $this->nonce_length = isset($opts['nonce_length']) ? $opts['nonce_length'] : 32;
        $this->version = isset($opts['version']) ? $opts['version'] : '1.0';
        $this->parameter_seperator = isset($opts['parameter_seperator']) ? $opts['parameter_seperator'] : ', ';
        $this->realm = isset($opts['realm']) ? $opts['realm'] : null;
        $this->last_ampersand = isset($opts['last_ampersand']) ? $opts['last_ampersand'] : true;
        $this->signature_method = isset($opts['signature_method']) ? $opts['signature_method'] : 'PLAINTEXT';

        if ($this->signature_method == 'PLAINTEXT' && !isset($opts['hash_function'])) {
            $opts['hash_function'] = function ($base_string, $key) {
                return $key;
            };
        }

        if (!isset($opts['hash_function'])) {
            throw new Exception('hash_function option is required');
        }

        $this->hash_function = $opts['hash_function'];
        $this->body_hash_function = isset($opts['body_hash_function']) ? $opts['body_hash_function'] : $this->hash_function;
    }

    public function authorize($request, $token = null) {
        $oauth_data = array(
            'oauth_consumer_key' => $this->consumer['key'],
            'oauth_nonce' => $this->getNonce(),
            'oauth_signature_method' => $this->signature_method,
            'oauth_timestamp' => $this->getTimeStamp(),
            'oauth_version' => $this->version,
        );

        if ($token !== null && isset($token['key'])) {
            $oauth_data['oauth_token'] = $token['key'];
        }

        if (!isset($request['data'])) {
            $request['data'] = array();
        }

        if (isset($request['includeBodyHash']) && $request['includeBodyHash']) {
            $oauth_data['oauth_body_hash'] = $this->getBodyHash($request, isset($token['secret']) ? $token['secret'] : '');
        }

        $oauth_data['oauth_signature'] = $this->getSignature($request, isset($token['secret']) ? $token['secret'] : '', $oauth_data);

        return $oauth_data;
    }

    private function getSignature($request, $token_secret, $oauth_data) {
        return call_user_func($this->hash_function, $this->getBaseString($request, $oauth_data), $this->getSigningKey($token_secret));
    }

    private function getBodyHash($request, $token_secret) {
        $body = is_string($request['data']) ? $request['data'] : json_encode($request['data']);

        if (!$this->body_hash_function) {
            throw new Exception('body_hash_function option is required');
        }

        return call_user_func($this->body_hash_function, $body, $this->getSigningKey($token_secret));
    }

    private function getBaseString($request, $oauth_data) {
        return strtoupper($request['method']) . '&' . $this->percentEncode($this->getBaseUrl($request['url'])) . '&' . $this->percentEncode($this->getParameterString($request, $oauth_data));
    }

    private function getParameterString($request, $oauth_data) {
        if (isset($oauth_data['oauth_body_hash'])) {
            $base_string_data = $this->sortObject($this->percentEncodeData($this->mergeObject($oauth_data, $this->deParamUrl($request['url']))));
        } else {
            $base_string_data = $this->sortObject($this->percentEncodeData($this->mergeObject($oauth_data, $this->mergeObject($request['data'], $this->deParamUrl($request['url'])))));
        }

        $data_str = '';
        foreach ($base_string_data as $item) {
            $key = $item['key'];
            $value = $item['value'];

            if (is_array($value)) {
                sort($value);
                $valString = '';
                foreach ($value as $i => $val) {
                    $valString .= $key . '=' . $val;
                    if ($i < count($value) - 1) {
                        $valString .= '&';
                    }
                }
                $data_str .= $valString;
            } else {
                $data_str .= $key . '=' . $value . '&';
            }
        }
        return substr($data_str, 0, -1);
    }

    private function getSigningKey($token_secret) {
        $token_secret = $token_secret ?: '';

        if (!$this->last_ampersand && !$token_secret) {
            return $this->percentEncode($this->consumer['secret']);
        }

        return $this->percentEncode($this->consumer['secret']) . '&' . $this->percentEncode($token_secret);
    }

    private function getBaseUrl($url) {
        return explode('?', $url)[0];
    }

    private function deParam($string) {
        $arr = explode('&', $string);
        $data = array();

        foreach ($arr as $item) {
            $parts = explode('=', $item);
            $key = $parts[0];
            $value = isset($parts[1]) ? $parts[1] : '';

            if (isset($data[$key])) {
                if (!is_array($data[$key])) {
                    $data[$key] = array($data[$key]);
                }
                $data[$key][] = urldecode($value);
            } else {
                $data[$key] = urldecode($value);
            }
        }

        return $data;
    }

    private function deParamUrl($url) {
        $tmp = explode('?', $url);

        if (count($tmp) === 1) {
            return array();
        }

        return $this->deParam($tmp[1]);
    }

    private function percentEncode($str) {
        return rawurlencode($str);
    }

    private function percentEncodeData($data) {
        $result = array();

        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $newValue = array();
                foreach ($value as $val) {
                    $newValue[] = $this->percentEncode($val);
                }
                $value = $newValue;
            } else {
                $value = $this->percentEncode($value);
            }
            $result[$this->percentEncode($key)] = $value;
        }

        return $result;
    }

    public function toHeader($oauth_data) {
        $sorted = $this->sortObject($oauth_data);

        $header_value = 'OAuth ';

        if ($this->realm) {
            $header_value .= 'realm="' . $this->realm . '"' . $this->parameter_seperator;
        }

        foreach ($sorted as $item) {
            if (strpos($item['key'], 'oauth_') !== 0) {
                continue;
            }

            $header_value .= $this->percentEncode($item['key']) . '="' . $this->percentEncode($item['value']) . '"' . $this->parameter_seperator;
        }

        return array('Authorization' => substr($header_value, 0, -strlen($this->parameter_seperator)));
    }

    private function getNonce() {
        $word_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $result = '';

        for ($i = 0; $i < $this->nonce_length; $i++) {
            $result .= $word_characters[rand(0, strlen($word_characters) - 1)];
        }

        return $result;
    }

    private function getTimeStamp() {
        return time();
    }

    private function mergeObject($obj1, $obj2) {
        return array_merge($obj1, $obj2);
    }

    private function sortObject($data) {
        $keys = array_keys($data);
        sort($keys);
        // ksort($keys);
        $result = array();

        foreach ($keys as $key) {
            $result[] = array(
                'key' => $key,
                'value' => $data[$key],
            );
        }

        return $result;
    }
}

Gist: Link

主要就是以 ddo/oauth-1.0a 這套來移植。(感謝 ddo 大大)

在 NetSuite 的案例中測試的寫法如下:

$base_url = 'https://USERID.restlets.api.netsuite.com/app/site/hosting/restlet.nl';
$consumer_key = 'consumer_key_consumer_key_consumer_key';
$consumer_secret = 'consumer_secret_consumer_secret_consumer_secret';
$token = 'token_token_token_token_token_token_token_token_token_';
$token_secret = 'token_secret_token_secret_token_secret_token_secret_';
$realm = 'USERID';
$script = 256;
$deploy=1;
$oauth = new OAuth(array(
  'consumer' => array(
    'key' => $consumer_key,
    'secret' => $consumer_secret,
  ),
  'realm' => $realm,
  'signature_method' => 'HMAC-SHA256',
  'hash_function' => function ($base_string, $key) {
    return base64_encode(hash_hmac('sha256', $base_string, $key, true));
  },
));

$request = array(
  'method' => 'GET',
  'url' => $base_url . '?script=' . $script . '&deploy='.$deploy,
  // 'data' => array(
  //     'status' => 'Hello World!'
  // )
);

$token = array(
  'key' => $token,
  'secret' => $token_secret,
);

$oauth_data = $oauth->authorize($request, $token);
$header = $oauth->toHeader($oauth_data);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $request['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
  'Authorization: ' . $header['Authorization'],
  'Content-Type: application/json',
]);
$response = curl_exec($ch);
curl_close($ch);
$item = json_decode($response, true);

上面是 GET 的範例。

把 App 與 User 的 Key 還有 Script 的相關資訊帶入後,就可以完成請求。

POST 的部分其實差異就是把註解的 data 欄位打開,然後 curl 方法記得傳送 POST 的資料就可以。設定如下:

$request = array(
  'method' => 'POST',
  'url' => $base_url . '?script=' . $script . '&deploy='.$deploy,
  'includeBodyHash' => true,
  'data' => array(
    'query' => $sql,
    'params' => $params,
  ),
);

Share:

作者: Chun

資訊愛好人士。主張「人人都該為了偷懶而進步」。期許自己成為斜槓到變進度條 100% 的年輕人。[///////////____36%_________]

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


文章
Filter
Apply Filters
Mastodon