WeChat公式アカウントで発生するイベントを受信する
WeChatオフィシャルアカウントプラットフォームのアカウントに対してWeChatユーザーが何かアクションを起こすとイベントが発生します。
そのイベントはWeChatオフィシャルアカウントプラットフォームの管理画面で指定したサーバやWeChatオープンプラットフォームのサードパーティ授権機能で「消息管理」の権限を許可されたシステムに送信されます。
イベントがどのように送られて来るのか、どのようなイベントがあるのか見ていきましょう。
イベントを受け取る為の設定
イベントを受け取るには2つの方法があります。1つはオフィシャルアカウントプラットフォームの管理画面で受信サーバを指定する方法、もう1つはオープンプラットフォームのサードパーティ授権機能を使う方法です。
ここではオフィシャルアカウントプラットフォームの管理画面で設定する方法をご紹介します。
まず公式アカウントでオフィシャルアカウントプラットフォームにログインします。
管理画面の左にある「基本配置」をクリックすると右側に「服务器配置」が表示されます。「修改配置」をクリックするとサーバ情報の入力画面が開きますので、自分のサーバ情報を入力しましょう。設定するサーバ情報は以下の4つです。
服务器地址(URL):メッセージを受信するサーバのURL。「https://」または「http://」を含めたURLを指定します。
例:https://wechat.example.com/message_listener.php令牌(Token):WeChatシステムから送られて来るデータのチェックやメッセージを暗号化して送受信する場合に使います。また、サーバ設定を適用する際にWeChatのシステムが送ってくる確認の為のアクセスに応答する際にも使います。従って4番目の設定項目で暗号化を選択していな場合でも設定が必要です。(半角英数字3文字~32文字)
消息加解密密钥(EncodingAESKey):メッセージを暗号化して送受信する際に使います。長さは半角英数字43文字で固定です。設定画面にある「随机生成」ボタンを押すとランダムで43文字の文字列を自動で生成してくれます。メッセージを暗号化しない場合はこの値を使いませんが、必須項目なので設定は必要です。
消息加解密方式:送受信するメッセージを暗号化するかどうかを設定します。選択肢は3つあります。
・明文模式:メッセージを平文で送受信します
・兼容模式:メッセージを平文と暗号文の双方で送受信します
・安全模式(推荐):メッセージを暗号文のみで送受信します
暗号化される対象はWeChatシステムから送られて来るメッセージとそれに応答する際に送信するデータです。
データ受信プログラムで確認リクエストへ応答する
上記設定画面で「服务器地址(URL)」を指定するとWeChatシステムから指定したURLへGET文字列に以下の情報を含む確認リクエストが送られて来ます。
signature → データが偽造されていない事を確かめる為の署名
echostr → signatureをチェックして問題ない場合に応答する文字列
timestamp → signatureのチェックに使うタイムスタンプ
nonce → signatureのチェックにつかうランダム文字列
上記リクエストには以下のように応答します。
GET文字列で送られて来たtimestampとnonce、そして管理画面で指定したTokenをアルファベット順に並べ替えてから連結して1つの文字列にする
連結した文字列をSHA-1でハッシュ化する
ハッシュ化した文字列とGET文字列で送られて来たsignatureを比較して等しければGET文字列で送られて来たechostrを出力する
WeChatシステムはこの確認リクエストに対して「echostr」を応答してくる事を期待しているので、「服务器地址(URL)」に指定したプログラムには必ず上記処理を加えましょう。プログラムが「echostr」を応答しない場合はWeChatシステムに正常な処理が出来ていないと判断されイベントを受信する設定が自動的に無効化されます。
ちなみにsignatureのチェックは必須ではありませんので、チェックせずに「echostr」を応答してもOKです。
例えばこの確認リクエストに応答するだけで良いならこんなPHPプログラムで十分です。
(signatureのチェックはせずに常にGET文字列にあるechostrを返す)
<?php
echo $_GET['echostr'];
「echostr」がGET文字列で送られて来るのはこの設定時の確認リクエストのみですので、プログラムではまず最初にGET文字列に「echostr」があるかどうかをチェックして処理を切り替えると良いでしょう。
WeChatシステムが送ってくるイベントの種類
公式アカウントに関してWeChatシステムが送ってくるイベントは以下の通りです。
フォロー → ユーザーが公式アカウントをフォローすると発生するイベント
アンフォロー → ユーザーが公式アカウントのフォローを外すと発生するイベント
会話 → ユーザーが公式アカウントに対して話しかけると発生するイベント
QRコードスキャン → ユーザーが公式アカウントのQRコードをスキャンして公式アカウントへ入って来た場合に発生するイベント
※ただし、すでにフォローしているユーザーがトラッキングコード付きQRコードをスキャンした場合に限る。未フォローのユーザーがトラッキングコード付きQRコードをスキャンしてフォローした場合はフォローイベントにトラッキングコードが付与されるので、このイベントは発生しない位置情報 → 位置情報の提供に同意してるユーザーが公式アカウントを開いた時に送られて来るGPS情報
※開いたタイミングで1回だけ受信するモードと開いてから5秒ごとに受信するモードのどちらかを指定可能メニュークリック → 公式アカウントのメニューがクリックされると発生するイベント
送られて来るイベントの形式
イベント受信の設定が完了するとWeChatシステムから上記のような色々なイベントが送られて来ますが、それらの内容はXML形式でPOSTされて来ます。
その内容はサーバ設定の際に選択した「消息加解密方式(暗号化形式)」によって決まります。
例としてWeChatユーザーが公式アカウントをフォローした場合に送られて来るメッセージの各形式を示します。
「明文模式」の場合:
<xml>
<ToUserName><![CDATA[gh_xxxxxxxxxxxx]]></ToUserName>
<FromUserName><![CDATA[oOxxxxxxxxxxxxxxxxxxxxxxxxxx]]></FromUserName>
<CreateTime>1646380011</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
「明文模式」ではデータが平文で送られて来るので、XMLをそのまま解釈すればメッセージの内容を読み取る事が出来ます。
「兼容模式」の場合:
<xml>
<ToUserName><![CDATA[gh_xxxxxxxxxxxx]]></ToUserName>
<FromUserName><![CDATA[oOxxxxxxxxxxxxxxxxxxxxxxxxxx]]></FromUserName>
<CreateTime>1646391001</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
<Encrypt><![CDATA[hl3ItEJkbutG674kzoETtp2Ch6tDdH3+VhYGoeOsArNZIzNio9HyKynadZyrFk5HkMLidgWlT3Q3K94njQZPdFDVmPYwsfCHWbLEYE1cQv9iPHlHsGmVqjexJSbGJppYYWQxuv57GhxcTPZiAySCVySbjZWMf6SmokLCXa3kHkOAeiPR45SGZOjnSHTfs+D4RKAcxY+1Al6h7+xl0I0xqxVT9ebNiz41ChUDbgYDeQZseBBswYlqu7iRU8ghvi9A3JOP7iN7op2/5mrURRPvmVkzpixR0gVdih8joCHEWWS5i19nObziOrUBbs853c4JCRgST0fiogxqlDsh5ES6ySX4Ylz33JU39Tn1G90ivwXMSV4Etpe3bzrnkcjGDI/gtbBwxfYf/VjXfJgk/S5aYyPNMwPwnVVAU7653Z36Wv4=]]></Encrypt>
</xml>
「兼容模式」ではデータが平文と暗号文の両方で送られて来ます。XMLには「Encrypt」という要素が追加されており、これが暗号化されたデータになります。このデータを復号化すると平文のXMLデータになります。
「安全模式」の場合:
<xml>
<ToUserName><![CDATA[gh_xxxxxxxxxxxx]]></ToUserName>
<Encrypt><![CDATA[5WSFCsP3ZO3euKxzYy6HsUNLpwegfYOaufF3uw/xZin2nfFnNFySD6eXdJrD3x1Prm5jfDiZiTEmJtqA/KAHZbVSP7bWMSu0oGBGNlFrSMLudsiyTZzrVTJpd/fWbKdAdI04/36UrN1/1zGGACyKATDFhB1TwFrcsd0xTBow5s57mhuudcXrw42GLa/Z/CVVLMQ++HCOnvC8XTZ6OzvygOe3kol5H12T5bXGTgGxJ6+XhLFNGgCSU3LZQvvz/I0ExV9fx4wZqcyOAkM8/EYt2iWIAmXa3r9m/TBRm1wLsA72a4OLoN2eOG+DAypAYnYSfM4vq7rp0pMAEIA5CAa3RYxw1q63rS0AyQhXoLTAQaOpxrhdq4h3B8UJ9K/jWuXPCL/AYQQcoZ9c4w3i5ZTwG4j2fqgsfy/xTMaYIpsCuPw=]]></Encrypt>
</xml>
「安全模式」ではデータが暗号文のみで送られて来ます。要素「Encrypt」を復号するとXML形式のメッセージを取り出す事が出来ます。
これとは別にURLのGET文字列に以下の情報が含まれます。
「明文模式」の場合:
signature → データが偽造されていない事を確かめる為の署名
timestamp → 署名チェックに利用するタイムスタンプ
nonce → 署名チェックに利用するランダム文字列
openid → アクションを起こしたユーザーのOpenID
「兼容模式」と「安全模式」の場合:
signature → データが偽造されていない事を確かめる為の署名
timestamp → 署名チェックに利用するタイムスタンプ
nonce → 署名チェックに利用するランダム文字列
openid → アクションを起こしたユーザーのOpenID
encrypt_type → 暗号化方式(「aes」で固定)
msg_signature → 暗号化されたデータの検証用署名
暗号化されたメッセージの復号
暗号化されたメッセージ(要素「Encrypt」)を復号する手順は以下の通りです。
GET文字列で送られて来たtimestampとnonce、管理画面で指定したToken、XMLで送られて来た「Encrypt」をアルファベット順に並べ替えてから連結して1つの文字列にする
連結した文字列をSHA-1でハッシュ化する
ハッシュ化した文字列とGET文字列で送られて来たmsg_signatureを比較して等しいことを確認する(signatureと間違えないように注意)
管理画面で設定したEncodingAESKeyの末尾に「=」を追加したうえでBase64でデコードして共通鍵を得る
「Encrypt」を手順4でデコードした共通鍵と共通鍵の先頭16バイトを初期ベクトルとしてAES-256-CBCで復号する
復号したデータからXMLを取り出す方法
復号されたバイナリデータは以下のような構成になっています。(データはAES-256-CBCで暗号化されている関係でPKCS#7という方式でパディングされています)
ランダム文字列(16バイト) + XMLのデータ長(4バイト) + XML本文 + AppID + パディングデータ
以下の手順でXMLを取り出せます
データ先頭から数えて17~20バイトまでの4バイトを取り出し、ビッグエンディアンのunsigned longとしてunpackしてXMLの文字列長を取得する
データの21バイト目から手順1で取得したXMLのデータ長分のデータがXMLデータとなる
テンセントのサンプルコードを利用する
テンセントは暗号化と復号化のサンプルコード(PHP、Java、C++、Python、C#)を公開しており、それらを使えば簡単に実装は出来ます。ただし、オープンソースライセンスで提供しているわけではなさそうなので、納品物などに含める場合は自分で書き直しましょう。
【テンセントが提供しているサンプルコード】
https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip
なお、PHPのサンプルコードはlibmcryptを使っています。libmcryptはメンテナンスされていないライブラリでPHP7.1以降で非推奨となり、PHP7.2でサポートされなくなりました。
無理やりlibmcryptを使う方法もありますが、素直にOpenSSLに書き換える事をお勧めします。「mcrypt openssl php」といったキーワードで検索すれば有益な情報が得られると思います。WeChatAPIでの実装方法については機会があれば記事にしたいと思います。