Vue.js+Firebase環境でreCAPTCHA v3を組み込む

## はじめに
フロントエンドでVue.js(Vue CLI 3、Typescript)、バックエンドでFirebaseを使って個人？アプリを作成中ですが、reCAPTCHA v3を組み込むときに時間がかかったので、自身の忘備録として残したいと思います。
結論としては、まんまの↓を実装すれば良いです。
https://developers-jp.googleblog.com/2017/09/guard-your-web-content-from-abuse-with.html

私の場合、要件に合わなかったので合う形に変えたサンプルをgithubにupしています。
secret keyなどは雑に扱ってます。実コードでは埋め込まないようにしましょう。。。
https://github.com/ryoutoku/vue-recaptcha-firebase


### 想定読者

* Vue.jsを使う
    * 注：TypeScript使っています
* reCAPTCHA v3を組み込む必要がある
    * 注：バックエンドとしてFirebase(Cloud Functions)使っています

### 前提
* Firebaseでプロジェクトを作成しており、Vue.jsのコードのデプロイが可能な状態

### 書くこと

* サンプルコードなど
* 自身が理解した動作の内容

### 書かないこと

* VueとかFirebaseのプロジェクトの作成方法
* Firebaseのプロジェクトの設定(デプロイなど含む)
* reCAPTCHAのサイト登録などの方法

### 最終結果

* reCAPTCHAのスコアを取得(表示)する

## reCAPTCHAとは

