feat: webview geetest (#1342)

* feat: webview geetest

* opt: geetest

* fix: linux

* remove pwd mobile check

* fix linux check
This commit is contained in:
My-Responsitories
2025-09-27 10:57:41 +08:00
committed by GitHub
parent ee8af925be
commit e3e6bb0e39
5 changed files with 202 additions and 44 deletions

View File

@@ -28,18 +28,16 @@ class CaptchaDataModel {
}
class GeetestData {
GeetestData({
this.challenge,
this.gt,
const GeetestData({
required this.challenge,
required this.gt,
});
String? challenge;
String? gt;
final String challenge;
final String gt;
GeetestData.fromJson(Map<String, dynamic> json) {
challenge = json["challenge"];
gt = json["gt"];
}
factory GeetestData.fromJson(Map<String, dynamic> json) =>
GeetestData(challenge: json["challenge"], gt: json["gt"]);
}
class Tencent {

View File

@@ -9,6 +9,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/login.dart';
import 'package:PiliPlus/models/common/account_type.dart';
import 'package:PiliPlus/models/login/model.dart';
import 'package:PiliPlus/pages/login/geetest/geetest_webview_dialog.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/utils.dart';
@@ -118,7 +119,31 @@ class LoginPageController extends GetxController
}
// 申请极验验证码
void getCaptcha(String? geeGt, String? geeChallenge, VoidCallback onSuccess) {
Future<void> getCaptcha(
String geeGt,
String geeChallenge,
VoidCallback onSuccess,
) async {
void updateCaptchaData(Map<String, dynamic> json) {
captchaData
..validate = json['geetest_validate']
..seccode = json['geetest_seccode']
..geetest = GeetestData(
challenge: json['geetest_challenge'],
gt: geeGt,
);
}
if (Utils.isDesktop) {
final res = await Get.dialog<Map<String, dynamic>>(
GeetestWebviewDialog(geeGt, geeChallenge),
);
if (res != null) {
updateCaptchaData(res);
onSuccess();
}
return;
}
var registerData = Gt3RegisterData(
challenge: geeChallenge,
gt: geeGt,
@@ -137,13 +162,7 @@ class LoginPageController extends GetxController
if (code == "1") {
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
SmartDialog.showToast('验证成功');
captchaData
..validate = message['result']['geetest_validate']
..seccode = message['result']['geetest_seccode']
..geetest = GeetestData(
challenge: message['result']['geetest_challenge'],
gt: geeGt,
);
updateCaptchaData(message['result']);
onSuccess();
} else {
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
@@ -293,7 +312,7 @@ class LoginPageController extends GetxController
}
if (data['status'] == 2) {
SmartDialog.showToast(data['message']);
if (!Utils.isMobile) {
if (Platform.isLinux) {
return;
}
// return;
@@ -381,8 +400,8 @@ class LoginPageController extends GetxController
"(${preCaptureRes['code']}) ${preCaptureRes['msg']} ${preCaptureRes['data']}",
);
}
String? geeGt = preCaptureRes['data']['gee_gt'];
String? geeChallenge = preCaptureRes['data']['gee_challenge'];
String geeGt = preCaptureRes['data']['gee_gt'];
String geeChallenge = preCaptureRes['data']['gee_challenge'];
captchaData.token = preCaptureRes['data']['recaptcha_token'];
if (!isGeeArgumentValid(geeGt, geeChallenge)) {
SmartDialog.showToast(
@@ -500,7 +519,7 @@ class LoginPageController extends GetxController
case 0:
// login success
break;
case -105 when (Utils.isMobile):
case -105 when (!Platform.isLinux):
String captureUrl = res['data']['url'];
Uri captureUri = Uri.parse(captureUrl);
captchaData.token = captureUri.queryParameters['recaptcha_token']!;
@@ -670,7 +689,7 @@ class LoginPageController extends GetxController
return;
}
getCaptcha(geeGt, geeChallenge, sendSmsCode);
getCaptcha(geeGt!, geeChallenge!, sendSmsCode);
break;
default:
SmartDialog.showToast(res['msg']);

View File

@@ -0,0 +1,135 @@
import 'dart:convert';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/ua_type.dart';
import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
class GeetestWebviewDialog extends StatelessWidget {
const GeetestWebviewDialog(this.gt, this.challenge, {super.key});
final String gt;
final String challenge;
static const _geetestJsUri =
'https://static.geetest.com/static/js/fullpage.0.0.0.js';
static Future<LoadingState<String>> _getConfig(
String gt,
String challenge,
) async {
final res = await Request().get<String>(
'https://api.geetest.com/gettype.php',
queryParameters: {'gt': gt},
options: Options(
responseType: ResponseType.plain,
extra: {'account': const NoAccount()},
),
);
if (res.data case String data) {
if (data.startsWith('(') && data.endsWith(')')) {
final Map<String, dynamic> config;
try {
config = jsonDecode(data.substring(1, data.length - 1));
} catch (e) {
return Error(e.toString());
}
if (config['status'] == 'success') {
return Success(
jsonEncode(
config['data'] as Map<String, dynamic>..addAll({
"gt": gt,
"challenge": challenge,
"offline": false,
"new_captcha": true,
"product": "bind",
"width": "100%",
"https": true,
"protocol": "https://",
}),
),
);
} else {
return Error(data);
}
}
}
return Error(res.data['message']);
}
@override
Widget build(BuildContext context) {
final future = _getConfig(gt, challenge);
return AlertDialog(
title: const Text('验证码'),
content: SizedBox(
width: 300,
height: 400,
child: InAppWebView(
webViewEnvironment: webViewEnvironment,
initialSettings: InAppWebViewSettings(
clearCache: true,
javaScriptEnabled: true,
forceDark: ForceDark.AUTO,
useHybridComposition: false,
algorithmicDarkeningAllowed: true,
useShouldOverrideUrlLoading: true,
userAgent: UaType.mob.ua,
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
),
initialData: InAppWebViewInitialData(
data:
'<!DOCTYPE html><html><head></head><body><script src="$_geetestJsUri"></script><script>function R(n,o){flutter_inappwebview.callHandler(n,o)}</script></body></html>',
),
onWebViewCreated: (ctr) {
ctr
..addJavaScriptHandler(
handlerName: 'success',
callback: (args) {
if (args.isNotEmpty) {
if (args[0] case Map<String, dynamic> data) {
Get.back(result: data);
return;
}
}
debugPrint('geetest invalid result: $args');
},
)
..addJavaScriptHandler(
handlerName: 'error',
callback: (args) {
debugPrint('geetest error: $args');
},
);
},
onLoadStop: (ctr, _) async {
final config = await future;
if (config.isSuccess) {
ctr.evaluateJavascript(
source:
'let t=Geetest(${config.data}).onSuccess(()=>R("success",t.getValidate())).onError((o)=>R("error",o));t.onReady(()=>t.verify());',
);
} else {
config.toast();
Get.back();
}
},
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: ColorScheme.of(context).outline),
),
),
],
);
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:ui';
import 'package:PiliPlus/common/constants.dart';
@@ -30,7 +31,6 @@ class _LoginPageState extends State<LoginPage> {
// 二维码生成时间
bool showPassword = false;
GlobalKey globalKey = GlobalKey();
bool get isMobile => kDebugMode || Utils.isMobile;
Widget loginByQRCode(ThemeData theme) {
return Column(
@@ -75,7 +75,7 @@ class _LoginPageState extends State<LoginPage> {
icon: const Icon(Icons.save),
label: const Text('保存至相册'),
),
if (isMobile)
if (kDebugMode || Utils.isMobile)
TextButton.icon(
onPressed: () => PageUtils.launchURL(
_loginPageCtr.codeInfo.value.data.url,
@@ -374,7 +374,7 @@ class _LoginPageState extends State<LoginPage> {
Builder(
builder: (context) {
return PopupMenuButton(
enabled: isMobile,
enabled: !Platform.isLinux,
padding: EdgeInsets.zero,
tooltip:
'选择国际冠码,'
@@ -423,7 +423,7 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(width: 6),
Expanded(
child: TextField(
enabled: isMobile,
enabled: !Platform.isLinux,
controller: _loginPageCtr.telTextController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
@@ -455,7 +455,7 @@ class _LoginPageState extends State<LoginPage> {
children: [
Expanded(
child: TextField(
enabled: isMobile,
enabled: !Platform.isLinux,
controller: _loginPageCtr.smsCodeTextController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.sms_outlined),
@@ -470,10 +470,10 @@ class _LoginPageState extends State<LoginPage> {
),
Obx(
() => TextButton.icon(
onPressed: isMobile
? (_loginPageCtr.smsSendCooldown > 0
onPressed: !Platform.isLinux
? _loginPageCtr.smsSendCooldown > 0
? null
: _loginPageCtr.sendSmsCode)
: _loginPageCtr.sendSmsCode
: null,
icon: const Icon(Icons.send),
label: Text(
@@ -489,7 +489,7 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 20),
OutlinedButton.icon(
onPressed: isMobile ? _loginPageCtr.loginBySmsCode : null,
onPressed: !Platform.isLinux ? _loginPageCtr.loginBySmsCode : null,
icon: const Icon(Icons.login),
label: const Text('登录'),
),

View File

@@ -218,22 +218,28 @@ class AccountManager extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final path = response.requestOptions.path;
if (path.startsWith(HttpString.appBaseUrl) || _skipCookie(path)) {
final options = response.requestOptions;
final path = options.path;
if (path.startsWith(HttpString.appBaseUrl) ||
_skipCookie(path) ||
options.extra['account'] is NoAccount) {
return handler.next(response);
} else {
_saveCookies(
final future = _saveCookies(
response,
).whenComplete(() => handler.next(response)).catchError(
(dynamic e, StackTrace s) {
final error = DioException(
requestOptions: response.requestOptions,
error: e,
stackTrace: s,
);
handler.reject(error, true);
},
);
).whenComplete(() => handler.next(response));
assert(() {
future.catchError(
(Object e, StackTrace s) {
throw DioException(
requestOptions: response.requestOptions,
error: e,
stackTrace: s,
);
},
);
return true;
}());
}
}