首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 微信公眾平台接入驗證全流程詳解:從 Token 配置到消息回復

     編輯:PHP 綜合知識

前言

微信公眾平台開發的第一步是「接入驗證」,只有通過驗證,服務器才能接收微信推送的消息與事件。本文基於官方示例代碼,詳細講解驗證原理、代碼結構、常見失敗原因、驗證後的消息處理邏輯,並提供生產可用的最佳實踐代碼。

驗證原理圖解

  1. 開發者在公眾平台后台填寫 URL(接收推送的接口地址)和 Token(自定義字符串)。
  2. 微信服務器發送 GET 請求 到該 URL,攜帶參數:signature、timestamp、nonce、echostr。
  3. 開發者服務器按規則驗證 signature:
    • 將 token、timestamp、nonce 三個參數進行字典序排序。
    • 將排序後的字符串拼接成一個字符串進行 SHA1 加密。
    • 加密後的字符串與 signature 對比,相等則驗證通過。
  4. 驗證通過後,原樣返回 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 BOMUTF-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';
}

MessageHandler 核心類結構

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 字符串用法總結 等專題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved