Flutter身份认证插件flutter_libjeid的使用

Flutter身份认证插件flutter_libjeid的使用

LibJeID(日本电子身份证库)是一个用于智能手机访问公共IC卡(如个人编号卡、驾照和护照)的库。这些卡在日本非常流行。

logo

特性

目前我们正在使用libjeid的免费版本,因此无需任何产品库密钥即可安装。付费版本正在开发中。

官方文档

官方文档

Android版本功能列表

  • 读取个人编号卡
  • 个人编号卡的公共个人认证功能
  • 读取居住卡和特别永久居民证书
  • 验证居住卡和特别永久居民证书的真实性
  • 读取驾照
  • 驾照真实性验证

iOS版本功能列表

  • 读取个人编号卡
  • 读取居住卡和特别永久居民证书
  • 验证居住卡和特别永久居民证书的真实性
  • 读取驾照
  • 驾照真实性验证

免费版和付费版的功能项

available_data

如何使用

前往示例项目查看如何使用该库的完整示例。

首先创建一个FlutterLibjeid实例,并检查NFC是否可用,然后订阅事件:

final _scanner = FlutterLibjeid();

final isNfcAvailable = await _scanner.isAvailable();

if (!isNfcAvailable) {
  // 显示错误信息
  return;
}

final _subscription = _scanner.eventStream.listen((event) {
  switch (event) {
    case FlutterLibjeidEventScanning():
      // 使用 .setMessage() 在扫描对话框中显示消息
      _scanner.setMessage(message: 'Scanning...');
      break;

    case FlutterLibjeidEventConnecting():
      _scanner.setMessage(message: 'Connecting...');
      break;

    case FlutterLibjeidEventParsing():
      _scanner.setMessage(message: 'Parsing...');
      break;

    case FlutterLibjeidEventSuccess(data: FlutterLibjeidCardData data):
      // 使用 .stopScan() 取消扫描过程
      _scanner.stopScan();
      _onSuccess(data);
      break;

    case FlutterLibjeidEventFailed(error: FlutterLibjeidError error):
      _scanner.stopScan();
      _onError(error);
      break;

    case FlutterLibjeidEventCancelled():
      break;
  }
});

然后处理数据或错误:

void _onSuccess(FlutterLibjeidCardData data) {
  switch (data) {
    case ResidentCardData():
      // 对数据进行操作
      break;

    case MyNumberCardData():
      // 对数据进行操作
      break;

    case DriverLicenseCardData():
      // 对数据进行操作
      break;

    case PassportCardData():
      // 对数据进行操作
      break;
  }
}

void _onError(FlutterLibjeidError error) {
  switch (error) {
    case NfcNotAvailableError():
      // 对错误进行操作
      break;

    case NfcTagUnableToConnectError():
      // 对错误进行操作
      break;

    case NfcCardBlockedError():
      // 对错误进行操作
      break;

    case NfcCardTypeMismatchError():
      // 对错误进行操作
      break;

    case InvalidMethodArgumentsError():
      // 对错误进行操作
      break;

    case InvalidCardPinError(int remainingTimes):
      // 对错误进行操作
      break;

    case InvalidCardKeyError():
      // 对错误进行操作
      break;

    case UnknownError():
      // 对错误进行操作
      break;
  }
}

接下来,调用您要使用的扫描方法:

await _scanner.scanResidentCard(cardNumber: 'xxxxxxxxxxxx');

await _scanner.scanMyNumberCard(pin: 'xxxx');

await _scanner.scanDriverLicenseCard(pin1: 'xxxx', pin2: 'xxxx');

await _scanner.scanPassportCard(cardNumber: 'xxxxxxxxxx', birthDate: 'xxxxxxxxxx', expiredDate: 'xxxxxxxxxx');

最后,不再需要时取消事件流订阅:

_subscription.cancel();

连接丢失问题

有几种原因可能导致标签连接丢失:

  • iPhone对位置非常敏感,即使在读取过程中卡片稍微移动也可能导致“标签连接丢失”。
  • 不同设备的可读性存在差异。我们还确认了一些设备几乎无法读取卡片。即使是同一型号,也可能有个体差异。
  • 读取卡片时尽量保持卡片靠近标签,不要移动它。

问题已在官方仓库报告:https://github.com/osstech-jp/libjeid-ios-app/issues/1

贡献

欢迎为该项目贡献代码。

如果您发现了一个错误或希望实现一个功能,但不知道如何修复/实现,请填写一个问题。 如果您修复了一个错误或实现了某个功能,请发送一个拉取请求。

示例代码

import 'dart:convert';

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

import 'package:flutter/services.dart';
import 'package:flutter_libjeid/flutter_libjeid.dart';

