Amplify と React で FaceLiveness 顔認証なりすまし検知デモアプリを開発・精度検証

# なりすまし検知とは

### 顔認証システム
例えばAWSのRekognitionを使えば画像認識で人の顔を分析することができ、さらに顔コレクションからの顔検索を使うと顔認証システムを構築することができます。
顔認証の技術は駅の改札やチケット販売など、世間一般的に徐々に導入されつつあります知らんけど。富士急は顔認証つこてはりました。

しかし顔認証には考慮しないといけない重大な問題があります。
それは画像や動画などを使った「なりすまし」による不正認証の可能性です。
現在導入されている顔認証システムはどうやってこれに対処してるんでしょうか。
例えば3D検知ができる専用デバイスを使って実際にそこに人がいるかを確認したり、使用される場所が限られているのであれば運用でカバーしたりなどの方法があります。
ただこれだと専用デバイスや運用にコストがかかり、セキュアな顔認証の導入はハードルが高いため、「やっぱやーめた」となるわけです。

### FaceLiveness（フェイスライブネス）

そこで！！
AWSが2023年の４月ころに発表したサービス「Face Liveness」が使えるんです。
スマホなどの一般的なカメラデバイスで撮影するだけで実際にそこに人がいるのかを検知することができるんです。
「なんで？どうやって？」って感じですが、それを聞かれたら「AWS独自のめっちゃすごい画像認識AIモデルで人間の目にはわからない特徴を抽出しているようだね。」と答えましょう。
今回の記事ではAmplifyでデモアプリを作る方法を紹介します。

# 参考記事

https://aws.amazon.com/jp/blogs/news/detect-real-and-live-users-and-deter-bad-actors-using-amazon-rekognition-face-liveness/

https://aws.amazon.com/jp/blogs/mobile/detect-real-users-with-aws-amplify-and-face-liveness/

# 今回作るもの

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/74cae5ec-27dd-bbe7-fedd-b1e8507a0a74.png" width="50%">

なりすまし検知の点数と認証成功かどうかを表示するだけのシンプルなアプリです。
リアルの顔だと認証に成功し、人の顔の画像などで認証しようとすると失敗します。

黒い丸はもちろん後から加工したモザイクです。
顔出しNGでやらせてもろてます。

# 開発環境

