MENU

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

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

最近需要用到钉钉的自定义机器人做个消息推送,钉钉默认的鉴权方式三种,分别是关键词,签名和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://img.qzone.work:26683/qzone_work/usr/uploads/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;
}