Flutter:极验一键获取手机号

内容分享21小时前发布
0 0 0

极验一键获取手机号:插件地址
官方文档:安卓文档
ios文档

1、封装一键获取手机号(appid请自行到极验官网申请)


import 'dart:io';
import 'package:get/get.dart';
import 'package:gt_onelogin_flutter_plugin/gt_onelogin_flutter_plugin.dart';
/// 极验一键登录服务
class OneLoginService extends GetxService {
  static OneLoginService get to => Get.find();

  // 极验 AppID(根据平台动态获取)
  static String get appId {
    if (Platform.isIOS) {
      return '********';  // iOS 使用 iOS 的 AppID
    } else {
      return '********';     // Android 使用 Android 的 AppID
    }
  }
  
  // SDK 实例
  late GtOneloginFlutterPlugin _plugin;

  /// 初始化 SDK
  Future<void> init() async {
    try {
      // 根据平台使用不同的 AppID
      final currentAppId = appId;
      print('当前平台: ${Platform.isIOS ? "iOS" : "Android"}');
      print('使用 AppID: $currentAppId');
      
      // 通过构造函数初始化
      _plugin = GtOneloginFlutterPlugin(currentAppId, 10000); // 10秒超时
      
      // 开启日志(可选,调试时使用)
      _plugin.setLogEnable(true);
      
      print('极验一键登录 SDK 初始化成功');
    } catch (e) {
      print('极验一键登录 SDK 初始化失败: $e');
    }
  }

  /// 预取号(加速登录)
  /// 建议在进入登录页面前调用
  /// 注意:SDK 初始化后会自动预取号,此方法用于重新预取号
  /// 
  /// [timeout] 超时时间(毫秒),默认 10000ms(10秒)
  /// [checkInterval] 检查间隔(毫秒),默认 500ms
  /// 
  /// 返回 Map:
  /// - success: bool 是否成功
  /// - message: String 错误信息(如果失败)
  Future<Map<String, dynamic>> preGetToken({
    int timeout = 10000,
    int checkInterval = 500,
  }) async {
    try {
      // 触发重新预取号(异步操作)
      _plugin.renewPreGetToken();
      
      print('已触发预取号,等待完成...');
      final startTime = DateTime.now();
      
      // 等待预取号完成(根据网络情况,可能需要 2-10 秒)
      final maxAttempts = timeout ~/ checkInterval;
      
      for (int i = 0; i < maxAttempts; i++) {
        await Future.delayed(Duration(milliseconds: checkInterval));
        
        // 检查预取号是否完成
        final isAvailable = await _plugin.isAvailable();
        if (isAvailable) {
          final duration = DateTime.now().difference(startTime).inMilliseconds;
          print('✅ 预取号成功(耗时: ${duration}ms)');
          return {
            'success': true,
            'duration': duration,
          };
        }
        
        // 每秒输出一次进度
        if ((i + 1) % 2 == 0) {
          print('预取号进行中... ${(i + 1) * checkInterval}ms');
        }
      }
      
      // 超时 - 给出详细的错误信息
      print('❌ 预取号超时(${timeout}ms 内未完成)');
      print('可能原因:');
      print('1. 网络信号较差');
      print('2. SIM卡状态异常(停机、欠费)');
      print('3. 运营商服务响应慢');
      print('4. 使用了虚拟运营商');
      
      return {
        'success': false,
        'errorCode': '-20105', // 超时错误码
        'message': '预取号超时(${timeout}ms)',
        'detail': '网络信号较差或SIM卡状态异常',
      };
    } catch (e) {
      print('❌ 预取号异常: $e');
      return {
        'success': false,
        'errorCode': '-20999',
        'message': '预取号异常',
        'detail': e.toString(),
      };
    }
  }
  
  /// 检查预取号是否有效
  /// 在调用 requestToken 前可以先检查
  Future<bool> checkPreGetTokenValid() async {
    try {
      return await _plugin.isAvailable();
    } catch (e) {
      print('检查预取号状态失败: $e');
      return false;
    }
  }

