【仙山】flutter版谷歌身份驗證器

愛爾蘭真是太好了發表於2019-07-21

google-authenticator-flutter

谷歌身份驗證器Flutter版

github地址

Table of Contents

介紹谷歌驗證器

最近公司使用專案管理gitlab的時候,使用到了兩步驗證器。所以藉著這個機會熟悉一下flutter,就以谷歌身份驗證器這個專案來進行實踐。

步驟實現

以具體的步驟來分析完成。

攝像頭的幀資料

flutter的外掛專案中有camera這個專案,所以直接整合使用即可。

自定義掃碼介面

  • 自定義掃描中間控制元件
    canvas.drawRect(Offset.zero & size, rPaint);
    canvas.drawLine(Offset(0, 2), Offset(20, 2), paint);
    canvas.drawLine(Offset(2, 0), Offset(2, 20), paint);
    canvas.drawLine(Offset(size.width, 2), Offset(size.width - 20, 2), paint);
    canvas.drawLine(
        Offset(size.width - 2, 0), Offset(size.width - 2, 20), paint);
    canvas.drawLine(
        Offset(0, size.height - 2), Offset(20, size.height - 2), paint);
    canvas.drawLine(Offset(2, size.height), Offset(2, size.height - 20), paint);
    canvas.drawLine(Offset(size.width - 2, size.height),
        Offset(size.width - 2, size.height - 20), paint);
    canvas.drawLine(Offset(size.width, size.height - 2),
        Offset(size.width - 20, size.height - 2), paint);
