企业微信应用消息回调

发布于 2023-04-08  121 次阅读


在企业微信应用中设置API接收

需要配置可信ip

URL验证

当点击“保存”提交以上信息时,企业微信会发送一条验证消息到填写的URL,发送方法为GET。企业的接收消息服务器接收到验证请求后,需要作出正确的响应才能通过URL验证。

假设你的api设置的地址为https://xxx.com,那么企业微信会提交如下请求:http://xxx.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFS×tamp=13500001234&nonce=123412323&echostr=ENCRYPT_STR

参数必须说明
msg_signature企业微信加密签名,msg_signature结合了企业填写的token、请求中的timestamp、nonce参数、加密的消息体
timestamp时间戳
nonce随机数
echostr加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、CorpID四个字段,其中msg即为消息内容明文

使用官方加密库:https://github.com/sbzhu/weworkapi_php/tree/master/callback

 include_once "WXBizMsgCrypt.php";

通过$_GET方式接收这四个参数。

 include_once "callback/WXBizMsgCrypt.php";
 $encodingAesKey = "你设置的encodingAesKey";
 $token = "你设置的token";
 $corpId = "企业微信ID";
 
 $sVerifyMsgSig = $_GET["msg_signature"] ;
 $sVerifyTimeStamp = $_GET["timestamp"];
 $sVerifyNonce = $_GET["nonce"];
 $sVerifyEchoStr = $_GET["echostr"];

接收到参数之后我们需要对msg_signature进行效验,然后解析echostr

 $EchoStr = "";
 $wxcpt = new WXBizMsgCrypt($token, $encodingAesKey, $corpId);
 $errCode = $wxcpt->VerifyURL($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sVerifyEchoStr, $sEchoStr);
 if ($errCode == 0) {
  echo $sEchoStr;//原样返回明文消息内容
 } else {
  print("ERR: " . $errCode . "\n\n");
 }

echostr 报错解决,PHP版本问题,加一句:

 error_reporting(0);

接收消息

 $sReqData = file_get_contents("php://input");
 $sMsg = "";
 $wxcpt = new WXBizMsgCrypt($token, $encodingAesKey, $corpId);
 $errCode = $wxcpt->DecryptMsg($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sReqData, $sMsg);
 if ($errCode == 0) {
    try {
        $test = new SimpleXMLElement($sMsg);
        $Content = strval($test->Content);//解析出的明文内容
        $FromUserName = strval($test->FromUserName);//发送消息的人
        $CreateTime = strval($test->CreateTime);
        $MsgId = strval($test->MsgId);
    } catch (Exception $e) {
        $Content = "错误!";
    }
 } else {
    print("ERR: " . $errCode . "\n\n");
 }

然后就可以对接收的消息进行处理了

简单版:


    <?php
      error_reporting(0);
    include_once "callback/callback.php";
    //接收消息服务器配置
    $encodingAesKey = "$encodingAesKey";
    $token = "$token";
    $corpId = "$corpId";
    
    
    $sVerifyMsgSig = $_GET["msg_signature"] ;
    $sVerifyTimeStamp = $_GET["timestamp"];
    $sVerifyNonce = $_GET["nonce"];
    $sVerifyEchoStr = $_GET["echostr"];
    
    //## URL验证
    
    $EchoStr = "";
    $wxcpt = new WXBizMsgCrypt($token, $encodingAesKey, $corpId);
    $errCode = $wxcpt->VerifyURL($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sVerifyEchoStr, $sEchoStr);
    if ($errCode == 0) {
      echo $sEchoStr;
    } else {
      print("ERR: " . $errCode . "\n\n");
    }
    
    //## 接收消息
    
    $sReqData = file_get_contents("php://input");
    $sMsg = "";
    $wxcpt = new WXBizMsgCrypt($token, $encodingAesKey, $corpId);
    $errCode = $wxcpt->DecryptMsg($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sReqData, $sMsg);
    if ($errCode == 0) {
      try {
        $test = new SimpleXMLElement($sMsg);
        $Content = strval($test->Content);//解析出的明文内容
        $FromUserName = strval($test->FromUserName);//发送消息的人
        $CreateTime = strval($test->CreateTime);
        $MsgId = strval($test->MsgId);
      } catch (Exception $e) {
        $Content = "错误!";
      }
    }
    
    // 日志
    $date = date('Y-m-d');
    $logDir = 'log';
    $logFileName = "{$logDir}/log_{$date}.txt";
    
    file_put_contents($logFileName, "“[" . $FromUserName . "]" . $Content . "”\r\n", FILE_APPEND);
    
    ?>