  /// 一键登录
  /// 返回完整的结果对象,包含 token、process_id、app_id 等
  /// 
  /// 成功返回示例:
  /// {
  ///   'status': 200,
  ///   'token': 'xxx',
  ///   'process_id': 'xxx',
  ///   'app_id': 'xxx',
  ///   'authcode': 'xxx', // 可选,仅电信
  ///   'msg': 'xxx'
  /// }
  /// 
  /// 失败返回示例:
  /// {
  ///   'status': 500,
  ///   'errorCode': 'xxx',
  ///   'msg': 'xxx'
  /// }
  Future<Map<String, dynamic>?> requestToken([OLUIConfiguration? uiConfig]) async {
    try {
      // 如果没有传入UI配置,使用默认配置
      uiConfig ??= OLUIConfiguration();

      final result = await _plugin.requestToken(uiConfig);
      
      print('========== 一键登录返回结果 ==========');
      print('原始结果: $result');

      // 返回完整的结果对象
      return result;
    } catch (e) {
      print('一键登录异常: $e');
      return null;
    }
  }

  /// 关闭授权页面
  void closeAuthPage() {
    try {
      _plugin.dismissAuthView();
    } catch (e) {
      print('关闭授权页面失败: $e');
    }
  }

  /// 检查网络环境
  /// 返回当前运营商类型
  Future<OLCarrierType?> getCurrentCarrier() async {
    try {
      return await _plugin.getCurrentCarrier();
    } catch (e) {
      print('获取运营商信息失败: $e');
      return null;
    }
  }

  /// 获取当前网络类型
  Future<OLNetworkInfo?> getCurrentNetworkInfo() async {
    try {
      return await _plugin.getCurrentNetworkInfo();
    } catch (e) {
      print('获取网络信息失败: $e');
      return null;
    }
  }

  /// 检查预取号token是否还有效
  Future<bool> isTokenAvailable() async {
    try {
      return await _plugin.isAvailable();
    } catch (e) {
      print('检查token有效性失败: $e');
      return false;
    }
  }

  /// 销毁SDK(仅Android)
  void destroy() {
    try {
      _plugin.destroy();
    } catch (e) {
      print('销毁SDK失败: $e');
    }
  }
}

2、全局初始化


import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'common/index.dart';

class Global {
  static Future<void> init() async {
    // 插件初始化
    WidgetsFlutterBinding.ensureInitialized();
    
    // 初始化极验一键登录
    Get.put<OneLoginService>(OneLoginService());
    await OneLoginService.to.init();

  }
}

3、一键获取手机号页面


import 'dart:io';
import 'package:ayidaojia/common/index.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gt_onelogin_flutter_plugin/gt_onelogin_flutter_plugin.dart';
import 'package:permission_handler/permission_handler.dart';

class RealnameController extends GetxController {
  RealnameController();
  
  // 预取号状态
  bool isTokenValid = false;

  @override
  void onReady() {
    super.onReady();
    _initData();
  }

  _initData() async {
    // 自动执行预取号
    await _autoPreGetToken();
    update(["realname"]);
  }

