极验一键获取手机号:插件地址
官方文档:安卓文档
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>

© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...





