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 { class GeetestData {
GeetestData({ const GeetestData({
this.challenge, required this.challenge,
this.gt, required this.gt,
}); });
String? challenge; final String challenge;
String? gt; final String gt;
GeetestData.fromJson(Map<String, dynamic> json) { factory GeetestData.fromJson(Map<String, dynamic> json) =>
challenge = json["challenge"]; GeetestData(challenge: json["challenge"], gt: json["gt"]);
gt = json["gt"];
}
} }
class Tencent { class Tencent {

View File

@@ -9,6 +9,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/login.dart'; import 'package:PiliPlus/http/login.dart';
import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/account_type.dart';
import 'package:PiliPlus/models/login/model.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.dart';
import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/utils.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( var registerData = Gt3RegisterData(
challenge: geeChallenge, challenge: geeChallenge,
gt: geeGt, gt: geeGt,
@@ -137,13 +162,7 @@ class LoginPageController extends GetxController
if (code == "1") { if (code == "1") {
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询 // 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
SmartDialog.showToast('验证成功'); SmartDialog.showToast('验证成功');
captchaData updateCaptchaData(message['result']);
..validate = message['result']['geetest_validate']
..seccode = message['result']['geetest_seccode']
..geetest = GeetestData(
challenge: message['result']['geetest_challenge'],
gt: geeGt,
);
onSuccess(); onSuccess();
} else { } else {
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried. // 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
@@ -293,7 +312,7 @@ class LoginPageController extends GetxController
} }
if (data['status'] == 2) { if (data['status'] == 2) {
SmartDialog.showToast(data['message']); SmartDialog.showToast(data['message']);
if (!Utils.isMobile) { if (Platform.isLinux) {
return; return;
} }
// return; // return;
@@ -381,8 +400,8 @@ class LoginPageController extends GetxController
"(${preCaptureRes['code']}) ${preCaptureRes['msg']} ${preCaptureRes['data']}", "(${preCaptureRes['code']}) ${preCaptureRes['msg']} ${preCaptureRes['data']}",
); );
} }
String? geeGt = preCaptureRes['data']['gee_gt']; String geeGt = preCaptureRes['data']['gee_gt'];
String? geeChallenge = preCaptureRes['data']['gee_challenge']; String geeChallenge = preCaptureRes['data']['gee_challenge'];
captchaData.token = preCaptureRes['data']['recaptcha_token']; captchaData.token = preCaptureRes['data']['recaptcha_token'];
if (!isGeeArgumentValid(geeGt, geeChallenge)) { if (!isGeeArgumentValid(geeGt, geeChallenge)) {
SmartDialog.showToast( SmartDialog.showToast(
@@ -500,7 +519,7 @@ class LoginPageController extends GetxController
case 0: case 0:
// login success // login success
break; break;
case -105 when (Utils.isMobile): case -105 when (!Platform.isLinux):
String captureUrl = res['data']['url']; String captureUrl = res['data']['url'];
Uri captureUri = Uri.parse(captureUrl); Uri captureUri = Uri.parse(captureUrl);
captchaData.token = captureUri.queryParameters['recaptcha_token']!; captchaData.token = captureUri.queryParameters['recaptcha_token']!;
@@ -670,7 +689,7 @@ class LoginPageController extends GetxController
return; return;
} }
getCaptcha(geeGt, geeChallenge, sendSmsCode); getCaptcha(geeGt!, geeChallenge!, sendSmsCode);
break; break;
default: default:
SmartDialog.showToast(res['msg']); 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 'dart:ui';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
@@ -30,7 +31,6 @@ class _LoginPageState extends State<LoginPage> {
// 二维码生成时间 // 二维码生成时间
bool showPassword = false; bool showPassword = false;
GlobalKey globalKey = GlobalKey(); GlobalKey globalKey = GlobalKey();
bool get isMobile => kDebugMode || Utils.isMobile;
Widget loginByQRCode(ThemeData theme) { Widget loginByQRCode(ThemeData theme) {
return Column( return Column(
@@ -75,7 +75,7 @@ class _LoginPageState extends State<LoginPage> {
icon: const Icon(Icons.save), icon: const Icon(Icons.save),
label: const Text('保存至相册'), label: const Text('保存至相册'),
), ),
if (isMobile) if (kDebugMode || Utils.isMobile)
TextButton.icon( TextButton.icon(
onPressed: () => PageUtils.launchURL( onPressed: () => PageUtils.launchURL(
_loginPageCtr.codeInfo.value.data.url, _loginPageCtr.codeInfo.value.data.url,
@@ -374,7 +374,7 @@ class _LoginPageState extends State<LoginPage> {
Builder( Builder(
builder: (context) { builder: (context) {
return PopupMenuButton( return PopupMenuButton(
enabled: isMobile, enabled: !Platform.isLinux,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
tooltip: tooltip:
'选择国际冠码,' '选择国际冠码,'
@@ -423,7 +423,7 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: TextField( child: TextField(
enabled: isMobile, enabled: !Platform.isLinux,
controller: _loginPageCtr.telTextController, controller: _loginPageCtr.telTextController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
@@ -455,7 +455,7 @@ class _LoginPageState extends State<LoginPage> {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
enabled: isMobile, enabled: !Platform.isLinux,
controller: _loginPageCtr.smsCodeTextController, controller: _loginPageCtr.smsCodeTextController,
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: Icon(Icons.sms_outlined), prefixIcon: Icon(Icons.sms_outlined),
@@ -470,10 +470,10 @@ class _LoginPageState extends State<LoginPage> {
), ),
Obx( Obx(
() => TextButton.icon( () => TextButton.icon(
onPressed: isMobile onPressed: !Platform.isLinux
? (_loginPageCtr.smsSendCooldown > 0 ? _loginPageCtr.smsSendCooldown > 0
? null ? null
: _loginPageCtr.sendSmsCode) : _loginPageCtr.sendSmsCode
: null, : null,
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
label: Text( label: Text(
@@ -489,7 +489,7 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: isMobile ? _loginPageCtr.loginBySmsCode : null, onPressed: !Platform.isLinux ? _loginPageCtr.loginBySmsCode : null,
icon: const Icon(Icons.login), icon: const Icon(Icons.login),
label: const Text('登录'), label: const Text('登录'),
), ),

View File

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