  // 自动预取号
  Future<void> _autoPreGetToken() async {
    try {
      print('========== 开始自动预取号 ==========');
      
      // 1. 检查网络环境
      final networkInfo = await OneLoginService.to.getCurrentNetworkInfo();
      final carrier = await OneLoginService.to.getCurrentCarrier();
      
      print('网络类型: $networkInfo');
      print('运营商: $carrier');
      
      // 判断网络环境是否符合要求(仅警告,不阻止)
      if (networkInfo != OLNetworkInfo.cellular &&  networkInfo != OLNetworkInfo.cellularAndWifi) {
        print('⚠️  网络类型不符合:需要移动网络(当前: $networkInfo)');
        print('一键登录可能无法使用,但仍会尝试预取号');
      }
      
      if (carrier == OLCarrierType.unknow) {
        print('⚠️  无法识别运营商(当前: $carrier)');
        print('一键登录可能无法使用,但仍会尝试预取号');
      } else {
        print('✅ 网络环境检查通过');
      }
      
      // 2. 先检查预取号状态(可能在 global.dart 中已经预取号成功)
      isTokenValid = await OneLoginService.to.checkPreGetTokenValid();
      
      if (isTokenValid) {
        print('✅ 预取号已有效,无需重新预取');
        return;
      }
      
      // 3. 预取号无效,开始重新预取号(增加超时时间到 60 秒)
      print('预取号无效,开始重新预取号...');
      final startTime = DateTime.now();
      
      // 执行预取号,超时时间 60 秒
      final result = await OneLoginService.to.preGetToken(
        timeout: 60000,  // 60 秒超时
        checkInterval: 1000,  // 每秒检查一次
      );
      
      final duration = DateTime.now().difference(startTime).inMilliseconds;
      print('预取号总耗时: ${duration}ms');
      
      if (result['success'] == true) {
        // 再次确认 token 有效性
        isTokenValid = await OneLoginService.to.checkPreGetTokenValid();
        if (isTokenValid) {
          print('✅ 预取号成功,Token 有效');
        } else {
          print('⚠️  预取号完成,但 Token 无效');
          isTokenValid = false;
        }
      } else {
        // 预取号失败 - 显示详细错误信息
        final errorCode = result['errorCode'] ?? '未知';
        final message = result['message'] ?? '未知错误';
        final detail = result['detail'] ?? '';
        
        print('========== 预取号失败详情 ==========');
        print('❌ 预取号失败');
        print('错误码 (errorCode): $errorCode');
        print('错误信息 (message): $message');
        if (detail.isNotEmpty) {
          print('详细信息 (detail): $detail');
        }
        print('=====================================');
        isTokenValid = false;
      }
    } catch (e) {
      print('❌ 自动预取号异常: $e');
      isTokenValid = false;
    }
  }