升级版

<?php
// ======================================
// 接收消息服务器配置

// 集中定义配置参数
$config = [
    'encodingAesKey' => 'encodingAesKey', 
    'token' => 'token',                     
    'corpId' => 'corpId',                    
    'corpSecret' => 'corpSecret'           
];

error_reporting(E_ALL);
include_once "callback/WXBizMsgCrypt.php";

$encodingAesKey = $config['encodingAesKey'];
$token = $config['token'];
$corpId = $config['corpId'];

// ======================================

    
// 获取URL参数并进行空值检查
$sVerifyMsgSig = isset($_GET["msg_signature"]) ? $_GET["msg_signature"] : '';
$sVerifyTimeStamp = isset($_GET["timestamp"]) ? $_GET["timestamp"] : '';
$sVerifyNonce = isset($_GET["nonce"]) ? $_GET["nonce"] : '';
$sVerifyEchoStr = isset($_GET["echostr"]) ? $_GET["echostr"] : '';

//## URL验证
if (!empty($sVerifyEchoStr)) {
    $sEchoStr = "";
    $wxcpt = new WXBizMsgCrypt($token, $encodingAesKey, $corpId);
    $errCode = $wxcpt->VerifyURL($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sVerifyEchoStr, $sEchoStr);
    
    if ($errCode == 0) {
        writeLog("URL验证成功");
        echo $sEchoStr;
    } else {
        // 增强错误日志,特别关注-40001错误(签名验证失败)
        $errorCodeDesc = getErrorCodeDescription($errCode);
        writeLog("URL验证失败,错误码: {$errCode} ({$errorCodeDesc}),Timestamp: {$sVerifyTimeStamp}, Nonce: {$sVerifyNonce}, Signature: {$sVerifyMsgSig}");
        print("ERR: " . $errCode . "\n\n");
    }
}

//## 接收消息

$sReqData = file_get_contents("php://input");


$sMsg = "";
$wxcpt = new WXBizMsgCrypt($token, $encodingAesKey, $corpId);
$errCode = $wxcpt->DecryptMsg($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sReqData, $sMsg);
if ($errCode == 0) {
    try {
        $test = new SimpleXMLElement($sMsg);
        $MsgType = strval($test->MsgType); // 消息类型
        $FromUserName = strval($test->FromUserName); // 发送消息的人
        $CreateTime = strval($test->CreateTime);
        $MsgId = strval($test->MsgId);
        $AgentID = strval($test->AgentID); // 应用ID

        // 根据不同类型的消息进行处理
        switch ($MsgType) {
            case 'text':
                $Content = strval($test->Content); // 文本消息内容
                writeLog("[" . $FromUserName . "][text] " . $Content);
                break;
                
            case 'image':
                $PicUrl = strval($test->PicUrl); // 图片链接
                $MediaId = strval($test->MediaId); // 媒体文件ID
                writeLog("[" . $FromUserName . "][image] 收到图片消息,MediaId: " . $MediaId);
                // 调用下载和保存图片的函数
                downloadAndSaveImage($MediaId, $FromUserName, $PicUrl);
                break;
                
            case 'voice':
                $MediaId = strval($test->MediaId); // 媒体文件ID
                $Format = strval($test->Format); // 语音格式
                writeLog("[" . $FromUserName . "][voice] 收到语音消息,MediaId: " . $MediaId);
                // 调用下载和保存语音的函数
                downloadAndSaveVoice($MediaId, $FromUserName);
                break;
                
            case 'video':
                $MediaId = strval($test->MediaId);
                writeLog("[" . $FromUserName . "][video] 收到视频消息,MediaId: " . $MediaId);
                // 调用下载和保存视频的函数
                downloadAndSaveVideo($MediaId, $FromUserName);
                break;
                
            case 'location':
                $Location_X = strval($test->Location_X);
                $Location_Y = strval($test->Location_Y);
                $Scale = strval($test->Scale);
                $Label = strval($test->Label);
                writeLog("[" . $FromUserName . "][location] 收到位置消息,坐标: (" . $Location_X . ", " . $Location_Y . ")");
                break;
                
            default:
                writeLog("[" . $FromUserName . "][unknown] 收到未知类型消息: " . $MsgType);
                break;
        }

    } catch (Exception $e) {
        writeLog("解析消息XML失败: " . $e->getMessage());
    }
} else {
    writeLog("解密消息失败,错误码: {$errCode},Timestamp: {$sVerifyTimeStamp}, Nonce: {$sVerifyNonce}, Signature: {$sVerifyMsgSig}");
}

/**
 * 下载并保存图片
 */
