MENU

[PHP] 钉钉自定义机器人签名踩坑记录

August 31, 2020 • Read: 2644 • 技术杂谈

最近需要用到钉钉的自定义机器人做个消息推送,钉钉默认的鉴权方式三种,分别是关键词,签名和 IP 白名单;关键词没有做多考虑放弃了,IP 白名单比较简单,但考虑到测试、生产环境换着来换 IP 比较麻烦,所以还是觉得签名或许好一点。

钉钉文档:https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq,根据文档中的描述,刚开始觉得,emmmm... 好像很简单,结果一搞就出问题。

踩坑一,参数 timestamp

文档描述:当前时间戳,单位是毫秒,与请求调用时间误差不能超过 1 小时,所以在用 PHP 写的时候,单纯使用 timemicrotime 函数都是不行的,正确的做法是:intval(microtime(true) * 1000),否则请求的时候就返回一个无效时间戳描述...

踩坑二,签名函数 hash_hmac

函数文档:https://www.php.net/manual/zh/function.hash-hmac.php
前三个参数分别是哈希算法,计算数据和密钥,均不能为空,好像没什么问题,第一次加密的时候也的的确确返回了数据,但请求之后接口一直报签名错误...
折腾半小时重新看了 hash_hmac 函数文档,问题出在第四个参数 raw_output,“设置为 TRUE 输出原始二进制数据, 设置为 FALSE 输出小写 16 进制字符串”,然而默认是 false,所以计算之后返回的字符串再经过 base64 编码,肯定就错了;
所以正确的签名做法是:urlencode(base64_encode(hash_hmac('sha256', "{$time}\n{$secret}", $secret, true)));

效果图:

PC效果.png

手机效果.png

完整源码:

  • <?php
  • $secret = 'SEC065d711a3b14615ae20bbf2ccea4eb86a8e3d6c2b221eeba440d8f3f9b42bf6y'; // 替换自己serert
  • $token = 'e6111ebe513727ac288e9bbb6b7d212c2ab34b0666eca9c5d0af88f34d7b2cf16'; // 替换自己access token
  • $time = intval(microtime(true) * 1000);
  • $sign = urlencode(base64_encode(hash_hmac('sha256', "{$time}\n{$secret}", $secret, true)));
  • $api = "https://oapi.dingtalk.com/robot/send?access_token={$token}&timestamp={$time}&sign={$sign}";
  • $headers = ['Content-Type: application/json;charset=utf-8'];
  • $text = <<<TEXT
  • ## 莫名博客
  • > 嘿嘿哈哈
  • 这里是莫名的博客哦!\n
  • ![](https://qzone.work/usr/uploads/images/qzone_work/2020/08/974760054.jpg)
  • TEXT;
  • $post = [
  • 'msgtype' => 'actionCard',
  • 'actionCard' => [
  • 'title' => date('Y-m-d H:i:s'),
  • 'text' => $text,
  • 'btnOrientation' => 1,
  • 'btns' => [
  • [
  • 'title' => 'Yes',
  • 'actionURL' => 'https://qzone.work',
  • ],
  • [
  • 'title' => 'No',
  • 'actionURL' => 'https://www.baidu.com',
  • ]
  • ],
  • ],
  • ];
  • $res = json_decode(getCurl($api, [
  • 'post' => json_encode($post),
  • 'headers' => $headers,
  • ]), true);
  • print_r($res);
  • function getCurl($url, $opt = [])
  • {
  • $cookie = '';
  • if (is_array($opt['cookie'])) {
  • foreach ($opt['cookie'] as $k => $v) {
  • $cookie .= $k . '=' . $v . '; ';
  • }
  • }
  • $cookie = (mb_substr($cookie, 0, mb_strlen($cookie) - 2));
  • $ch = curl_init();
  • curl_setopt($ch, CURLOPT_URL, $url);
  • curl_setopt($ch, CURLOPT_COOKIE, $cookie);
  • curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  • curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  • curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  • curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  • curl_setopt($ch, CURLOPT_NOBODY, $opt['nobody']);
  • curl_setopt($ch, CURLOPT_HEADER, $opt['header'] ?? false);
  • curl_setopt($ch, CURLOPT_HTTPHEADER, $opt['headers'] ?? []);
  • curl_setopt($ch, CURLOPT_TIMEOUT_MS, $opt['rtime'] ?? 10000);
  • curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $opt['ctime'] ?? 10000);
  • curl_setopt($ch, CURLOPT_REFERER, $opt['refer'] ?? 'https://user.qzone.qq.com/');
  • curl_setopt($ch, CURLOPT_USERAGENT,
  • $opt['UA'] ?? "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36");
  • if (isset($opt['post'])) {
  • curl_setopt($ch, CURLOPT_POST, 1);
  • curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($opt['post']) ? http_build_query($opt['post']) : $opt['post']);
  • }
  • if (isset($opt['proxy']) && is_array($opt['proxy'])) {
  • curl_setopt($ch, CURLOPT_PROXY, $opt['proxy']['ip']);
  • curl_setopt($ch, CURLOPT_PROXYPORT, $opt['proxy']['port']);
  • }
  • $res = curl_exec($ch);
  • $error = curl_error($ch);
  • $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  • curl_close($ch);
  • if ($opt['detail']) {
  • return ['code' => $code, 'error' => $error, 'response' => $res,];
  • }
  • return $res;
  • }