  // 极验一键获取手机号
  Future<void> oneKeyGetPhone() async {
    try {
      print('========== 开始一键获取手机号 ==========');
      print('当前平台: ${Platform.isAndroid ? "Android" : "iOS"}');
      
      // 0. 检查并申请权限(仅 Android 需要)
      if (Platform.isAndroid) {
        print('检查 READ_PHONE_STATE 权限...');
        var permissionStatus = await Permission.phone.status;
        print('权限状态: $permissionStatus');
        
        if (!permissionStatus.isGranted) {
          print('权限未授予,开始申请权限...');
          Loading.showLottie('正在申请权限...');
          
          permissionStatus = await Permission.phone.request();
          Loading.dismiss();
          
          if (!permissionStatus.isGranted) {
            // 权限被拒绝,显示友好的对话框
            await _showPhonePermissionDialog(permissionStatus.isPermanentlyDenied);
            print('❌ 权限未授予,无法使用一键登录');
            return;
          }
          
          print('✅ 权限申请成功');
        } else {
          print('✅ 权限已授予');
        }
      } else {
        // iOS 不需要 READ_PHONE_STATE 权限
        print('✅ iOS 平台,跳过 READ_PHONE_STATE 权限检查');
      }
      
      // 1. 再次检查 Token 有效性(可能已过期)
      isTokenValid = await OneLoginService.to.checkPreGetTokenValid();
      print('当前 Token 有效性: $isTokenValid');
      
      if (!isTokenValid) {
        print('Token 无效,尝试重新预取号...');
        Loading.showLottie('正在检测网络环境...');
        
        // 检查网络环境
      final networkInfo = await OneLoginService.to.getCurrentNetworkInfo();
      final carrier = await OneLoginService.to.getCurrentCarrier();
      
      print('网络类型: $networkInfo');
      print('运营商: $carrier');
      
      // 检查是否是移动网络
      if (networkInfo != OLNetworkInfo.cellular &&  networkInfo != OLNetworkInfo.cellularAndWifi) {
          Loading.dismiss();
          Loading.toast('请关闭WiFi,使用移动网络');
        return;
      }
      
        // 检查运营商
      if (carrier == OLCarrierType.unknow) {
          Loading.dismiss();
        Loading.toast(
          '无法识别运营商

'
          '可能原因:
'
          '1. SIM卡已停机或欠费
'
          '2. 使用了虚拟运营商
'
          '3. 未开启移动数据

'
            '请更换手机SIM卡并重试',
        );
        return;
      }
      
        print('✅ 网络环境检查通过,开始预取号...');
        
        // 重新预取号(增加超时时间到 60 秒)
        final result = await OneLoginService.to.preGetToken(
          timeout: 60000,  // 60 秒超时
          checkInterval: 1000,  // 每秒检查一次
        );
        
        Loading.dismiss();
        
        if (result['success'] != true) {
          // 预取号失败 - 显示详细错误
          final errorCode = result['errorCode'] ?? '未知';
          final message = result['message'] ?? '未知错误';
          final detail = result['detail'] ?? '';
          
          print('========== 预取号失败详情 ==========');
          print('❌ 预取号失败');
          print('错误码 (errorCode): $errorCode');
          print('错误信息 (message): $message');
          if (detail.isNotEmpty) {
            print('详细信息 (detail): $detail');
          }
          print('=====================================');
          
          // 根据错误码给出友好提示
          String userMessage = _getErrorMessage(errorCode, message);
          Loading.toast(userMessage);
          return;
        }
        
        // 再次确认 token 有效性
        isTokenValid = await OneLoginService.to.checkPreGetTokenValid();
        if (!isTokenValid) {
          Loading.toast('预取号完成但 Token 无效
请稍后重试');
          return;
        }
        
        print('✅ 预取号成功,Token 有效');
      }
      
      // 2. 拉起授权页面获取 token
      print('拉起授权页面...');
      final result = await OneLoginService.to.requestToken();
      
      // 3. 处理返回结果
      if (result != null && result.isNotEmpty) {
        print('========== 原始返回数据 ==========');
        print('完整结果: $result');
        
        final statusCode = result['status'];
        print('状态码: $statusCode');
        
        if (statusCode == 200) {
          // 成功获取 Token
          final token = result['token'] ?? '';
          final processId = result['process_id'] ?? '';
          final authcode = result['authcode'] ?? '';
        
          print('========== 解析结果 ==========');
          print('Token: $token');
          print('Process ID: $processId');
          print('Authcode: $authcode');
          
          if (token.isEmpty || processId.isEmpty) {
            Loading.toast('获取 Token 失败,请手动输入');
            return;
          }
        
          // 4. 调用后端接口验证 token 获取真实手机号
          Loading.showLottie('正在验证手机号...');
          print('调用后端接口验证 Token...');
          
        final phone = await UserApi.verifyOneLoginToken(token, processId);
        
          Loading.dismiss();
          
          if (phone.isNotEmpty) {
            print('✅ 获取手机号成功: $phone');
            
            // 关闭授权页面
            OneLoginService.to.closeAuthPage();
            print('✅ 已关闭授权页面');

            update(["realname"]);
          } else {
            print('❌ 验证失败:后端返回为空');
            
            // 关闭授权页面
            OneLoginService.to.closeAuthPage();
            
            Loading.toast('验证失败,请手动输入');
          }
        } else {
          // 获取失败
          final errorCode = result['errorCode'] ?? '';
          final msg = result['msg'] ?? '';
          
          print('❌ 获取 Token 失败');
          print('错误码: $errorCode');
          print('消息: $msg');
          
          // 关闭授权页面
          OneLoginService.to.closeAuthPage();
          
          // 根据错误码给出友好提示
          String userMessage = _getErrorMessage(errorCode, msg);
          Loading.toast(userMessage);
          
          // Token 失效,标记为无效
          isTokenValid = false;
        }
      } else {
        print('❌ 用户取消或网络异常');
        // 用户取消不需要提示(用户主动关闭授权页,SDK 已自动关闭)
      }
    } catch (e) {
      Loading.dismiss();
      print('❌ 一键获取手机号异常: $e');
      Loading.toast('获取失败,请手动输入手机号');
      isTokenValid = false;
    }
  }

  // 显示电话权限对话框(Android)
  /// [isPermanentlyDenied] 是否被永久拒绝
  Future<void> _showPhonePermissionDialog(bool isPermanentlyDenied) async {
    final context = Get.context;
    if (context == null) {
      Loading.error('需要"电话"权限才能使用一键登录,请在设置中开启权限'.tr);
      return;
    }

    return showCupertinoDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext dialogContext) {
        return CupertinoAlertDialog(
          title: Text('无法使用一键登录'.tr),
          content: Text(
            isPermanentlyDenied
                ? '当前无电话权限,建议前往系统设置,允许应用访问[电话]权限

'
                    '一键登录需要此权限来验证您的手机号码'
                : '需要"电话"权限才能使用一键登录功能

'
                    '此权限用于验证您的手机号码,不会拨打电话或产生费用',
          ),
          actions: <Widget>[
            CupertinoDialogAction(
              child: Text(
                '取消'.tr,
                style: const TextStyle(fontSize: 16, color: AppTheme.color999),
              ),
              onPressed: () {
                Navigator.of(dialogContext).pop();
              },
            ),
            if (isPermanentlyDenied)
              CupertinoDialogAction(
                isDefaultAction: true,
                child: Text(
                  '前往系统设置'.tr,
                  style: const TextStyle(fontSize: 16, color: Colors.black),
                ),
                onPressed: () async {
                  Navigator.of(dialogContext).pop();
                  // 跳转到系统设置
                  await openAppSettings();
                },
              )
            else
              CupertinoDialogAction(
                isDefaultAction: true,
                child: Text(
                  '重新授权'.tr,
                  style: const TextStyle(fontSize: 16, color: Colors.black),
                ),
                onPressed: () async {
                  Navigator.of(dialogContext).pop();
                  // 重新请求权限
                  final status = await Permission.phone.request();
                  if (status.isGranted) {
                    Loading.success('授权成功,请重新点击一键获取'.tr);
                  }
                },
              ),
          ],
        );
      },
    );
  }

  // 根据错误码返回友好的错误提示
  String _getErrorMessage(String errorCode, String defaultMsg) {
    switch (errorCode) {
      case '-20103':
        return '请勿重复调用一键登录';
      case '-20200':
        return '网络不可用
请检查网络连接';
      case '-20202':
        return '请开启移动数据并关闭WiFi';
      case '-20203':
        return '不支持的运营商
仅支持移动/联通/电信';
      case '-20204':
        return '认证已失效
请重新尝试';
      case '-20105':
        return '网络超时
请检查网络信号';
      case '-20201':
        return '未检测到SIM卡
请插入SIM卡';
      case '-20205':
        return '已取消授权';
      case '-20206':
      case '-20207':
        return '已取消操作';
      default:
        if (errorCode.startsWith('-201')) {
          return 'SDK初始化失败
请稍后重试';
        } else if (errorCode.startsWith('-202')) {
          return '网络环境异常
请检查网络和SIM卡';
        } else if (errorCode.startsWith('-203')) {
          return '获取失败
请重新尝试';
        }
        return defaultMsg.isNotEmpty ? defaultMsg : '获取失败,请手动输入';
    }
  }
}