まず前提として以下の二つが必要。
- [AWSアカウント](https://aws.amazon.com/free/)
- npmでインストールした[Node](https://nodejs.org/en)

バージョンは以下の通り
（`npm list --depth=0` の出力結果コピペ）
```
├── @aws-amplify/api@6.0.5
├── @aws-amplify/ui-react-liveness@3.0.3
├── @aws-amplify/ui-react@6.0.3
├── @types/node@20.10.1
├── @types/react-dom@18.2.17
├── @types/react@18.2.39
├── aws-amplify@6.0.5
├── next@14.0.3
├── react-dom@18.2.0
├── react@18.2.0
└── typescript@5.3.2
```

# 開発手順

### Nextアプリのセットアップ

新しい Next.js アプリケーションを作成する。

```bash
$ npx create-next-app
```

いくつか対話形式で聞かれる中で`typescript`を使うかを問われたら使うので`yes`。
`App Router`を利用するバージョンを使うかに関しては、今回は`App Router`を使用しないので`no`。
それ以外はなんでもいいと思う。

新しい Next アプリのディレクトリに移動して、Amplify ライブラリをインストール。

```bash
$ cd 作成されたアプリのディレクトリ

$ npm i @aws-amplify/ui-react-liveness @aws-amplify/ui-react aws-amplify
```

まだインストールしていない場合は、Amplify CLI をインストール。

```bash
$ npm i @aws-amplify/cli -g
```

こちらもまだの場合、AWSのユーザーを設定

```bash
$ amplify configure
```

### Amplifyをセットアップ

以下のコマンドを実行

```bash
$ amplify init
```

選択肢は基本的にデフォルトのものを選べばよい。
これで`src`フォルダ内に`aws-exports`ファイルが作成される。

次にユーザー認証を追加する。デモアプリのためだけに認証が本当に必要かはわからないが参考にした記事では使用しており、手間ではないのでささっと入れておく。

```bash
$ amplify add auth
```
認証にはEmailを使うように選択する。

そして、ここまでのセットアップをデプロイする。

```bash
$ amplify push
```

選択事項で気を付ける部分は以下の通り
- `Manual Configuration`を選択
- `Allow unauthenticated logins`を聞かれたら`yes`

これでアプリの実行に必要なロールも作成されたので、このロールに必要な権限を与える。
- AWSマネージメントコンソールにログインしてIAMの画面まで入る。
- 「ロール」を選択し、`amplify-プロジェクト名-dev-アカウントID-authRole`という名前のロールを選択。
- 「権限を追加」→「インラインポリシーを作成」→「JSON」より、以下のポリシーに書き換える。
    ```json
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "rekognition:StartFaceLivenessSession"
                ],
                "Resource": "*"
            }
        ]
    }
    ```
- 「確認」→「作成」で作成完了

### バックエンド構築

権限と認証の設定が完了したので、Express を実行する Node.js AWS Lambda 関数を使用して REST API を追加する。 
Express クライアントを使用して、FaceLivenessの結果を作成して取得するためのいくつかのエンドポイントをセットアップする。

ターミナルを開き、add api コマンドを実行。

```bash
$ amplify add api
```

続く設定はこんな感じ。
RESTを選択し、エンドポイントのリソース名やLambda関数の名前などを決める。

```bash
? Select from one of the below mentioned services: REST
✔ Provide a friendly name for your resource to be used as a label for this category in the project: ·liveness
✔ Provide a path (e.g., /book/{isbn}): · /session
Only one option for [Choose a Lambda source]. Selecting [Create a new Lambda function].
? Provide an AWS Lambda function name: liveness
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Serverless ExpressJS function (Integration with API Gateway)

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? Yes
```

これにより、API Gateway と Lambda 関数用にいくつかの新しいフォルダーが生成される。

ここに新しいライブラリを追加する。
`amplify/backend/function/liveness/src`フォルダーに移動し、次のコマンドを実行して Rekognition クライアントの正しい依存関係をインストール。

``` bash
$ cd amplify/backend/function/liveness/src

$ npm i @aws-sdk/client-rekognition
```

作成した関数を編集する必要がある。`amplify/backend/function/liveness/src`に`utils`フォルダを追加し、`getRekognitionClient.js`という新しいファイルを作成。
Rekognition クライアントを作成するコードを追加する。

```javascript: getRekognitionClient.js
const { Rekognition } = require("@aws-sdk/client-rekognition");

const getRekognitionClient = () => {
  const rekognitionClient = new Rekognition({
    region: 'ap-northeast-1'  // 自分のリージョンを指定
  });

  return rekognitionClient;
};

module.exports = getRekognitionClient;
```

`region`には利用しているAWSのリージョンを指定するが、そのリージョンでFaceLivenessが使えるかを確認する必要がある。（東京リージョンは使えた）

`amplify/backend/function/liveness/src`フォルダで`app.js`ファイルを開く。
GET、POST、PUT、DELETE 関数のサンプルがいくつあるが、サンプルは削除できる。
以下のように編集。

```javascript: app.js
// この行追加
const getRekognitionClient = require("./utils/getRekognitionClient");

const express = require('express')
const bodyParser = require('body-parser')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')

// declare a new express app
const app = express()
app.use(bodyParser.json())
app.use(awsServerlessExpressMiddleware.eventContext())

// Enable CORS for all methods
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*")
  res.header("Access-Control-Allow-Headers", "*")
  next()
});

// ここから追加部分

// セッションを生成するエンドポイント
app.get("/session/create", async function (req, res) {
  const rekognition = getRekognitionClient();
  const response = await rekognition.createFaceLivenessSession({});

  // セッションIDを返す
  return res.status(200).json({
    sessionId: response.SessionId,
  });
});

// FaceLivenessの結果を取得して返すエンドポイント
app.get("/session/get", async function (req, res) {
  const rekognition = getRekognitionClient();
  const response = await rekognition.getFaceLivenessSessionResults({
    SessionId: req.query.sessionId,
  });

  // なりすまし検知の点数が90を超えたら検知成功とする
  const isLive = response.Confidence > 90;
  console.log("confidence: ", response.confidence)

  // 成功したかの結果と点数を返す
  res.status(200).json({ "isLive": isLive, "confidence": response.Confidence });
});
```

これでバックエンドは完成なので、プロジェクト直下ディレクトリに戻ってプッシュする。

```bash
$ cd プロジェクト直下ディレクトリ

$ amplify push
```

### Next.js （React）でフロント実装

src/pages/_app.tsxを以下のように編集

```ts: _app.tsx
import type { AppProps } from "next/app";
import { Amplify } from "aws-amplify";
import { withAuthenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import awsExports from "../aws-exports";
Amplify.configure(awsExports);

function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default withAuthenticator(App);
```

次に、`src`ディレクトリの中に`components/Liveness.tsx`を作成する。
このファイルでなりすまし検知の主なUI設計をする。

```ts: Liveness.tsx
import { useEffect, useState } from "react";
import { Loader, Heading } from "@aws-amplify/ui-react";
import { FaceLivenessDetector } from "@aws-amplify/ui-react-liveness";
import { get } from 'aws-amplify/api';

export function LivenessQuickStart() {
    const [loading, setLoading] = useState<boolean>(true);
    const [sessionId, setSessionId] = useState<{
        sessionId: string;
    } | null>(null);
    const [success, setSuccess] = useState('');
    const [confidence, setConfidence] = useState(0);

    useEffect(() => {
        const fetchCreateLiveness = async () => {
        const resrOperation = get({
            apiName: "liveness",
            path: "/session/create"
        });
        const response = await resrOperation.response;
        const data = await response.body.json();
        setSessionId(data);
        setLoading(false);
        };

        fetchCreateLiveness();
    }, []);

    const handleAnalysisComplete = async () => {

        const restOperation = get({
            apiName: "liveness",
            path: "/session/get",
            options: {
                queryParams: {
                    sessionId: sessionId.sessionId,
                }
            }
        });
        const response = await restOperation.response;
        const data = await response.body.json();

        // 結果をstateに格納
        setConfidence(data.confidence)
        console.log("confidence: ", confidence)

        if (data.isLive) {
        setSuccess("認証成功");
        console.log("live");
        } else {
        setSuccess("認証失敗");
        console.log("not live");
        }
    };

    const handleError = (error: Error) => {
        console.log("got error", error);
    };
    // UI
    return (
        <>
        {loading ? (
            <Loader />
        ) : (
            <>
            // あとはインポートしたUIコンポーネントに丸投げ
            <FaceLivenessDetector
                sessionId={sessionId?.sessionId ?? "1"}
                region="ap-northeast-1" 　// 利用しているリージョンを指定
                onAnalysisComplete={handleAnalysisComplete}
                onError={handleError}
            />
            <Heading level={2}>{confidence}%</Heading>
            <Heading level={2}>{success}</Heading>
            </>
        )}
        </>
    );
}
```

最後に、先ほどの`Liveness.tsx`のコンポーネントを表示させるように`index.tsx`をいじる。

```ts: index.tsx

import Head from 'next/head'
import Image from 'next/image'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'

// この二つのimport追加
import { LivenessQuickStart } from "@/components/Liveness";
import { Button, useAuthenticator, View } from "@aws-amplify/ui-react";

const inter = Inter({ subsets: ['latin'] })

export default function Home() {
  
  // ここから追加部分
  const { signOut } = useAuthenticator((context) => [context.signOut]);
  return (
    <>
      <View width="600px" margin="0 auto">
        <LivenessQuickStart />
        <Button onClick={signOut} variation="warning">
          Sign Out
        </Button>
      </View>
  // 追加ここまで（これ以降は消していいかも）
  
      <Head>
        <title>Create Next App</title>
        
        ...(以下省略)
```

# 実行

これで実装は完了したので、実行する。

```bash
$ npm run dev
```

localhost:3000にアクセスして、認証画面が出たらCreate Accountから新しいユーザーを作成する。自分が使っているメールアドレスを使って２段階認証を行う。

それが終わったらいよいよなりすまし検知。
`Begin check`を押して、言われるがまま顔を動かしてみる。結果が表示されたら成功！

動きを指示してくれる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/4c21edc3-057e-4737-d13c-c6358a0e3247.png" width="50%">

結果が表示される。
実際に自分の顔で認証したので成功となる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/74cae5ec-27dd-bbe7-fedd-b1e8507a0a74.png" width="50%">

もし画像で認証しようとすると失敗する。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/ece2233b-c771-0aaa-c8b5-45ffb892709d.png" width="50%">

しゅごい…

# 精度検証

私の顔で検証して現時点でわかっていることは、
- 画像や動画で認証が通ることはまずない（検証では30回くらいやって0）
- スコアが0〜100まであるが、写真や動画だとスコアが0〜1で、リアルの顔だと95以上になり、かなりの差を検出できる
- リアルの顔で認証失敗になることがある
  - マスクをしていたら認証できない
  - 顔の向きが正面でなければ失敗しやすい
  - 変顔をすると失敗しやすい
    - 目をつむるのは大丈夫

# まとめ

これは使えそうです。
UIは提供されているコンポーネントをそのまま使いましたが、UIをいじれたらなお良いと思います。（どこまでいじれるかはまだ調査してませんが。）
今回はReactで開発しましたが、iOSとAndroid用にもコンポーネントが用意されてるらしいです。