最近需要用到钉钉的自定义机器人做个消息推送,钉钉默认的鉴权方式三种,分别是关键词,签名和IP白名单;关键词没有做多考虑放弃了,IP白名单比较简单,但考虑到测试、生产环境换着来换IP比较麻烦,所以还是觉得签名或许好一点。
钉钉文档:https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq,根据文档中的描述,刚开始觉得,emmmm... 好像很简单,结果一搞就出问题。
踩坑一,参数timestamp
文档描述:当前时间戳,单位是毫秒,与请求调用时间误差不能超过1小时,所以在用PHP写的时候,单纯使用time
和microtime
函数都是不行的,正确的做法是: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)));
效果图:
完整源码:
<?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}×tamp={$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;
}