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

PHP实现NetSuite OAuth 1.0签名无效致401错误求助

NetSuite OAuth 1.0 PHP代码签名无效问题排查

我参考两篇Stack Overflow文章编写了NetSuite的OAuth 1.0 PHP代码,但调用NetSuite REST API时始终返回401错误,提示token_rejected/Invalid login attempt,但相同配置在Postman中可以正常运行。后续确认错误根源是InvalidSignature,但不清楚代码问题所在。

我的代码如下:

public function getValueFromOAuth(string $headerValue, string $key): ?string{
    if (preg_match('/'.preg_quote($key).'="(.*?)"/i', $headerValue, $matches)){
        return $matches[1];
    }else{
        return null;
    }
}

public function getOAuthHeaderValue(string $key, string $value): string{
    if (strtolower($key)!=='authorization'){
       return $value;
    }
    if (strpos($value,'OAuth') === false){
        return $value;
    }
    $value =  str_replace(""", '"', $value);

    $rebuildOauth1 = [];
    $consumerKey = $this->getValueFromOAuth($value, 'oauth_consumer_key');
    $sigMethod = $this->getValueFromOAuth($value, 'oauth_signature_method');
    $token = $this->getValueFromOAuth($value, 'oauth_token');
    $version = $this->getValueFromOAuth($value, 'oauth_version');
    $realm = $this->getValueFromOAuth($value, 'realm');

    $timestamp = time();
    $nonce = mb_substr(md5(mt_rand()),0 , 11);

    $consumerSecret = $this->getValueFromOAuth($value, 'oauth_consumer_secret');
    $tokenSecret = $this->getValueFromOAuth($value, 'oauth_token_secret');

    $rebuildOauth1 []= 'oauth_consumer_key="'.$consumerKey.'"';
    if ($nonce) {
        $rebuildOauth1 [] = 'oauth_nonce="' . $nonce . '"';
    }
    if (!is_null($sigMethod)) {
        $rebuildOauth1 [] = 'oauth_signature_method="' . $sigMethod . '"';
    }
    if ($timestamp) {
        $rebuildOauth1 [] = 'oauth_timestamp="' . $timestamp . '"';
    }
    if ($token){
        $rebuildOauth1[] = 'oauth_token="'.$token.'"';
    }
    if (!is_null($version)) {
        $rebuildOauth1 [] = 'oauth_version="' . $version . '"';
    }

    $rebuildOauth1Str = implode(',',$rebuildOauth1);

    $baseString = strtoupper($this->getRequestType()).'&'.rawurlencode($this->getRequestUrl()).'&'.rawurlencode($rebuildOauth1Str);
    $sigString = urlencode($consumerSecret).'&'.urlencode($tokenSecret);
    $signature = base64_encode(hash_hmac("sha256", $baseString, $sigString, true));
    if (!is_null($realm)) {
        $rebuildOauth1 [] = 'realm="' . $realm . '"';
        $rebuildOauth1Str = implode(',',$rebuildOauth1);
    }

    $rebuiltOauthHeader = 'OAuth oauth_signature="'.$signature.'",'.
        $rebuildOauth1Str;
    return $rebuiltOauthHeader;
}

错误响应示例:

*   Trying 123.123.12.1:443...
* Connected to 123456-sb1.suitetalk.api.netsuite.com (104.114.79.8) port 443 (#0)
...
> GET /services/rest/record/v1/purchaseOrder/?q=custbody15+IS+TRUE&limit=1 HTTP/2
Host: 123456-sb1.suitetalk.api.netsuite.com
authorization: OAuth oauth_signature="ABC123=",oauth_consumer_key="123",oauth_nonce="518cf0608cd",oauth_signature_method="HMAC-SHA256",oauth_timestamp="1699471397",oauth_token="456",oauth_version="1.0",realm="123456_SB1"

< HTTP/2 401 
< www-authenticate: OAuth realm=&quot;123456_SB1&quot;, error=&quot;token_rejected&quot;, error_description=&quot;Invalid login attempt.&quot;
...
{"type":"https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.2","title":"Unauthorized","status":401,"o:errorDetails":[{"detail":"Invalid login attempt. For more details, see the Login Audit Trail in the NetSuite UI at Setup > Users/Roles > User Management > View Login Audit Trail.","o:errorCode":"INVALID_LOGIN"}]}

代码问题分析及修复方案

1. Realm未包含在签名基础串中

NetSuite要求realm必须作为OAuth参数的一部分参与签名生成,但你的代码是在签名生成完成后才把realm加入header,导致签名不匹配。需要在构建签名基础串前就将realm加入参数列表。

2. OAuth参数未按字典序排序

OAuth 1.0规范要求,构建签名基础串时,所有参数(包括OAuth参数和请求查询参数)必须按字典序排序,你的代码直接按添加顺序拼接,这会导致签名和NetSuite验证时的预期不一致。

3. 请求查询参数未加入签名基础串

如果请求URL包含查询参数(比如示例中的q=custbody15+IS+TRUE&limit=1),这些参数也必须提取出来,和OAuth参数合并后排序,再加入签名基础串。

修正后的核心代码部分:

public function getOAuthHeaderValue(string $key, string $value): string{
    if (strtolower($key)!=='authorization'){
       return $value;
    }
    if (strpos($value,'OAuth') === false){
        return $value;
    }
    $value =  str_replace(&quot;&amp;quot;&quot;, '&quot;', $value);

    // 提取基础参数
    $consumerKey = $this->getValueFromOAuth($value, 'oauth_consumer_key');
    $sigMethod = $this->getValueFromOAuth($value, 'oauth_signature_method');
    $token = $this->getValueFromOAuth($value, 'oauth_token');
    $version = $this->getValueFromOAuth($value, 'oauth_version');
    $realm = $this->getValueFromOAuth($value, 'realm');

    $timestamp = time();
    $nonce = mb_substr(md5(mt_rand()),0 , 11);

    $consumerSecret = $this->getValueFromOAuth($value, 'oauth_consumer_secret');
    $tokenSecret = $this->getValueFromOAuth($value, 'oauth_token_secret');

    // 构建所有OAuth参数(包含realm)
    $oauthParams = [
        'realm' => $realm,
        'oauth_consumer_key' => $consumerKey,
        'oauth_nonce' => $nonce,
        'oauth_signature_method' => $sigMethod,
        'oauth_timestamp' => $timestamp,
        'oauth_token' => $token,
        'oauth_version' => $version
    ];
    // 过滤空值参数
    $oauthParams = array_filter($oauthParams, fn($val) => !is_null($val) && $val !== '');
    
    // 按字典序排序参数
    ksort($oauthParams);

    // 格式化为OAuth参数字符串(key="value")
    $paramStrings = [];
    foreach ($oauthParams as $k => $v) {
        $paramStrings[] = sprintf('%s=&quot;%s&quot;', $k, $v);
    }

    // 处理请求URL:分离基础URL和查询参数
    $urlParts = parse_url($this->getRequestUrl());
    $baseUrl = sprintf('%s://%s%s', $urlParts['scheme'], $urlParts['host'], $urlParts['path']);
    $queryParams = [];
    if (!empty($urlParts['query'])) {
        parse_str($urlParts['query'], $queryParams);
        // 查询参数也需要排序并格式化为同格式
        ksort($queryParams);
        foreach ($queryParams as $k => $v) {
            $paramStrings[] = sprintf('%s=&quot;%s&quot;', $k, $v);
        }
        // 重新排序所有参数(OAuth+查询参数)
        sort($paramStrings);
    }
    $rebuildOauth1Str = implode(',', $paramStrings);

    // 构建签名基础串
    $baseString = strtoupper($this->getRequestType()) . '&' . rawurlencode($baseUrl) . '&' . rawurlencode($rebuildOauth1Str);
    // 构建签名密钥
    $sigString = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret);
    // 生成签名
    $signature = base64_encode(hash_hmac("sha256", $baseString, $sigString, true));

    // 构建最终的Authorization header:把signature放在最前面,其余参数排序后跟上
    array_unshift($paramStrings, sprintf('oauth_signature=&quot;%s&quot;', $signature));
    $rebuiltOauthHeader = 'OAuth ' . implode(',', $paramStrings);
    
    return $rebuiltOauthHeader;
}

额外注意事项:

  • 确保getRequestType()返回的是大写的HTTP方法(如GET、POST)
  • 验证consumerSecret和tokenSecret是否正确提取,没有被转义或截断
  • 时间戳必须是当前的Unix时间戳,确保服务器和本地时间同步(时间差过大也会导致签名无效)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 16:28:11