複製程式碼
  • 動畫展示
 controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween(begin: 0.0, end: 200.0).animate(controller)
      ..addListener(() {
        setState(() => {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reset();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();

複製程式碼
  • 使用Align佈局新增自定義控制元件
   Align(
          alignment: FractionalOffset.center,
          child: ScanView(),
        ),
複製程式碼
  • 使用Positioned佈局新增中心四周陰影效果

獲取二維碼資訊

將幀資料傳給端進行解析。以Android為例。

  • flutter層進行資料傳送與接收
 static const MethodChannel _channel =
      const MethodChannel('fairy.e.validator/qrcode');
 static Future<String> loadImageBytes({
    List<Uint8List> bytes,
    int imageWidth = 720,
    int imageHeight = 1280,
  }) async {
    return await _channel.invokeMethod(
      'imageStream',
      {
        "cameraBytes": bytes,
        "width": imageWidth,
        "height": imageHeight,
      },
    );
  }
複製程式碼
  • Android層進行資料接收與傳送
 MethodChannel channel = new MethodChannel(getFlutterView(), QR_CODE_CHANNEL);
        channel.setMethodCallHandler(
                new MethodChannel.MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call,MethodChannel.Result result) {
                        switch (call.method) {
                            case QR_CODE_BYTES:
                                runOnFrame((HashMap) call.arguments, result);
                                break;
                            default:
                                result.notImplemented();
                                break;
                        }
                    }
                });

複製程式碼
  • 楨資料轉換
      List<byte[]> bytesList = (List<byte[]>) args.get("cameraBytes");
            ByteBuffer Y = ByteBuffer.wrap(bytesList.get(0));
            ByteBuffer U = ByteBuffer.wrap(bytesList.get(1));
            ByteBuffer V = ByteBuffer.wrap(bytesList.get(2));
            int Yb = Y.remaining();
            int Ub = U.remaining();
            int Vb = V.remaining();
            byte[] data = new byte[Yb + Ub + Vb];
            Y.get(data, 0, Yb);
            V.get(data, Yb, Vb);
            U.get(data, Yb + Vb, Ub);
複製程式碼
  • 匯入Zxing庫進行解析
  Result rawResult = null;
        PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height, 0, 0,
                width, height);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        try {
            rawResult = multiFormatReader.decodeWithState(bitmap);
        } catch (ReaderException e) {
            e.printStackTrace();
        } finally {
            multiFormatReader.reset();
        }
複製程式碼

獲取資料並儲存

  • 依次解析二維碼資訊
    Uri uri = Uri.parse(result);
    String period =
        isTotp ? uri.queryParameters['period'] : uri.queryParameters['counter'];
    if (null == period) {
      period = isTotp ? '30' : '1';
    } else if (!isTotp) {
      period = (int.parse(period) + 1).toString();
    }
    String secret = uri.queryParameters["secret"];
    if (null == secret) {
      ToastHelper.showToast(context, 'QR碼非法');
      return;
    }
    String path = uri.path;
    if (null == path) {
      ToastHelper.showToast(context, 'QR碼非法');
      return;
    }
    path = path.substring(1);
    String issuer = uri.queryParameters["issuer"];
複製程式碼
  • 資料庫儲存

使用開源儲存庫sqflite進行資料儲存

獲取校驗值

  • 演算法獲取
 Uint8List keys = base32.decode(secret);
    var hmacSha1 = new Hmac(sha1, keys);
    int time = isTotp ? (currentTimeMillis() / 1000) ~/ 30: int.parse(counter);

    Uint8List data = new Uint8List(8);
    int value = time;
    for (int i = 8; i-- > 0; value >>= 8) {
      data[i] = value;
    }
    Uint8List digest = hmacSha1.convert(data).bytes;
    int offset = digest[20 - 1] & 0xF;
    int truncatedHash = 0;
    for (int i = 0; i < 4; ++i) {
      truncatedHash <<= 8;
      truncatedHash |= (digest[offset + i] & 0xFF);
    }
    truncatedHash &= 0x7FFFFFFF;
    truncatedHash %= 1000000;
    print("結果為${truncatedHash.toString()}");
複製程式碼
  • 值填充
 static String getNumberHash(String number){
    for(int i=number.length;i<6;i++){
      number="0"+number;
    }
    return number;
  }
複製程式碼

頁面展示

  • ListTile控制元件展示
    ListTile(
            contentPadding:
                EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
            trailing: data.isTotp ? _totpIcon() : _hotpIcon(data.id),
            title: Text(
              Utils.getNumber(data.secret, data.isTotp, data.period),
              style: styleNumber,
            ),
            subtitle: Text(
              Utils.getPath(data.path, data.issuer),
              style: styleSubtitle,
            ),
            onTap: () => {_hotpTap(data)},
            onLongPress: () => {_show(data)},
          ),
複製程式碼
  • 基於時間動畫效果
    double start = 2 * pi * ((30 - time) / 30);
    controller =
        AnimationController(duration: Duration(seconds: time), vsync: this);
    animation = Tween(begin: start, end: 2 * pi).animate(controller)
      ..addListener(() {
        setState(() => {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          viewListener.onSuccess();
          controllerTotp = AnimationController(
              duration: const Duration(seconds: 30), vsync: this);
          animation = Tween(begin: 0.0, end: 2 * pi).animate(controllerTotp)
            ..addListener(() {
              setState(() => {});
            })
            ..addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                controllerTotp.reset();
                viewListener.onSuccess();
              } else if (status == AnimationStatus.dismissed) {
                controllerTotp.forward();
              }
            });
          controllerTotp.forward();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
複製程式碼
  • 基於計數器動畫效果

以Map儲存相關id與Timer。實現點選單個響應單個的效果。

  if (!valueMap.containsKey(data.id) && !data.isTotp) {
      startCountdownTimer(data.id);
    }
 void startCountdownTimer(id) {
    Timer timer = Timer(new Duration(seconds: 5), () {
      print("倒數計時完成");
      setState(() {
        valueMap.remove(id);
      });
    });
    setState(() {
      valueMap[id] = timer;
    });
  }
複製程式碼

實現效果

點選檢視

掃碼體驗

點選檢視

亟待解決

ios的資料解析,還有待實現。

相關文章