用 Delphi 发送钉钉机器人消息时遇到的加签问题
钉钉的机器人群消息很好用,也不用申请特别的权限,免费的消息条数也很多,做个监控类的消息通知很合适。一般都是用 python、JAVA 等语言来编写调用代码,简单使用也可以直接命令行调 curl。
这几天想给一个用 delphi 编写的运维工具加上钉钉消息功能。尝试编写了一下,用自定义关键字方式发送消息很简单,一次就通过了;然而加签方式却死活通不过,总是返回加签错误。仔细阅读了N遍官方文档,就是个很常用的 HMAC-SHA256 + Base64 加签算法。蹊跷的是,我换了三四种不同的代码去实现,每一种算法得到的结果和网上的在线计算器的结果都一模一样,然而就是和官方的 Python 语言例程的结果不一样。
晚饭后出门散步时我继续思考这个问题:既然我的代码和在线计算器的一致,说明算法本身没有错误,那么只能是输入参数不一致;这时我突然领悟到,Python 和 JAVA 语言都会自动处理 \n 这样的转义字符串而 Delphi 不会,官方文档里要求在加签字符串中添加了一个 \n,肯定就是这里导致的错误。后来在代码中用 #10 来代替 \n,果然验签通过。
那么就分享一段完整的用 delphi 发送钉钉机器人消息的代码吧:
unit main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
System.JSON, Vcl.StdCtrls, Hash, System.Net.URLClient, DateUtils,
System.Net.HttpClient, System.Net.HttpClientComponent, NetEncoding;
type
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
NetHTTPClient1: TNetHTTPClient;
function dingtalk(content: string): boolean;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
dingtalk('test,测试。[关键字]');
end;
function TForm1.dingtalk(content: string): boolean;
//钉钉机器人消息发送函数
//URL 格式:https://oapi.dingtalk.com/robot/send?access_token=XXXXXX×tamp=XXX&sign=XXX
var
url, keywords, token, secret, timestamp, sign, str: string;
s: TStringStream;
json: TJSONObject;
begin
//result := false;
begin
NetHTTPClient1.ContentType := 'application/json';
NetHTTPClient1.AcceptCharSet := 'utf-8';
NetHTTPClient1.AcceptLanguage := 'zh-CN';
token := 'xxxxxxxx';
keywords := '';
secret := 'SECxxxxxxxx';
url := 'https://oapi.dingtalk.com/robot/send?access_token=' + token;
(*
发送消息有两种安全策略,自定义关键字和加签,至少选择一种,也可同时使用。
secret 为加签密钥,不为空时表示需要加签。
timestamp 为毫秒级时间戳。
加签算法是常用的 HmacSHA256 + Base64:
把 timestamp+"\n"+secret 当做签名字符串,使用HmacSHA256算法计算签名,
然后进行 Base64 编码,将得到的结果再进行 urlEncode 编码,从而得到最终签名数据 sign。
*)
if secret <> '' then
begin
//获取毫秒级时间戳
timestamp := (MilliSecondsBetween(Now, EncodeDateTime(1970, 1, 1, 0, 0, 0, 0)) - 8 * 60 * 60 * 1000).ToString;
//加签
sign := TNetEncoding.Base64.EncodeBytesToString(THashSHA2.GetHMACAsBytes(timestamp + #10 + secret, secret, SHA256));
//URLEncode 编码
sign := TNetEncoding.url.Encode(sign);
//拼接 url
url := url + '×tamp=' + timestamp + '&sign=' + sign;
end;
//keywords 为自定义关键字,若为空时则必须加签,即 secret 不能为空。
if keywords <> '' then
keywords := keywords + '\n';
//拼接 json 格式的消息内容字符串,如有自定义关键字,可插入到内容的任意位置。
s := TStringStream.Create('{"msgtype":"text","text": {"content":"' + keywords + content + '"}}', TEncoding.UTF8);
//发送 post 请求
try
str := NetHTTPClient1.Post(url, s).ContentAsString(nil);
except
on E: Exception do
memo1.Lines.Add('发送消息时出错:' + e.Message);
end;
//解析返回结果
try
json := TJSONObject.ParseJSONValue(str, true, true) as TJSONObject;
if json.GetValue<integer>('errcode') = 0 then
begin
result := true;
memo1.Lines.Add('消息发送成功。');
end
else
begin
result := false;
memo1.Lines.Add('消息发送失败,错误代码:' + json.GetValue<string>('errcode') + json.GetValue<string>('errmsg'));
end;
s.Free;
json.Free;
except
on E: Exception do
begin
result := false;
memo1.Lines.Add('发送消息时出错:' + e.Message);
end;
end;
end;
end;
end.