simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

PlayFab:CloudScriptの実行頻度からクライアントを改竄したリクエストを無視する

前置き

自作ゲームの PlayFab におけるチート行為防止の話です。

f:id:simplestar_tech:20191227192113p:plain
現在の見た目(ここのところ変わってませんね)

キューブで構成される世界データをたった一つのサーバーに格納しています。
simplestar-tech.hatenablog.com

今のところユーザーからのリクエストはほぼすべて信用して処理しているのでかなり危険なつくりとなっています。
最近ゲームの仕様として、最も優れたゲーム内ツールを使ったとしても、一秒はブロックの破壊に要することに決めました。

となると、あるユーザーがキューブに対するリクエストを連続で行う場合に、一秒より短いリクエストをしてきた場合
これはクライアント改竄にほかなりません(またはコンピュータ内の時間の流れを速めているチート行為かもしれません)

別に最強すぎるピッケルとか作ってゲーム内で効率的に地面を掘ってもらってもよいのですが、データを集約しているサーバーにアクセスが集中してしまうのは
ほかのお客さんに対して不利益が生じてしまいます。アクセスが遅くなったり、貴重なアイテムの争奪戦で常に負かされてしまったりと

そこで、PlayFab の CloudScript でそういう高頻度アクセスの対処方法って何がベストなのかな~と調べたのが始まりでした。

UserInternalData に前回のリクエスト時刻を打つ

見つけたやり取りがこちら

どうやら UserInternalData にユーザーごとの情報を格納して、なんとか検知してみるといいとのことです。
自分は UserInternalData に LastRequestTime を記入して、次のリクエスト時にこれを参照して、ちゃんと 1秒以上経っていることを確認してからリクエストを処理し始めることにしました。
実験して気づけましたが、for文などでほぼ同時にリクエストすると、InternalData に書き込み終える前にリクエストを並列で処理し始めてしまいます。
そんなときのために DataVersion が確実にインクリメントされていることをチェックしています。(これ必須でした)

実際に動作確認できたコードがこちら

// cubedata request
handlers.requestCubeData = function(args, context) {
  const now = Date.now();
  // get last request time
  const lastTimeKey = "LastRequestTimeCubeData";
  const getInternalDataResponse = server.GetUserInternalData({
    PlayFabId: currentPlayerId,
    Keys: [lastTimeKey]
  });
  // request must have 1000 ms interval
  const lastTimeData = getInternalDataResponse.Data[lastTimeKey];
  if (null != lastTimeData) {
    const lastTime = Number(lastTimeData.Value);
    if (1000 > now - lastTime) {
      return JSON.stringify({
        status: 400,
        message: "called in interval"
      });
    }
  }
  // imidiately set last request time
  const updateInternalDataResponse = server.UpdateUserInternalData({
    PlayFabId: currentPlayerId,
    Data: {
      [lastTimeKey]: now
    }
  });
  // check DataVersion up 1
  if (
    updateInternalDataResponse.DataVersion !=
    getInternalDataResponse.DataVersion + 1
  ) {
    return JSON.stringify({
      status: 401,
      message: "Data inconsistency"
    });
  }

  // do something

クライアントを改竄してチート検出できているかチェック

f:id:simplestar_tech:20191227191344p:plain
連続で8回リクエストしたうち、一回だけ成功したので、大成功

まとめ

知見としては、連続して CloudScript を実行すると、関数は並列で走ることが裏付けされたことが大きい収穫

また DataVersion が関数内で正しくインクリメントされたケースを使えば、特定ユーザーの高速連続リクエストの不正なものだけを拒否できました。
良い例を示せたんじゃないかな?