ShopifyのWebhookをStrapiで受け取るまで


Shopifyでは、外部サービスへの通知として様々なWebhookが用意されています。(カートへの追加、アカウントの追加、注文作成、商品作成...etc)
-> 公式ドキュメント: Events > Webhook もご参考に...

Webhookをうまく使えば、Shopifyから他のサービスへの連携も可能になることでしょう。
というわけで、ShopifyからのWebhookをStrapiで受け取るまでを試してみました。

※ShopifyのWebhookについては、こちらの記事も参考になると思います。

ShoipfyのWebhook設定

今回は、Shopifyのストアに通知を設定します。
※Shopifyのストアは、パートナーアカウントの管理画面から「開発」用のストアを用意しました。

1. 設定画面へ移動

  • ストアの管理画面にある左側メニューの最下部に「設定」のリンクから設定画面に移動します

2. 通知設定へ移動

  • 設定画面の真ん中あたりにある「通知」のリンクから通知用の設定画面に移動します

3. Webhookを作成

  • 最下部にWebhookに関する設定があります。そこからWebhookを作成します

イベントの選択、コールバックURLの指定などを経て、Webhookを作成します。
コールバックURLは後で変更可能ですので、現時点では適当なURLを指定しておきます。
※作成したWebhookはテスト通知を送信できますので、以降で確認したいと思います。

また、「すべてのWehookは...」と表示されている箇所で、ランダムな文字列が表示されます。この文字列はWebhook検証時に利用します。

ShopifyのWebhook検証方法

公式ドキュメントにwebhookの検証に関する記載があります。基本的にはこの内容に沿って実装する感じです。

具体的な検証ロジックとしては、リクエストヘッダーにあるShopifyからのHAMC (X-Shopify-Hmac-SHA256)とリクエストボディとシークレットキー(Webhook作成時に生成された文字列)を使って、算出したHMACが同じ値になるかを比較する、という具合です。

なお、今回はStrapiというheadless CMSを使って、Webhookを受け取ることを試しました。
※Strapi -> https://strapi.io/

1. ctx.request.bodyに、RawBodyを保持するための設定を追加

(すみません。詳細は割愛しますが...)
StrapiがHTTPリクエストを受け取ってカスタマイズするコード内で処理するまでの間に、様々なmiddlewareがリクエストを処理します。
その中の1つにkoa-body(https://github.com/dlau/koa-body) を使ってパースしている処理があります。
このパーサーに、RawBodyを含めるよう設定しないと後述するWebhook検証ができなくなるので、ここで設定を一部変更します。

具体的には config > environments > (development | staging | production) > request.json を変更します。
※request.jsonの中で、 "parser""includeUnparsed": trueを追加します

{
  "session": {
    "enabled": true,
    "client": "redis",
    "connection": "redis",
    "key": "strapi.sid",
    "prefix": "strapi:sess:",
    "secretKeys": ["mySecretKey1", "mySecretKey2"],
    "httpOnly": true,
    "maxAge": 86400000,
    "overwrite": true,
    "signed": false,
    "rolling": false
  },
  "logger": {
    "level": "debug",
    "exposeInContext": true,
    "requests": true
  },
  "parser": {
    "includeUnparsed": true, <--(ココ)
    "enabled": true,
    "multipart": true
  }
}

2. APIの作成

Webhookを受け取るためのAPIを作成します。


$ strapi generate:api webhook

apiディレクトリ直下に一式作成されます。

3. Webhook検証処理の実装

だいたいこんな感じです。

  • 環境変数SHOIPFY_WEBHOOK_SECRETに、Webhook作成時に生成された文字列を持たせておきます
const crypto = require('crypto');

module.exports = async (ctx, next) => {

  // ctx.request.header の中身
  // {
  //   'content-type': 'application/json',
  //   'x-shopify-topic': 'customers/create',
  //   'x-shopify-shop-domain': (ストアのドメイン),
  //   'x-shopify-hmac-sha256': (shopifyで算出したhmac),
  //   'x-shopify-api-version': '2020-01',
  //   'accept-encoding': 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
  //   accept: '*/*',
  //   'user-agent': 'Ruby',
  //   'content-length': '552',
  //   connection: 'close',
  //   'x-forwarded-proto': 'https',
  //   'x-forwarded-for': '35.196.97.22'
  // }
  const hmac_header = ctx.request.header['x-shopify-hmac-sha256'];

  // rawBodyとしてセットされているbodyを取得する
  //  request.json内で、 "parser" に "includeUnparsed": true を追加すると取得できるようになる
  const unparsedSymbol = Symbol.for('unparsedBody');
  const rawBody = ctx.request.body[unparsedSymbol];
  const calculated_hmac = crypto.createHmac('sha256', process.env.SHOIPFY_WEBHOOK_SECRET)
    .update(rawBody, 'utf8')
    .digest('base64');

  console.log('------');
  console.log('hmac_header     >> ' + hmac_header);
  console.log('calculated_hmac >> ' + calculated_hmac);
  const isvalid = crypto.timingSafeEqual(Buffer.from(hmac_header), Buffer.from(calculated_hmac));
  if (!isvalid) {
    return ctx.unauthorized('Invalid Request');
  }
  console.log('shopify webhook: ok.');

  // 何がしかの処理...

  ctx.send(200);
};

4. 実際に試してみる

Strapiをローカルで起動して確認する際には、ローカルで起動したアプリにインターネットからアクセスできるようにする必要があります。
そこで今回は、ngrokというトンネルサービスを利用して、ローカルで立ち上げたサーバーを一時的に、インターネットからアクセスできるようにします。

Strapiは通常ポート番号:1337で起動しますので、こんな感じでngrokを起動させておきます。


$ ngrok http 1337

インターネットで公開されたURLと合わせて、コールバックURLをShopifyのストアで作成したWebhookに設定しておきましょう。
WebhookのコールバックURLは後から変更可能です。

そして、「テスト通知を送信する」というリンクからコールバックURLにテストをPOSTしますので、結果をローカルのコンソールで確認しましょう。
こんな感じで算出された値が一致すれば成功です!

------
hmac_header     >> pl6HMcWpl4f0OAmDsdv83LX1RSmf7PX6ghKOV76VkUo=
calculated_hmac >> pl6HMcWpl4f0OAmDsdv83LX1RSmf7PX6ghKOV76VkUo=
shopify webhook: ok.

(その他)

本題から外れますが、Strapiをどこにホストするか?というあたりで、EC2を用意するなど色々な選択肢があると思いますが、個人的にはHeroku推しです(笑

もちろん、Heroku上にStrapiをデプロイすることは可能です。セッションの管理にredisを使用するとか物理的なファイルアップロード先にcloudinaryやS3を設定するなど、Heroku特有の設定を施すことは必要ですが、サーバのメンテナンスにかける工数が確保できないような状況では非常に心強いと思います。

Shopify -> Heroku -> ?(Salesforceなど)

と、うまい具合に連携できるようになると、可能性はまた一段と広がるかと思います。