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

如何用PHP生成TikTok Partner API调用所需签名及问题排查

如何使用PHP生成TikTok Partner API调用所需的签名?

TikTok API的签名实现非常棘手,除了官方文档(示例采用Golang编写)外,几乎找不到相关资料。我已写出两行PHP加密代码,但对签名步骤5中「若请求头content_type不是multipart/form-data,则在末尾追加body」的描述感到困惑——我要发起的是GET请求,所谓的body指什么?这可能是我的签名始终被API判定为无效的原因。
后续我通过Postman中的API示例成功完成调用,现附上官方提供的可用Javascript、Golang签名代码,以及我的无效PHP代码,请求帮忙排查问题所在。

PHP基础加密代码

$hmac = hash_hmac('sha256', $input, $secretKey, true);
$urlSafeEncoded = urlencode(rtrim(strtr(base64_encode($hmac), '+/', '-_'), '='));

可用Javascript签名代码

// Replace secret with your own  
var secret = "c4bb0b4d6ad7407cb53e6bc038a3074b15fc53b0"  
  
function objKeySort(obj) {  
    var newKey = Object.keys(obj).sort()  
    var newObj = {}  
    for (var i = 0; i < newKey.length; i++) {  
        newObj[newKey[i]] = obj[newKey[i]]  
    }  
    return newObj  
}  
  
function getEnvVar(k) {  
    var v = pm.variables.get(k)  
    if (v != null) {  
        return v  
    }  
    v = pm.environment.get(k)  
    if (v != null) {  
        return v  
    }  
    v = pm.globals.get(k)  
    if (v != null) {  
        return v  
    }  
    return null  
}  
  
var ts = Date.parse(new Date()) / 1000  
pm.variables.set("timestamp", ts)  
  
calSign = function(secret) {  
    var ts = getEnvVar("timestamp")  
    var queryParam = pm.request.url.query.members  
    var param = {}  
    for (var item in queryParam) {  
        if (queryParam[item].key == "timestamp") {  
            v = ts  
        } else {  
            var v = queryParam[item].value  
            if (v == null || v == "{{" + queryParam[item].key + "}}") {  
                v = getEnvVar(queryParam[item].key)  
            }  
        }  
        param[queryParam[item].key] = v  
    }  
  
    delete param["sign"];  
    delete param["access_token"]  
    var sortedObj = objKeySort(param)  
    var signstring = secret + pm.request.url.getPath()  
    for (var key in sortedObj) {  
        signstring = signstring + key + sortedObj[key]  
    }  
    signstring = signstring + secret  
    sign = CryptoJS.HmacSHA256(signstring, secret).toString()  
    return sign  
}  
  
var sign = calSign(secret)  
pm.variables.set("sign", sign)

可用Golang签名代码

import (  
   "crypto/hmac"  
   "crypto/sha256"  
   "encoding/hex"  
   "sort"  
)  
/**  
** path: API path, for example /api/orders  
** queries: Extract all query param EXCEPT 'sign','access_token',query param,not body  
** secret: App secter  
**/  
func generateSHA256(path string, queries map[string]string, secret string) string{  
     
   //Reorder the params based on alphabetical order.  
   keys := make([]string, len(queries))  
   idx := 0  
   for k, _ := range queries{  
      keys[idx] = k  
      idx++  
   }  
   sort.Slice(keys, func(i, j int) bool {  
      return keys[i] < keys[j]  
   })  
     
   //Concat all the param in the format of {key}{value} and append the request path to the beginning  
   input := path  
   for _, key := range keys{  
      input = input + key + queries[key]  
   }  
     
   //Wrap string generated in up with app_secret.  
   input = secret + input + secret  

   //Encode the digest byte stream in hexadecimal and use sha256 to generate sign with salt(secret)  
   h := hmac.New(sha256.New, []byte(secret))  
     
   // error log  
   if _, err := h.Write([]byte(input)); err != nil{  
      // todo: log error  
      return ""  
   }  

   return hex.EncodeToString(h.Sum(nil))  
}

无效PHP签名代码

public function encrypt_sign($vars)
{
  $qstring = '';
  ksort($vars['qvars']);
  if (count($vars['qvars']) > 0) {
    foreach ($vars['qvars'] as $k=>$v) {
      if ($k != 'sign') {
        $qstring .= $k.$v;
      }
    }
  }

  //add body of request here!?!  what body, its a get request!
  //$qstring = $qstring.{request body};

  $qstring = $this->tiktok_app_secret . $qstring . $this->tiktok_app_secret;

  $sign = $this->encryptForUrl($qstring, $this->tiktok_app_secret);
  
  return $sign;
}

public function encryptForUrl($input, $secretKey)
{
    $hmac = hash_hmac('sha256', $input, $secretKey, true);

    $urlSafeEncoded = urlencode(rtrim(strtr(base64_encode($hmac), '+/', '-_'), '='));

    return $urlSafeEncoded;
}

问题排查与修复方案

核心问题点

  1. 遗漏API路径拼接:官方示例中签名字符串开头必须拼接API路径(如/api/orders),你的PHP代码直接从查询参数开始拼接,缺少了这一关键部分。
  2. 未排除access_token参数:官方规则明确要求排除sign和access_token两个参数,你的代码只排除了sign。
  3. 签名输出格式不匹配:官方最终输出的是十六进制编码的HMAC结果,你的代码做了Base64 URL安全编码,格式不符。
  4. GET请求的body疑问:GET请求无请求体,步骤5的规则对GET请求不生效,这部分你的理解正确,不是问题根源。

修复后的PHP签名代码

public function encrypt_sign($apiPath, $vars)
{
    $qstring = '';
    // 过滤掉sign和access_token参数
    $filteredVars = array_filter($vars['qvars'], function($key) {
        return $key !== 'sign' && $key !== 'access_token';
    }, ARRAY_FILTER_USE_KEY);
    
    ksort($filteredVars);
    
    if (count($filteredVars) > 0) {
        foreach ($filteredVars as $k => $v) {
            $qstring .= $k . $v;
        }
    }

    // 按照官方规则拼接签名原始字符串:secret + path + 排序后的参数 + secret
    $signRaw = $this->tiktok_app_secret . $apiPath . $qstring . $this->tiktok_app_secret;

    // 生成十六进制格式的HMAC-SHA256签名
    $sign = hash_hmac('sha256', $signRaw, $this->tiktok_app_secret);
    
    return $sign;
}

使用说明

  • 调用时需传入完整的API路径(如/api/orders),不能只传查询参数。
  • 确保查询参数中的timestamp为当前有效时间戳,且与请求中携带的timestamp完全一致。
  • 生成的签名直接作为sign参数传入请求即可,无需额外URL编码。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 09:24:59