function downloadAndSaveImage($mediaId, $fromUserName, $picUrl) {
    // 获取access_token
    $accessToken = getAccessToken();
    if (!$accessToken) {
        writeLog("[downloadAndSaveImage] 获取access_token失败");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("下载图片失败:获取access_token失败", 'text');
        
        return;
    }

    // 构造下载图片的API地址
    $downloadUrl = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token={$accessToken}&media_id={$mediaId}";

    // 使用curl下载图片
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $downloadUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch); // 获取curl错误信息
    curl_close($ch);

    // 检查是否是有效的图片数据(非错误JSON响应)
    $isJson = json_decode($result) !== null;
    if ($httpCode !== 200 || $isJson) {
        // 尝试直接从PicUrl下载图片
        $result = file_get_contents($picUrl);
        if ($result === false) {
            writeLog("[downloadAndSaveImage] 从PicUrl下载图片失败,MediaId: {$mediaId}");
            
            // 包含错误提示功能
            include_once "send_post.php";
            send_message("图片下载失败:" . $err, 'text');
            
            return;
        }
    }

    // 创建用户专属目录
    $upload_dir = "upload/";
    $user_image_dir = $upload_dir . preg_replace('/[^a-zA-Z0-9\-_]/', '_', $fromUserName) . "/";
    if (!is_dir($user_image_dir)) {
        if (!mkdir($user_image_dir, 0777, true)) {
            writeLog("[downloadAndSaveImage] 创建用户目录失败: {$user_image_dir}");
            return;
        }
    }

    // 生成图片文件名(使用时间戳+随机数+原始媒体ID确保唯一性)
    $fileName = date('YmdHis') . '_' . rand(1000, 9999) . '_' . $mediaId . '.jpg';
    $filePath = $user_image_dir . $fileName;

    // 保存图片
    $res = file_put_contents($filePath, $result);
    if ($res !== false) {
        writeLog("[downloadAndSaveImage] 图片保存成功: {$filePath}");
    } else {
        writeLog("[downloadAndSaveImage] 图片保存失败,MediaId: {$mediaId}");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("图片保存失败", 'text');
    }
}

/**
 * 下载并保存语音
 */
function downloadAndSaveVoice($mediaId, $fromUserName) {
    // 获取access_token
    $accessToken = getAccessToken();
    if (!$accessToken) {
        writeLog("[downloadAndSaveVoice] 获取access_token失败");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("下载语音失败:获取access_token失败", 'text');
        
        return;
    }

    // 构造下载语音的API地址
    $downloadUrl = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token={$accessToken}&media_id={$mediaId}";

    // 使用curl下载语音
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $downloadUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch); // 获取curl错误信息
    curl_close($ch);

    // 检查是否是有效的语音数据(非错误JSON响应)
    $isJson = json_decode($result) !== null;
    if ($httpCode !== 200 || $isJson) {
        writeLog("[downloadAndSaveVoice] 下载语音失败,MediaId: {$mediaId}, HTTP状态码: {$httpCode}");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("语音下载失败:" . $err, 'text');
        
        return;
    }

    // 创建用户专属目录
    $upload_dir = "upload/";
    $user_voice_dir = $upload_dir . preg_replace('/[^a-zA-Z0-9\-_]/', '_', $fromUserName) . "/";
    if (!is_dir($user_voice_dir)) {
        if (!mkdir($user_voice_dir, 0777, true)) {
            writeLog("[downloadAndSaveVoice] 创建用户目录失败: {$user_voice_dir}");
            return;
        }
    }

    // 生成语音文件名(使用时间戳+随机数+原始媒体ID确保唯一性)
    $fileName = date('YmdHis') . '_' . rand(1000, 9999) . '_' . $mediaId . '.amr';
    $filePath = $user_voice_dir . $fileName;

    // 保存语音
    $res = file_put_contents($filePath, $result);
    if ($res !== false) {
        writeLog("[downloadAndSaveVoice] 语音保存成功: {$filePath}");
    } else {
        writeLog("[downloadAndSaveVoice] 语音保存失败,MediaId: {$mediaId}");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("语音保存失败", 'text');
    }
}

/**
 * 下载并保存视频
 */