void main() {
  runApp(const MyApp());
}

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

  [@override](/user/override)
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Map<String, dynamic> _data = {};
  String? _error;
  final GlobalKey<ScaffoldState> _globalKey = GlobalKey();

  final _flutterLibjeidPlugin = FlutterLibjeid();
  final TextEditingController _rcCardNumberController = TextEditingController();
  final TextEditingController _inCardPinController = TextEditingController();
  final TextEditingController _dlCardPin1Controller = TextEditingController();
  final TextEditingController _dlCardPin2Controller = TextEditingController();
  final TextEditingController _epCardNumberController = TextEditingController();
  final TextEditingController _epCardBirthDateController = TextEditingController();
  final TextEditingController _epCardExpiredDateController = TextEditingController();

  late final TabController _tabController;
  late final StreamSubscription<FlutterLibjeidEvent> _eventStreamSubscription;

  [@override](/user/override)
  void initState() {
    _tabController = TabController(length: 4, vsync: this);
    _eventStreamSubscription = _flutterLibjeidPlugin.eventStream.listen(_onEventReceived);

    scheduleMicrotask(() {
      _flutterLibjeidPlugin.isAvailable().then((isAvailable) {
        if (!isAvailable) {
          setState(() => _error = 'NFC Not Available');
        }
      });
    });
    super.initState();
  }

  [@override](/user/override)
  void dispose() {
    _eventStreamSubscription.cancel();
    super.dispose();
  }

  void _onEventReceived(FlutterLibjeidEvent event) {
    switch (event) {
      case FlutterLibjeidEventScanning():
        _flutterLibjeidPlugin.setMessage(message: 'Scanning...');
        setState(() {
          _data = Map.from({});
          _error = null;
        });
        break;

      case FlutterLibjeidEventConnecting():
        _flutterLibjeidPlugin.setMessage(message: 'Connecting...');
        break;

      case FlutterLibjeidEventParsing():
        _flutterLibjeidPlugin.setMessage(message: 'Parsing...');
        break;

      case FlutterLibjeidEventSuccess(data: FlutterLibjeidCardData data):
        _flutterLibjeidPlugin.stopScan();

        _data = data.toJson();

        FocusScope.of(context).unfocus();

        _globalKey.currentState?.showBottomSheet(
          (_) => BottomSheet(
            onClosing: () {},
            constraints: BoxConstraints(
              maxHeight: MediaQuery.of(context).size.height * 0.9,
            ),
            builder: (_) => SingleChildScrollView(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: _data.entries
                    .map(
                      (e) => Container(
                        padding: const EdgeInsets.symmetric(vertical: 10),
                        decoration: BoxDecoration(
                          border: Border(
                            bottom: BorderSide(color: Colors.grey[200]!),
                          ),
                        ),
                        child: Row(
                          children: [
                            Expanded(flex: 2, child: Text(e.key)),
                            Expanded(
                              flex: 3,
                              child: Builder(builder: (context) {
                                final value = e.value;
                                if (value is String && value.startsWith('data:image/')) {
                                  return Image.memory(
                                    const Base64Decoder().convert(
                                      value.replaceFirst(
                                          'data:image/png;base64,', ''),
                                    ),
                                  );
                                }
                                return Text(e.value.toString());
                              }),
                            )
                          ],
                        ),
                      ),
                    )
                    .toList(),
              ),
            ),
          ),
        );

        setState(() {});
        break;

      case FlutterLibjeidEventFailed(error: FlutterLibjeidError error):
        _flutterLibjeidPlugin.stopScan();
        setState(() => _error = error.toString());
        break;

      case FlutterLibjeidEventCancelled():
        break;
    }
  }

  Future<void> startScanResidentCard() async {
    try {
      await _flutterLibjeidPlugin.scanResidentCard(
        cardNumber: _rcCardNumberController.text,
      );
    } catch (e) {
      setState(() => _error = e.toString());
    }
  }

  Future<void> scanMyNumberCard() async {
    try {
      await _flutterLibjeidPlugin.scanMyNumberCard(
        pin: _inCardPinController.text,
      );
    } catch (e) {
      setState(() => _error = e.toString());
    }
  }

  Future<void> startScanDriverLicenseCard() async {
    try {
      await _flutterLibjeidPlugin.scanDriverLicenseCard(
        pin1: _dlCardPin1Controller.text,
        pin2: _dlCardPin2Controller.text,
      );
    } catch (e) {
      setState(() => _error = e.toString());
    }
  }

  Future<void> startScanPassportCard() async {
    try {
      await _flutterLibjeidPlugin.scanPassportCard(
        cardNumber: _epCardNumberController.text,
        birthDate: _epCardBirthDateController.text,
        expiredDate: _epCardExpiredDateController.text,
      );
    } catch (e) {
      setState(() => _error = e.toString());
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GestureDetector(
        onTap: () => FocusScope.of(context).unfocus(),
        child: Scaffold(
          key: _globalKey,
          appBar: AppBar(
            systemOverlayStyle: SystemUiOverlayStyle.dark,
            title: const Text('Libjeid Example'),
          ),
          body: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                const Text(
                  '该插件仍在开发中,可能会导致应用程序崩溃。保持冷静 ^_^',
                  style: TextStyle(
                    fontStyle: FontStyle.italic,
                  ),
                ),
                const SizedBox(height: 16),
                TabBar(
                  controller: _tabController,
                  labelStyle: const TextStyle(fontSize: 16),
                  labelColor: Colors.blue,
                  isScrollable: true,
                  tabs: const [
                    SizedBox(height: 30, child: Text('我的编号 (IN)')),
                    SizedBox(height: 30, child: Text('驾驶执照 (DL)')),
                    SizedBox(height: 30, child: Text('居住卡 (RC)')),
                    SizedBox(height: 30, child: Text('护照 (EP)')),
                  ],
                  onTap: (index) {
                    setState(() {
                      _data = Map.from({});
                      _error = null;
                    });
                  },
                ),
                const SizedBox(height: 10),
                Expanded(
                  child: TabBarView(
                    controller: _tabController,
                    children: [
                      Column(
                        children: [
                          TextFormField(
                            controller: _inCardPinController,
                            maxLength: 4,
                            decoration: const InputDecoration(
                              hintText: '输入PIN',
                            ),
                          ),
                          const SizedBox(height: 10),
                          ElevatedButton(
                            onPressed: scanMyNumberCard,
                            child: const Text('扫描我的编号卡'),
                          ),
                        ],
                      ),
                      Column(
                        children: [
                          Row(
                            children: [
                              Expanded(
                                child: TextFormField(
                                  controller: _dlCardPin1Controller,
                                  maxLength: 4,
                                  decoration: const InputDecoration(
                                    hintText: '输入PIN 1',
                                  ),
                                ),
                              ),
                              const SizedBox(width: 12),
                              Expanded(
                                child: TextFormField(
                                  controller: _dlCardPin2Controller,
                                  maxLength: 4,
                                  decoration: const InputDecoration(
                                    hintText: '输入PIN 2',
                                  ),
                                ),
                              )
                            ],
                          ),
                          const SizedBox(height: 10),
                          ElevatedButton(
                            onPressed: startScanDriverLicenseCard,
                            child: const Text('扫描驾驶执照卡'),
                          ),
                        ],
                      ),
                      Column(
                        children: [
                          TextFormField(
                            maxLength: 12,
                            controller: _rcCardNumberController,
                            decoration: const InputDecoration(
                              hintText: '输入卡片号码',
                            ),
                          ),
                          const SizedBox(height: 10),
                          ElevatedButton(
                            onPressed: startScanResidentCard,
                            child: const Text('扫描居住卡'),
                          ),
                        ],
                      ),
                      Column(
                        children: [
                          TextFormField(
                            maxLength: 12,
                            controller: _epCardNumberController,
                            decoration: const InputDecoration(
                              hintText: '输入卡片号码',
                            ),
                          ),
                          TextFormField(
                            maxLength: 12,
                            controller: _epCardBirthDateController,
                            decoration: const InputDecoration(
                              hintText: '输入出生日期 (YYMMDD)',
                            ),
                          ),
                          TextFormField(
                            maxLength: 12,
                            controller: _epCardExpiredDateController,
                            decoration: const InputDecoration(
                              hintText: '输入到期日期 (YYMMDD)',
                            ),
                          ),
                          const SizedBox(height: 10),
                          ElevatedButton(
                            onPressed: startScanPassportCard,
                            child: const Text('扫描护照卡'),
                          ),
                        ],
                      )
                    ],
                  ),
                ),
                const SizedBox(height: 20),
                if (_error?.isNotEmpty ?? false)
                  Text(
                    '错误: $_error',
                    style: const TextStyle(color: Colors.red),
                  ),
                Expanded(
                  child: SingleChildScrollView(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: _data.entries
                          .map(
                            (e) => Container(
                              padding: const EdgeInsets.symmetric(vertical: 10),
                              decoration: BoxDecoration(
                                border: Border(
                                  bottom: BorderSide(color: Colors.grey[200]!),
                                ),
                              ),
                              child: Row(
                                children: [
                                  Expanded(flex: 2, child: Text(e.key)),
                                  Expanded(
                                    flex: 3,
                                    child: Builder(builder: (context) {
                                      final value = e.value;
                                      if (value is String && value.startsWith('data:image/')) {
                                        return Image.memory(
                                          const Base64Decoder().convert(
                                            value.replaceFirst(
                                                'data:image/png;base64,', ''),
                                          ),
                                        );
                                      }
                                      return Text(e.value.toString());
                                    }),
                                  )
                                ],
                              ),
                            ),
                          )
                          .toList(),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

更多关于Flutter身份认证插件flutter_libjeid的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter身份认证插件flutter_libjeid的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


flutter_libjeid 是一个用于读取日本身份证(My Number Card)信息的 Flutter 插件。它基于 libjeid 库,允许开发者从日本的身份证中读取个人信息、照片和其他数据。以下是如何在 Flutter 项目中使用 flutter_libjeid 插件的基本步骤。

1. 添加依赖

首先,在 pubspec.yaml 文件中添加 flutter_libjeid 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_libjeid: ^1.0.0  # 请使用最新版本

然后运行 flutter pub get 以安装依赖。

2. 导入插件

在你的 Dart 文件中导入 flutter_libjeid 插件:

import 'package:flutter_libjeid/flutter_libjeid.dart';

3. 初始化插件

在使用插件之前,确保你已经初始化了插件。通常情况下,你可以在 main() 函数中进行初始化:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterLibjeid.initialize();
  runApp(MyApp());
}

4. 读取身份证信息

使用 FlutterLibjeid 提供的 API 来读取身份证信息。你可以通过调用 readCard() 方法来读取身份证数据。

void readMyNumberCard() async {
  try {
    MyNumberCardData cardData = await FlutterLibjeid.readCard();
    print('Name: ${cardData.name}');
    print('Address: ${cardData.address}');
    print('Photo: ${cardData.photo}');
  } catch (e) {
    print('Failed to read card: $e');
  }
}

5. 处理读取到的数据

MyNumberCardData 类包含了从身份证中读取的所有信息。你可以根据需要处理这些数据。

class MyNumberCardData {
  String name;
  String address;
  Uint8List photo;
  // 其他字段...

  MyNumberCardData({
    required this.name,
    required this.address,
    required this.photo,
    // 其他字段...
  });
}

6. 显示照片

如果你想要在应用中显示从身份证中读取的照片,可以使用 Image.memory 组件:

Image.memory(cardData.photo);

7. 处理错误

在读取身份证信息的过程中,可能会遇到各种错误,例如设备不支持 NFC 或用户未授权访问。确保在你的代码中正确处理这些错误。

try {
  MyNumberCardData cardData = await FlutterLibjeid.readCard();
  // 处理数据
} on PlatformException catch (e) {
  print('PlatformException: ${e.message}');
} catch (e) {
  print('Unknown error: $e');
}

8. 请求权限

在读取身份证信息之前,你可能需要请求 NFC 权限。可以使用 permission_handler 插件来处理权限请求。

import 'package:permission_handler/permission_handler.dart';

void requestPermissions() async {
  if (await Permission.nfc.request().isGranted) {
    // 权限已授予
  } else {
    // 权限被拒绝
  }
}

9. 注意事项

  • 确保设备支持 NFC 功能。
  • 用户需要在设备的设置中启用 NFC。
  • 读取身份证信息可能需要用户的明确授权。

10. 示例代码

以下是一个完整的示例代码,展示了如何使用 flutter_libjeid 插件读取和显示身份证信息:

import 'package:flutter/material.dart';
import 'package:flutter_libjeid/flutter_libjeid.dart';
import 'dart:typed_data';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterLibjeid.initialize();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('My Number Card Reader'),
        ),
        body: Center(
          child: MyNumberCardReader(),
        ),
      ),
    );
  }
}

class MyNumberCardReader extends StatefulWidget {
  [@override](/user/override)
  _MyNumberCardReaderState createState() => _MyNumberCardReaderState();
}

class _MyNumberCardReaderState extends State<MyNumberCardReader> {
  MyNumberCardData? cardData;

  void readCard() async {
    try {
      MyNumberCardData data = await FlutterLibjeid.readCard();
      setState(() {
        cardData = data;
      });
    } catch (e) {
      print('Failed to read card: $e');
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (cardData != null)
          Column(
            children: [
              Text('Name: ${cardData!.name}'),
              Text('Address: ${cardData!.address}'),
              Image.memory(cardData!.photo),
            ],
          ),
        ElevatedButton(
          onPressed: readCard,
          child: Text('Read Card'),
        ),
      ],
    );
  }
}
回到顶部