🔐

【Flutter】アプリ起動時に生体認証をする

2023/12/29に公開

はじめに

私がよく見かける(と思ってる)、アプリを開くと同時に生体認証を求められる機能を実装しようと思います。

とは言っても、認証機能自体はほとんどドキュメントコピペでよくて、どうナビゲーションするかに少し悩みました。

実装

パッケージはlocal_authpermisiion_handlerを使います。

準備

iOSならInfo.plistNSFaceIDUsageDescriptionを追加。

<key>NSFaceIDUsageDescription</key>
<string>使わせてね</string>

AndroidならAndroidManifest.xmlUSE_BIOMETRICを追加し、

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>

MainActivity.ktの中身を変更します。

- import io.flutter.embedding.android.FlutterActivity
+ import io.flutter.embedding.android.FlutterFragmentActivity

- class MainActivity: FlutterActivity() {}
+ class MainActivity: FlutterFragmentActivity() {}

コード

まず認証を行うページを作ります。
こんな感じです。
ドキュメントからコピペしてちょっといじっただけです。

local_auth_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';
import 'package:permission_handler/permission_handler.dart';

class LocalAuthPage extends StatefulWidget {
  const LocalAuthPage({super.key});

  
  State<LocalAuthPage> createState() => _LocalAuthPageState();
}

class _LocalAuthPageState extends State<LocalAuthPage> {
  final LocalAuthentication auth = LocalAuthentication();
  bool _canCheckBiometrics = false;

  Future<void> _checkBiometrics() async {
    late bool canCheckBiometrics;
    try {
      canCheckBiometrics = await auth.canCheckBiometrics;
    } on PlatformException catch (e) {
      canCheckBiometrics = false;
      debugPrint(e.toString());
    }
    if (!mounted) {
      return;
    }

    setState(() {
      _canCheckBiometrics = canCheckBiometrics;
    });
  }

  Future<void> _authenticate() async {
    bool authenticated = false;
    try {
      authenticated = await auth.authenticate(
        localizedReason: '生体認証するよ',
        options: const AuthenticationOptions(
          stickyAuth: true,
        ),
      );
    } on PlatformException catch (e) {
      debugPrint(e.toString());
      return;
    }
    if (!mounted) {
      return;
    }

    if (authenticated) {
      backToPreviousPage();
    }
  }

  void backToPreviousPage() {
    Navigator.pop(context);
  }

  
  void initState() {
    super.initState();
    _authenticate();
    _checkBiometrics();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('生体認証してね'),
            ElevatedButton(
                onPressed: () => _authenticate(), child: const Text("する")),
            if (!_canCheckBiometrics)
              ElevatedButton(
                  onPressed: () => openAppSettings(),
                  child: const Text('オンにする'))
          ],
        ),
      ),
    );
  }
}

以下の部分で、認証が成功した際に前のページに戻るようにしています。

if (authenticated) {
  backToPreviousPage();
}

次に、生体認証ページに飛ぶ前のページです。
親の顔より見たカウンターアプリにナビゲーションを追加しただけです。

counter_page.dart
import 'package:flutter/material.dart';

import 'local_auth_page.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({super.key, required this.title});

  final String title;

  
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  void initState() {
    super.initState();
    Future<void>(() => Navigator.push(
        context, MaterialPageRoute(builder: (_) => const LocalAuthPage())));
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

initState()内でNavigator.push()はできないので、build()後に実行されるようにしていますが、このやり方が正解なのかは分かりません。

Future<void>(() => Navigator.push(
        context, MaterialPageRoute(builder: (_) => const LocalAuthPage())));

iOSのviewDidAppear()みたいなのがあればそこで実行したかったのですが、Flutterにはこれに値するものがなさそうです。

動作

iOS

こんな感じで動きます。
iOSの動作
iOSのシミュレータでFace IDを使うには、Features -> Face ID -> Enrolledでオンにして、認証時にMatching Faceで認証成功、Non-matching Faceで認証失敗できます。

Touch IDでも同様です。

Android

同様に動きます。
Androidの動作
Androidエミュレータは、設定のSecurity & Privacy -> Device unlockで指紋を登録できます。

登録する指紋は、エミュレータの上にある3点リーダーを押して出てくる画面で選べます。
Touch Sensorを押すと、選択している指紋で画面をタップしてくれます。
指紋認証をする時もこの画面で指紋を使います。

終わりに

公式がパッケージを用意してくれているおかげで、楽でいいですね。

Discussion