function downloadAndSaveVideo($mediaId, $fromUserName) {
    // 获取access_token
    $accessToken = getAccessToken();
    if (!$accessToken) {
        writeLog("[downloadAndSaveVideo] 获取access_token失败");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("下载视频失败:获取access_token失败", 'text');
        
        return;
    }

    // 构造下载视频的API地址
    $downloadUrl = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token={$accessToken}&media_id={$mediaId}";

    // 使用curl下载视频
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $downloadUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 视频文件较大,增加超时时间
    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch); // 获取curl错误信息
    curl_close($ch);

    // 检查是否是有效的视频数据(非错误JSON响应)
    $isJson = json_decode($result) !== null;
    if ($httpCode !== 200 || $isJson) {
        writeLog("[downloadAndSaveVideo] 下载视频失败,MediaId: {$mediaId}, HTTP状态码: {$httpCode}");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("视频下载失败:" . $err, 'text');
        
        return;
    }

    // 创建用户专属目录
    $upload_dir = "upload/";
    $user_video_dir = $upload_dir . preg_replace('/[^a-zA-Z0-9\-_]/', '_', $fromUserName) . "/";
    if (!is_dir($user_video_dir)) {
        if (!mkdir($user_video_dir, 0777, true)) {
            writeLog("[downloadAndSaveVideo] 创建用户目录失败: {$user_video_dir}");
            return;
        }
    }

    // 生成视频文件名(使用时间戳+随机数+原始媒体ID确保唯一性)
    $fileName = date('YmdHis') . '_' . rand(1000, 9999) . '_' . $mediaId . '.mp4';
    $filePath = $user_video_dir . $fileName;

    // 保存视频
    $res = file_put_contents($filePath, $result);
    if ($res !== false) {
        writeLog("[downloadAndSaveVideo] 视频保存成功: {$filePath}");
    } else {
        writeLog("[downloadAndSaveVideo] 视频保存失败,MediaId: {$mediaId}");
        
        // 包含错误提示功能
        include_once "send_post.php";
        send_message("视频保存失败", 'text');
    }
}

/**
 * 获取企业微信access_token
 */
function getAccessToken() {
    // 从全局配置获取参数
    $corpid = $GLOBALS['config']['corpId'];      
    $corpsecret = $GLOBALS['config']['corpSecret'];  // 从配置获取应用的Secret

    $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$corpid}&corpsecret={$corpsecret}";
    $response = file_get_contents($url);
    $result = json_decode($response, true);

    if (isset($result['access_token'])) {
        return $result['access_token'];
    } else {
        writeLog("[getAccessToken] 获取access_token失败: " . $response);
        return false;
    }
}
        
/**
 * 获取错误码描述
 */
function getErrorCodeDescription($errorCode) {
    $errorDescriptions = [
        -40001 => '签名验证错误',
        -40002 => 'xml解析失败',
        -40003 => 'sha加密生成签名失败',
        -40004 => 'encodingAesKey 非法',
        -40005 => 'corpid 校验错误',
        -40006 => 'aes 加密失败',
        -40007 => 'aes 解密失败',
        -40008 => '解密后得到的buffer非法',
        -40009 => 'base64加密失败',
        -40010 => 'base64解密失败'
    ];
    
    return isset($errorDescriptions[$errorCode]) ? $errorDescriptions[$errorCode] : '未知错误';
}

/**
 * 记录日志
 * 符合日志功能设计规范
 */
function writeLog($log) {
    $logData = date('Y-m-d H:i:s') . " - " . $log . PHP_EOL;
    $logFile = './logs/' . date('Y-m-d') . '.log';
    
    // 创建logs目录(如果不存在)
    $logDir = dirname($logFile);
    if (!is_dir($logDir)) {
        mkdir($logDir, 0755, true);
    }
    
    // 使用独占锁写入日志,符合日志功能设计规范
    file_put_contents($logFile, $logData, FILE_APPEND | LOCK_EX);
}
    
?>

一些问题&解决方法

echostr 报错解决,PHP版本问题,加一句:

 error_reporting(0);

遇到传参的时候,有空格的情况使用 \"$@\"

 #!/bin/bash
 sshpass -p "passwordxxx" ssh root@111.222.333.444 "sh /root/callBack.sh \"$@\""

php shell_exec 函数无法执行,在php中找到禁用函数,然后删掉(宝塔php管理里面就有)在 php.ini 里面 disable_functions= shell_exec 的函数删掉 ,顺便把安全模式关闭 safe_mode=off

如果试图通过函数popen()、system()或exec()等执行脚本,只有当脚本位于safe_mode_exec_dir配置指令指定的目录才可能。

在php脚本中调用shell脚本无法执行时,检查执行权限,打开/etc/sudoers ,找到这段,在下面加一行

 ## Allow root to run any commands anywhere 
 root ALL=(ALL) ALL
 www   ALL=(ALL)     NOPASSWD: ALL


https://developer.work.weixin.qq.com/document/path/90236

THE END