二、安卓配置

下载插件到根目录 /plugins/gt_onelogin_flutter_plugin-0.1.2


  # 极验手机号码验证
  dependencies:
  gt_onelogin_flutter_plugin:
    path: plugins/gt_onelogin_flutter_plugin-0.1.2

2./android/app/build.gradle


dependencies {
    // 极验一键登录 SDK
    implementation 'com.geetest.android:onelogin:2.9.8'
}

3.AndroidManifest.xml 权限配置


<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

4.android/app/src/main/res/xml文件配置


<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- 允许所有明文流量 -->
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <!-- 信任系统预装的CA证书 -->
            <certificates src="system" />
            <!-- 信任用户添加的CA证书 -->
            <certificates src="user" />
        </trust-anchors>
    </base-config>
    <!-- 特定域名的配置 -->
    <domain-config cleartextTrafficPermitted="true">
        <!-- 业务域名 -->
        <domain includeSubdomains="true">id6.me</domain>
        
        <!-- 极验一键登录域名 -->
        <domain includeSubdomains="true">geetest.com</domain>
        
        <!-- 运营商域名 -->
        <!-- 中国移动 -->
        <domain includeSubdomains="true">cmpassport.com</domain>
        <!-- 中国联通 -->
        <domain includeSubdomains="true">10010.com</domain>
        <domain includeSubdomains="true">wo.cn</domain>
        <!-- 中国电信 -->
        <domain includeSubdomains="true">189.cn</domain>
        <domain includeSubdomains="true">21cn.com</domain>
        
        <trust-anchors>
            <certificates src="system" />
            <certificates src="user" />
        </trust-anchors>
    </domain-config>