ざっくり言うと、ユーザがbotではないと判断するための以下のようなよくあるヤツです。
![](https://developers.google.com/recaptcha/images/newCaptchaAnchor.gif)
画像参照：https://developers.google.com/recaptcha/

ちなみにv3はUIはありません。

## 前準備
組み込むためには、使用するための前準備が必要です

1. googleアカウントを作成
2. reCAPTCHAを使用するサイト等を設定
    * 以下サイトからreCAPTCHA使うサイトの登録を行う必要があります <br>
サイトキーとシークレットキーの発行が必要です <br>
https://www.google.com/recaptcha/intro/v3.html

## 処理イメージ
今回実装したもののデータの流れのイメージはこんな感じになりました。
ちなみに「reCAPTCHAサーバ」とか正確な名称ではありませんが、ニュアンスだけ感じて頂ければ。

![Screenshot from 2019-07-08 14-45-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172007/b09a005a-b329-caf0-b503-d5cfb05755d5.png)

1. reCAPTCHAの設定で発行されたサイトキーを元にサーバにアクセス
2. サーバからtokenが発行される
3. 発行されたtokenを元にCloud Functionsにアクセス
    * CORSの設定上、フロントから直接reCAPTCHAサーバにアクセスできないためCloud Functionsを経由する必要がある
4. Cloud Functionsからtokenを元にreCAPTCHAサーバにアクセス
5. 判定結果がCloud Functionsに戻る
6. Cloud Functionsからロボットか否かの判定が帰ってくる

* 1~2は、以下の「Frontend integration」のコードに該当<br>https://developers.google.com/recaptcha/docs/v3
* 3~5は、以下ページの記載に該当<br>https://developers.google.com/recaptcha/docs/verify

## サンプルのUI

以下の様な簡単な動作をするものを作成しました。
1. ```reCAPTCHA実行する```ボタンを押下
2. 通信して戻ってきたtokenを```token```横に表示
3. そのtokenを使ってreCAPTCHAのチェックを実行、結果を```result```横に表示
4. errorがあれば```error```横に表示する

reCAPTCHA実行するボタン押す前
![Screenshot from 2019-07-09 14-29-27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172007/b14f3a5d-d7e6-0732-6700-5d32fc0ae888.png)

reCAPTCHA実行するボタン押した後
![Screenshot from 2019-07-09 14-29-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172007/251944c2-f225-8a97-526b-e46e6f1e69c4.png)


## 実装コード

### フォルダ構成
作成したプロジェクトの構成は↓の様な感じです。

```bash
├── babel.config.js
├── firebase.json
├── functions
│   ├── node_modules
│   ├── package.json
│   ├── package-lock.json
│   ├── src
│   │   └── index.ts		// ← バックエンド側のメインロジック
│   ├── tsconfig.json
│   └── tslint.json
├── node_modules
├── package.json
├── postcss.config.js
├── public
│   ├── favicon.ico
│   └── index.html
├── README.md
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── reCAPTCHAUI.vue	// ← フロント側のメインロジック
│   ├── main.ts
│   ├── shims-tsx.d.ts
│   └── shims-vue.d.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
```

VueにはreCAPTCHA v3用の[Vue reCAPTCHA-v3](https://github.com/AurityLab/vue-recaptcha-v3)といったコンポーネントも公開されていますが、あまりメリットを感じられなかったので[reCAPTCHA-v3](https://github.com/AurityLab/recaptcha-v3)を使って実装しています。

```bash
cd project_root

npm install recaptcha-v3
// or
yarn add recaptcha-v3
```

### バックエンド側

処理コードのメインロジック

```ts:functions/src/index.ts

import * as functions from "firebase-functions";
import * as rp from "request-promise";

export const checkRecaptcha = functions.https.onCall(async (data, context) => {
  // ここに発行したsecret keyを設定
  const secret = "secret";

  let response = {};
  await rp({
    uri: "https://recaptcha.google.com/recaptcha/api/siteverify",
    method: "POST",
    formData: {
      secret,
      response: data.token,
    },
    json: true,
  })
    .then(async result => {
      console.log("recaptcha result", result);
      response = result;
    })
    .catch(async reason => {
      console.log("Recaptcha request failure", reason);
      response = reason;
    });
  return response;
});

```
簡易解説：

* ```functions.https.onCall```でデータを受ける
    * ```functions.https.onRequest```を使っても可能だが、APIを外部に公開する訳ではない&外部公開する場合はHTTPメソッド(特にOPTIONS)に対応する必要があるためこちらを使う
* reqestモジュールを使ってPOSTで```https://recaptcha.google.com/recaptcha/api/siteverify```にtokenを投げる
    * 投げる先はこちら参照：https://developers.google.com/recaptcha/docs/verify

### フロントエンド側処理コード
処理コードのメインロジック抜き出し

```ts:src/components/reCAPTCHAUI.tsの一部

interface IReCAPTCHAResult {
  success: boolean;
  challenge_ts: string;
  hostname: string;
  "error-codes": [];
  [key: string]: any;
}

@Component
export default class reCAPTCHAUI extends Vue {
  private token: string = "";
  private result: IReCAPTCHAResult = {
    success: false,
    challenge_ts: "",
    hostname: "",
    "error-codes": []
  };

  // ここに発行したsite keyを設定
  private siteKey = "site-key";

  // ここでreCAPTHCAのチェックレベルを設定
  private action = "homepage";
  private error = {};

  private async click() {

    // モジュールを使ってtokenを取得する
    const recaptcha = await load(this.siteKey);
    this.token = await recaptcha.execute(this.action);

    // tokenを用いてCloud FunctionsのAPIを実行する
    const func = firebase.functions().httpsCallable("checkRecaptcha");
    await func({ token: this.token })
      .then(async response => {
        this.result = (await response.data) as IReCAPTCHAResult;
      })
      .catch(error => {
        this.error = error;
      });
  }
}

```
簡易解説：

* ボタン押下で```click()```を呼び出す
* ```action```には所定の文字列を設定
    * 参考：https://developers.google.com/recaptcha/docs/v3
* 結果取得まで処理を待ちたいので```async```と```await```を使用
* Cloud Functionsは```functions.https.onCall```を使用するので、それに準じ```firebase.functions().httpsCallable```を使用

## 終わりに
Vue.js+FirebaseでreCAPTCHAのスコアを持ってくるところまでできました。
本サンプルでは結果をそのままフロントまで返していますが、バックエンド側でscoreに依る判定を組み込めばokです。


## 参考
### 公式
https://www.google.com/recaptcha/intro/v3.html
https://developers-jp.googleblog.com/2017/09/guard-your-web-content-from-abuse-with.html

### その他参考
https://qiita.com/ritou/items/e92aad65c5d8c906edb3
https://github.com/AurityLab/vue-recaptcha-v3
https://github.com/AurityLab/recaptcha-v3