Flutter × Firebase App Check + Cloud Functions で環境構築する手順の解説

Flutter

1. プロジェクト作成と環境準備

手順

  1. プロジェクト作成:
    • flutter create my_app_name (スネークケースで!)
  2. Cursor (VS Code) での準備:
    • .dart ファイルを開かないと右下のステータスバーにFlutterのバージョンやデバイスが出ない(罠)。
  3. iOSシミュレーター起動:
    • リストになければ open -a Simulator で強制起動。
    • または xcrun simctl list devices available で確認。

💡 ハマりポイント & 疑問

  • Q: Hot Reloadが効かない?
    • A: const がついていると更新されない。親の const を削除するか、Hot Restart (緑の矢印) を使う。
  • Q: APIキー(Geminiなど)はアプリ内に書いていい?
    • A: 絶対ダメ。 リバースエンジニアリングでバレる。サーバー(Cloud Functions)経由にするのが定石。

2. Firebaseのセットアップ

手順

  1. Firebase CLIツールのインストール:
    • sudo npm install -g firebase-tools --force (権限エラーが出たら sudo--force でねじ伏せる)
    • firebase login
  2. FlutterFire CLIの準備:
    • dart pub global activate flutterfire_cli (これはDart製のツールを入れるコマンド)
  3. Flutterと接続:
    • flutterfire configure (これで lib/firebase_options.dart が自動生成される)

3. Cloud Functions (サーバー側) の実装

手順

  1. 初期化:
    • firebase init functions (TypeScript推奨)
  2. Node.jsバージョンの修正 (重要エラー):
    • 初期設定で Node.js 24 が指定されることがあるが、Cloud Functions (Gen 1) は未対応。
    • functions/package.jsonengines"20" に書き換える。
  3. コード実装 (index.ts):
    • import * as functions from "firebase-functions/v1"; (v1を明示)
    • onCall を使い、if (context.app == undefined) でApp Checkの検証を行う。
  4. デプロイ:
    • firebase deploy --only functions

コードの例

import * as functions from "firebase-functions/v1";

// 日本リージョン(tokyo)を指定すると速い
export const helloWorld = functions
  .region('asia-northeast1')
  .https.onCall((data, context) => {

    // ---------------------------------------------------------
    // 🛡️ セキュリティチェック (App Check)
    // context.app が undefined の場合、App Check のトークンを持っていない不正アクセスとみなす
    // ---------------------------------------------------------
    if (context.app == undefined) {
      throw new functions.https.HttpsError(
        'failed-precondition',
        'The function must be called from an App Check verified app.'
      );
    }

    // ---------------------------------------------------------
    // 📝 ここに本来の処理を書く (AI APIを叩くなど)
    // ---------------------------------------------------------
    console.log("App Check verified! User ID:", context.auth?.uid);
    
    return {
      message: "App Check成功!これはセキュアな通信です。",
      status: "success"
    };
});

4. Flutter (アプリ側) の実装とiOSの壁

手順

  1. パッケージ追加:
    • flutter pub add firebase_core firebase_app_check cloud_functions
  2. iOSバージョンの引き上げ (重要エラー):
    • Cloud Functionsなどの最新プラグインは iOS 15.0以上 が必須。
    • ios/Podfileplatform :ios, '15.0' のコメントアウトを外して有効化。
    • その後 cd ios -> pod install --repo-update

💡 疑問

  • Q: Podfileだけ変えて、Xcodeの設定(project.pbxproj)はそのままでいいの?
    • A: 開発中は動くのでOK。ただしリリース時はXcode側の設定(Deployment Target)も15.0に合わせるのが正解(古いOSの人がDLできないようにするため)。
    • Deployment TargetがAppStoreでインストールできるiOSのバージョンの指定となる

コードの例

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'firebase_options.dart'; // flutterfire configure で生成されたファイル

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 1. Firebase初期化
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // 2. App Check有効化
  // シミュレーター開発中は debug プロバイダーを使う
  await FirebaseAppCheck.instance.activate(
    // Androidエミュレーター用
    androidProvider: AndroidProvider.debug,
    // iOSシミュレーター用 (ここが重要!)
    appleProvider: AppleProvider.debug, 
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App Check Demo',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  String _response = "ボタンを押してテスト";
  bool _isLoading = false;

  Future<void> _callFunction() async {
    setState(() {
      _isLoading = true;
      _response = "通信中...";
    });

    // 🔍 【デバッグ用】トークンが取れているか確認するログ
    // エラーが出たらこのログを見て、FirebaseコンソールにUUIDを登録し直す
    try {
      final token = await FirebaseAppCheck.instance.getToken(true);
      print("APP CHECK TOKEN: $token"); // ← これがログに出ればOK
    } catch (e) {
      print("APP CHECK TOKEN ERROR: $e");
    }

    try {
      // 3. Cloud Functionsを呼び出す
      // ※ regionは functions/src/index.ts で指定したものと合わせる
      final result = await FirebaseFunctions.instanceFor(region: 'asia-northeast1')
          .httpsCallable('helloWorld')
          .call();
      
      setState(() {
        _response = result.data['message'];
      });
      
    } on FirebaseFunctionsException catch (e) {
      // Functions特有のエラー (App Check失敗など)
      setState(() {
        _response = "Functionsエラー: [${e.code}] ${e.message}";
      });
      print("Functions Error: ${e.code} / ${e.message}");
      
    } catch (e) {
      // その他のエラー (ネットワーク切れなど)
      setState(() {
        _response = "予期せぬエラー: $e";
      });
      print("General Error: $e");
      
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("App Check Test")),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                _response,
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 16),
              ),
              const SizedBox(height: 20),
              if (_isLoading)
                const CircularProgressIndicator()
              else
                ElevatedButton(
                  onPressed: _callFunction,
                  child: const Text("Cloud Functionsを叩く"),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

5. App Check トークンの登録 (最難関)

現象

アプリからFunctionsを叩くと failed-precondition エラーが出る。

解決手順

  1. デバッグトークンを探す:
    • Cursorのデバッグコンソールに出ない場合は、Xcode (open ios/Runner.xcworkspace) から実行してシステムログを見る。
    • Firebase App Check: Enter this debug secret... というUUIDを探す。
  2. Firebaseコンソールに登録:
    • 「App Check」→「アプリ」→「iOS」→「デバッグトークンを管理」に貼り付け。
  3. App Check APIの有効化 (忘れがち):
    • Google Cloud Console で「Firebase App Check API」を ENABLE にする。
  4. シミュレーターの区別:
    • バージョン違い(例:iOS 18.018.2) は別端末扱い。それぞれトークンが違うので注意。

6. セキュリティの最終確認

手順

  1. 正常系: アプリのボタンを押して「成功」と出るか。
  2. 攻撃シミュレーション:
    • ターミナルから curl (POST) でトークンなしのリクエストを送る。
    • 403 ForbiddenFAILED_PRECONDITION が返ってくれば、「URLがバレても攻撃を防げている」 という証明になる。
# 攻撃コマンド例
curl -X POST -H "Content-Type: application/json" \
     -d '{"data": {}}' \
     https://[リージョン]-[プロジェクト].cloudfunctions.net/[関数名]

コメント

タイトルとURLをコピーしました