微信公眾平台開發的第一步是「接入驗證」,只有通過驗證,服務器才能接收微信推送的消息與事件。本文基於官方示例代碼,詳細講解驗證原理、代碼結構、常見失敗原因、驗證後的消息處理邏輯,並提供生產可用的最佳實踐代碼。
signature、timestamp、nonce、echostr。signature:
token、timestamp、nonce 三個參數進行字典序排序。signature 對比,相等則驗證通過。echostr 參數內容(微信以此確認 URL 有效性)。<?php
/**
* 微信接入驗證示例 - 官方下載版本
*/
define('TOKEN', 'weixin'); // 必須與後台填寫的 Token 完全一致
$wechatObj = new WechatCallbackApiTest();
$wechatObj->valid(); // 執行驗證
class WechatCallbackApiTest {
public function valid() {
$echoStr = $_GET['echostr'] ?? '';
// 可選:簽名驗證
if ($this->checkSignature()) {
echo $echoStr; // 必須原樣輸出 echostr
exit;
}
}
public function responseMsg() {
// 獲取原始 POST 數據
$postStr = $GLOBALS['HTTP_RAW_POST_DATA'] ?? file_get_contents('php://input');
if (!empty($postStr)) {
libxml_disable_entity_loader(true); // 防 XXE
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$fromUsername = $postObj->FromUserName;
$toUsername = $postObj->ToUserName;
$keyword = trim($postObj->Content ?? '');
$time = time();
// 文本回復模板
$textTpl = "<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[%s]]></Content>
<FuncFlag>0</FuncFlag>
</xml>";
if (!empty($keyword)) {
$msgType = 'text';
$contentStr = 'Welcome to wechat world!';
$resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
echo $resultStr;
} else {
echo 'Input something...';
}
} else {
echo '';
exit;
}
}
private function checkSignature() {
$signature = $_GET['signature'] ?? '';
$timestamp = $_GET['timestamp'] ?? '';
$nonce = $_GET['nonce'] ?? '';
$token = TOKEN;
$tmpArr = [$token, $timestamp, $nonce];
sort($tmpArr, SORT_STRING); // 字典序排序
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
return $tmpStr === $signature;
}
}
| 節點 | 說明 | 常見錯誤 |
|---|---|---|
| Token 一致性 | 代碼常量與後台配置完全一致,區分大小寫 | 多餘空格、全半角字符、編碼不一致 |
| 排序規則 | sort($arr, SORT_STRING) 字典序升序 | 用 asort 保留鍵名、未排序、大小寫敏感度錯誤 |
| SHA1 加密 | sha1(implode($arr)),非 sha1($token.$timestamp.$nonce) | 拼接順序錯誤、缺少排序步驟 |
| 輸出 echostr | 驗證通過 必須 echo $echoStr; exit; | 輸出多餘內容、BOM 頭、錯誤信息、未 exit 導致後續代碼輸出 |
| 文件編碼 | UTF-8 without BOM | UTF-8 with BOM 導致隱形字符輸出 |
重要:驗證成功後,必須註釋掉
$wechatObj->valid();,改為調用$wechatObj->responseMsg();處理業務消息,否則手機端測試無任何響應。
// 驗證階段(僅首次提交配置時啟用) // $wechatObj->valid(); // 開發/生產階段(處理用戶消息與事件) $wechatObj->responseMsg();
<?php
/**
* 生產環境微信入口 - 含日誌、異常捕獲、異步處理
*/
define('TOKEN', 'your_secure_token_here');
require_once __DIR__ . '/vendor/autoload.php'; // Composer 自動加載
use App\Wechat\Handler\MessageHandler;
use App\Wechat\Exception\WechatException;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
// 初始化日誌
$log = new Logger('wechat');
$log->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/wechat.log', 30, Logger::INFO));
try {
$handler = new MessageHandler($log);
// 區分驗證請求與業務請求
if (isset($_GET['echostr'])) {
// 驗證階段
if ($handler->verifySignature()) {
echo $_GET['echostr'];
$log->info('WeChat verification passed', ['ip' => $_SERVER['REMOTE_ADDR']]);
} else {
$log->warning('WeChat verification failed', ['signature' => $_GET['signature'] ?? '']);
echo 'Verification failed';
}
} else {
// 業務處理階段
$handler->handleRequest();
}
} catch (WechatException $e) {
$log->error('WeChat business error', ['message' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
echo 'success'; // 避免微信重試
} catch (Throwable $e) {
$log->critical('WeChat fatal error', ['message' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
echo 'success';
}
namespace App\Wechat\Handler;
use Monolog\Logger;
class MessageHandler {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function verifySignature(): bool {
$signature = $_GET['signature'] ?? '';
$timestamp = $_GET['timestamp'] ?? '';
$nonce = $_GET['nonce'] ?? '';
$token = TOKEN;
$tmpArr = [$token, $timestamp, $nonce];
sort($tmpArr, SORT_STRING);
return sha1(implode($tmpArr)) === $signature;
}
public function handleRequest(): void {
$postStr = file_get_contents('php://input');
if (empty($postStr)) {
$this->logger->warning('Empty POST data');
echo 'success';
return;
}
libxml_disable_entity_loader(true);
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
// 記錄原始消息用於審計
$this->logger->info('Received message', ['xml' => $postStr]);
$msgType = (string)$postObj->MsgType;
$fromUser = (string)$postObj->FromUserName;
$toUser = (string)$postObj->ToUserName;
// 異步處理:將消息推送到隊列,立即返回 success
// $this->pushToQueue($postObj);
// echo 'success';
// return;
// 同步處理示例(簡單業務可用)
$reply = $this->dispatch($postObj, $msgType, $fromUser, $toUser);
if ($reply) {
echo $reply;
} else {
echo 'success';
}
}
private function dispatch($postObj, string $msgType, string $fromUser, string $toUser): ?string {
return match ($msgType) {
'text' => $this->handleText($postObj, $fromUser, $toUser),
'event' => $this->handleEvent($postObj, $fromUser, $toUser),
'image', 'voice', 'video', 'location', 'link' => $this->handleMedia($postObj, $fromUser, $toUser),
default => null,
};
}
private function handleText($postObj, string $fromUser, string $toUser): string {
$content = trim((string)$postObj->Content);
// 業務邏輯:關鍵詞匹配、NLP、數據庫查詢等
$replyContent = $this->getReplyByKeyword($content);
return $this->buildTextXml($fromUser, $toUser, $replyContent);
}
private function handleEvent($postObj, string $fromUser, string $toUser): string {
$event = strtolower((string)$postObj->Event);
return match ($event) {
'subscribe' => $this->buildTextXml($fromUser, $toUser, '歡迎關注!'),
'unsubscribe' => '', // 無法回復,僅記錄
'click' => $this->handleMenuClick($postObj, $fromUser, $toUser),
'scan' => $this->handleScan($postObj, $fromUser, $toUser),
default => 'success',
};
}
private function buildTextXml(string $to, string $from, string $content): string {
$time = time();
return sprintf(
"<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>",
$to, $from, $time, $content
);
}
// ... 其他私有方法
}
系統學習:PHP 微信開發技巧匯總、PHP 基礎語法入門教程、PHP 字符串用法總結 等專題。