token = $token; $this->encodingAesKey = $encodingAesKey; } /** * 验签 + 解密消息体 * * @param string $signature 从请求 query 参数 signature * @param string $timestamp 从请求参数 timestamp * @param string $nonce 从请求参数 nonce * @param string $encrypt 消息体中的 encrypt 字段 * @return array|false 解密后的数组,失败返回 false */ public function decryptMsg(string $signature, string $timestamp, string $nonce, string $encrypt) { // 1. 验签 $tmpArr = [$this->token, $timestamp, $nonce, $encrypt]; sort($tmpArr, SORT_STRING); $tmpStr = implode($tmpArr); $hash = sha1($tmpStr); if ($hash !== $signature) { return false; } // 2. 解密 $aesKey = base64_decode($this->encodingAesKey . '='); // 注意要加一个 '=' 补足 padding $iv = substr($aesKey, 0, 16); // 解密,使用 AES-256-CBC $cipherText = base64_decode($encrypt); $decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv); if ($decrypted === false) { return false; } // 去掉 padding, PKCS#7 $decrypted = $this->pkcs7Unpad($decrypted); // 解出: 16 位随机字符串 + 4 字节内容长度 network-order + 内容 + corpId(如果有的话) // 跳过前 16 位 $content = substr($decrypted, 16); // 读长度 $lenList = unpack("N", substr($content, 0, 4)); $xmlLen = $lenList[1]; $xmlContent = substr($content, 4, $xmlLen); // 如果有 corpId 在 xmlContent 后还有 // $fromCorpId = substr($content, 4 + $xmlLen); $json = $xmlContent; // 钉钉回调 body 是 JSON 字符串 // 解析 $arr = json_decode($json, true); return $arr; } /** * 加密回复消息,比如返回 “success” * * @param string $replyMsg 明文消息,比如 "success" * @param string $timestamp * @param string $nonce * @return array 包含加密后json结构 */ public function encryptMsg(string $replyMsg, string $timestamp, string $nonce) { // 1. 构造消息内容 $random16 = $this->getRandomStr(16); $replyMsgXml = $replyMsg; // 钉钉回调用 JSON 格式内容 $msgLen = strlen($replyMsgXml); $msgLenBin = pack("N", $msgLen); $corpId = ''; // 如果不需要 corpId,可以空 $plain = $random16 . $msgLenBin . $replyMsgXml . $corpId; // 2. PKCS#7 padding $blockSize = 32; $pad = $blockSize - (strlen($plain) % $blockSize); if ($pad == 0) { $pad = $blockSize; } $plainPadded = $plain . str_repeat(chr($pad), $pad); // 3. AES 加密 $aesKey = base64_decode($this->encodingAesKey . '='); $iv = substr($aesKey, 0, 16); $encrypted = openssl_encrypt($plainPadded, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv); $encryptBase64 = base64_encode($encrypted); // 4. 签名 $signatureArr = [$this->token, $timestamp, $nonce, $encryptBase64]; sort($signatureArr, SORT_STRING); $signature = sha1(implode($signatureArr)); // 5. 返回结构 return [ 'msg_signature' => $signature, 'encrypt' => $encryptBase64 ]; } private function getRandomStr($length = 16) { $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; $str = ''; for ($i = 0; $i < $length; $i++) { $str .= $chars[ord(bin2hex(random_bytes(1))) % strlen($chars)]; } return $str; } private function pkcs7Unpad($text) { // 去掉结尾的 padding $pad = ord(substr($text, -1)); if ($pad < 1 || $pad > 32) { // padding 有问题 return false; } return substr($text, 0, -$pad); } }