</network-security-config> 

5.新增极验混淆规则proguard-rules.pro


# ==================== 极验一键登录混淆规则 ====================
-keep class com.geetest.onelogin.** { *; }
-dontwarn com.geetest.onelogin.**

# 运营商 SDK 混淆规则(中国移动、联通、电信)
-keep class cn.com.chinatelecom.** { *; }
-keep class com.mobile.auth.** { *; }
-keep class com.cmic.** { *; }
-keep class com.unicom.** { *; }
-dontwarn cn.com.chinatelecom.**
-dontwarn com.mobile.auth.**
-dontwarn com.cmic.**
-dontwarn com.unicom.**

三、ios配置

1.Podfile


# Uncomment this line to define a global platform for your project
platform :ios, '15.6'

...
...
...

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    
    # 配置 permission_handler 权限宏
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        
        ## 相机权限(实人认证、拍照上传)
        'PERMISSION_CAMERA=1',
        
        ## 相册权限(选择图片、保存图片)
        'PERMISSION_PHOTOS=1',
        
        
        ## 麦克风权限(录制视频、语音消息)
        'PERMISSION_MICROPHONE=1',
        
        ## 广告追踪权限(极验一键登录可能需要)
        'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
      ]
    end
  end
end

2.Info.plist



		<key>NSCameraUsageDescription</key>
		<string>获取相机权限,使用拍照功能,用于上传头像,实名认证人脸识别</string>
		<key>NSMicrophoneUsageDescription</key>
		<string>需要访问您的麦克风以录制视频</string>
		<key>NSPhotoLibraryAddUsageDescription</key>
		<string>获取相机权限,使用拍照功能,用于上传头像、保存图片</string>
		<key>NSPhotoLibraryUsageDescription</key>
		<string>获取相机权限,使用拍照功能,用于上传头像、保存图片</string>
	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
		<key>NSExceptionDomains</key>
		<dict>
			<!-- 极验服务器域名 -->
			<key>geetest.com</key>
			<dict>
				<key>NSIncludesSubdomains</key>
				<true/>
				<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
				<true/>
				<key>NSTemporaryExceptionRequiresForwardSecrecy</key>
				<false/>
			</dict>
			<key>onepass.geetest.com</key>
			<dict>
				<key>NSIncludesSubdomains</key>
				<true/>
				<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
				<true/>
				<key>NSTemporaryExceptionRequiresForwardSecrecy</key>
				<false/>
			</dict>
		</dict>
	</dict>
	<!-- 极验一键登录所需权限描述 -->
	<key>NSUserTrackingUsageDescription</key>
	<string>为了提供更好的一键登录服务,需要获取您的设备信息</string>
	<!-- 蜂窝网络权限(一键登录必需) -->
	<key>NSCellularUsageDescription</key>
	<string>需要使用蜂窝网络进行一键登录验证</string>
</dict>
</plist>

Flutter:极验一键获取手机号

© 版权声明

相关文章

暂无评论

none
暂无评论...