huixiang_app
1 year ago
14 changed files with 1522 additions and 168 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,366 @@
|
||||
import 'dart:convert'; |
||||
|
||||
import 'package:dio/dio.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
import '../../retrofit/retrofit_api.dart'; |
||||
import '../../utils/captcha_util.dart'; |
||||
import '../../utils/widget_util.dart'; |
||||
|
||||
typedef VoidSuccessCallback = dynamic Function(String v); |
||||
|
||||
class ClickWordCaptcha extends StatefulWidget { |
||||
final VoidSuccessCallback onSuccess; //文字点击后验证成功回调 |
||||
final VoidCallback onFail; //文字点击完成后验证失败回调 |
||||
|
||||
const ClickWordCaptcha({Key key, this.onSuccess, this.onFail}) |
||||
: super(key: key); |
||||
|
||||
@override |
||||
_ClickWordCaptchaState createState() => _ClickWordCaptchaState(); |
||||
} |
||||
|
||||
class _ClickWordCaptchaState extends State<ClickWordCaptcha> { |
||||
ClickWordCaptchaState _clickWordCaptchaState = ClickWordCaptchaState.none; |
||||
List<Offset> _tapOffsetList = []; |
||||
ClickWordCaptchaModel _clickWordCaptchaModel = ClickWordCaptchaModel(); |
||||
|
||||
Color titleColor = Colors.black; |
||||
Color borderColor = Color(0xffdddddd); |
||||
String bottomTitle = ""; |
||||
Size baseSize = Size(310.0, 155.0); |
||||
|
||||
//改变底部样式及字段 |
||||
_changeResultState() { |
||||
switch (_clickWordCaptchaState) { |
||||
case ClickWordCaptchaState.normal: |
||||
titleColor = Colors.black; |
||||
borderColor = Color(0xffdddddd); |
||||
break; |
||||
case ClickWordCaptchaState.success: |
||||
_tapOffsetList = []; |
||||
titleColor = Colors.green; |
||||
borderColor = Colors.green; |
||||
bottomTitle = "验证成功"; |
||||
break; |
||||
case ClickWordCaptchaState.fail: |
||||
_tapOffsetList = []; |
||||
titleColor = Colors.red; |
||||
borderColor = Colors.red; |
||||
bottomTitle = "验证失败"; |
||||
break; |
||||
default: |
||||
titleColor = Colors.black; |
||||
borderColor = Color(0xffdddddd); |
||||
bottomTitle = "数据加载中……"; |
||||
break; |
||||
} |
||||
setState(() {}); |
||||
} |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_loadCaptcha(); |
||||
} |
||||
|
||||
//加载验证码 |
||||
_loadCaptcha() async { |
||||
_tapOffsetList = []; |
||||
_clickWordCaptchaState = ClickWordCaptchaState.none; |
||||
_changeResultState(); |
||||
ApiService apiIpService = ApiService(Dio(), context: context); |
||||
ClickWordCaptchaModel baseData = await apiIpService.captchaGet({"captchaType": "clickWord"}).catchError((onError) {}); |
||||
if (baseData == null) { |
||||
_clickWordCaptchaModel.secretKey = ""; |
||||
bottomTitle = "加载失败,请刷新"; |
||||
_clickWordCaptchaState = ClickWordCaptchaState.normal; |
||||
_changeResultState(); |
||||
return; |
||||
} |
||||
else { |
||||
_clickWordCaptchaModel = baseData; |
||||
var baseR = await WidgetUtil.getImageWH( |
||||
image: Image.memory( |
||||
Base64Decoder().convert(_clickWordCaptchaModel.imgStr))); |
||||
baseSize = baseR.size; |
||||
|
||||
bottomTitle = "请依次点击【${_clickWordCaptchaModel.wordStr}】"; |
||||
} |
||||
|
||||
_clickWordCaptchaState = ClickWordCaptchaState.normal; |
||||
_changeResultState(); |
||||
} |
||||
|
||||
//校验验证码 |
||||
_checkCaptcha() async { |
||||
List<Map<String, dynamic>> mousePos = []; |
||||
_tapOffsetList.map((size) { |
||||
mousePos |
||||
.add({"x": size.dx.roundToDouble(), "y": size.dy.roundToDouble()}); |
||||
}).toList(); |
||||
var pointStr = json.encode(mousePos); |
||||
|
||||
var cryptedStr = pointStr; |
||||
|
||||
// secretKey 不为空 进行as加密 |
||||
if (!CaptchaUtil.isEmpty(_clickWordCaptchaModel.secretKey)) { |
||||
cryptedStr = CaptchaUtil.aesEncode( |
||||
key: _clickWordCaptchaModel.secretKey, content: pointStr); |
||||
// var dcrypt = CaptchaUtil.aesDecode( |
||||
// key: _clickWordCaptchaModel.secretKey, content: cryptedStr); |
||||
} |
||||
|
||||
// Map _map = json.decode(dcrypt); |
||||
ApiService apiIpService = ApiService(Dio(), context: context); |
||||
bool baseData = await apiIpService.captchaCheck({ |
||||
"pointJson": cryptedStr, |
||||
"captchaType": "clickWord", |
||||
"token": _clickWordCaptchaModel.token |
||||
}).catchError((onError) {}); |
||||
if (baseData) { |
||||
_checkFail(); |
||||
return; |
||||
} |
||||
//如果不加密 将 token 和 坐标序列化 通过 --- 链接成字符串 |
||||
var captchaVerification = "${_clickWordCaptchaModel.token}---$pointStr"; |
||||
if (!CaptchaUtil.isEmpty(_clickWordCaptchaModel.secretKey)) { |
||||
//如果加密 将 token 和 坐标序列化 通过 --- 链接成字符串 进行加密 加密密钥为 _clickWordCaptchaModel.secretKey |
||||
captchaVerification = CaptchaUtil.aesEncode( |
||||
key: _clickWordCaptchaModel.secretKey, |
||||
content: captchaVerification); |
||||
} |
||||
_checkSuccess(captchaVerification); |
||||
} |
||||
|
||||
//校验失败 |
||||
_checkFail() async { |
||||
_clickWordCaptchaState = ClickWordCaptchaState.fail; |
||||
_changeResultState(); |
||||
|
||||
await Future.delayed(Duration(milliseconds: 1000)); |
||||
_loadCaptcha(); |
||||
//回调 |
||||
if (widget.onFail != null) { |
||||
widget.onFail(); |
||||
} |
||||
} |
||||
|
||||
//校验成功 |
||||
_checkSuccess(String pointJson) async { |
||||
_clickWordCaptchaState = ClickWordCaptchaState.success; |
||||
_changeResultState(); |
||||
|
||||
await Future.delayed(Duration(milliseconds: 1000)); |
||||
|
||||
var cryptedStr = CaptchaUtil.aesEncode(key: 'BGxdEUOZkXka4HSj', content: pointJson); |
||||
|
||||
print(cryptedStr); |
||||
//回调 pointJson 是经过es加密之后的信息 |
||||
if (widget.onSuccess != null) { |
||||
widget.onSuccess(cryptedStr); |
||||
} |
||||
//关闭 |
||||
Navigator.pop(context); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
var data = MediaQuery.of(context); |
||||
var dialogWidth = 0.9 * data.size.width; |
||||
var isRatioCross = false; |
||||
if (dialogWidth < 320.0) { |
||||
dialogWidth = data.size.width; |
||||
isRatioCross = true; |
||||
} |
||||
return Scaffold( |
||||
backgroundColor: Colors.transparent, |
||||
body: Center( |
||||
child: Container( |
||||
width: dialogWidth, |
||||
height: 320, |
||||
color: Colors.white, |
||||
child: Column( |
||||
children: <Widget>[ |
||||
_topConttainer(), |
||||
_captchaContainer(), |
||||
_bottomContainer() |
||||
], |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
//图片验证码 |
||||
_captchaContainer() { |
||||
List<Widget> _widgetList = []; |
||||
if (!CaptchaUtil.isEmpty(_clickWordCaptchaModel.imgStr)) { |
||||
_widgetList.add(Image( |
||||
width: baseSize.width, |
||||
height: baseSize.height, |
||||
gaplessPlayback: true, |
||||
image: MemoryImage( |
||||
Base64Decoder().convert(_clickWordCaptchaModel.imgStr)))); |
||||
} |
||||
|
||||
double _widgetW = 20; |
||||
for (int i = 0; i < _tapOffsetList.length; i++) { |
||||
Offset offset = _tapOffsetList[i]; |
||||
_widgetList.add(Positioned( |
||||
left: offset.dx - _widgetW * 0.5, |
||||
top: offset.dy - _widgetW * 0.5, |
||||
child: Container( |
||||
alignment: Alignment.center, |
||||
width: _widgetW, |
||||
height: _widgetW, |
||||
decoration: BoxDecoration( |
||||
color: Color(0xCC43A047), |
||||
borderRadius: BorderRadius.all(Radius.circular(_widgetW))), |
||||
child: Text( |
||||
"${i + 1}", |
||||
style: TextStyle(color: Colors.white, fontSize: 15), |
||||
), |
||||
))); |
||||
} |
||||
_widgetList.add(//刷新按钮 |
||||
Positioned( |
||||
top: 0, |
||||
right: 0, |
||||
child: IconButton( |
||||
icon: Icon(Icons.refresh), |
||||
iconSize: 30, |
||||
color: Colors.deepOrangeAccent, |
||||
onPressed: () { |
||||
//刷新 |
||||
_loadCaptcha(); |
||||
}), |
||||
)); |
||||
|
||||
return GestureDetector( |
||||
onTapDown: (TapDownDetails details) { |
||||
debugPrint( |
||||
"onTapDown globalPosition全局坐标系位置: ${details.globalPosition} localPosition组件坐标系位置: ${details.localPosition} "); |
||||
if (!CaptchaUtil.isListEmpty(_clickWordCaptchaModel.wordList) && |
||||
_tapOffsetList.length < _clickWordCaptchaModel.wordList.length) { |
||||
_tapOffsetList.add( |
||||
Offset(details.localPosition.dx, details.localPosition.dy)); |
||||
} |
||||
setState(() {}); |
||||
if (!CaptchaUtil.isListEmpty(_clickWordCaptchaModel.wordList) && |
||||
_tapOffsetList.length == _clickWordCaptchaModel.wordList.length) { |
||||
_checkCaptcha(); |
||||
} |
||||
}, |
||||
child: Container( |
||||
width: baseSize.width, |
||||
height: baseSize.height, |
||||
child: Stack( |
||||
children: _widgetList, |
||||
), |
||||
)); |
||||
} |
||||
|
||||
//底部提示部件 |
||||
_bottomContainer() { |
||||
return Container( |
||||
height: 50, |
||||
margin: EdgeInsets.only(top: 10), |
||||
alignment: Alignment.center, |
||||
width: baseSize.width, |
||||
decoration: BoxDecoration( |
||||
borderRadius: BorderRadius.all(Radius.circular(4)), |
||||
border: Border.all(color: borderColor)), |
||||
child: |
||||
Text(bottomTitle, style: TextStyle(fontSize: 18, color: titleColor)), |
||||
); |
||||
} |
||||
|
||||
//顶部,提示+关闭 |
||||
_topConttainer() { |
||||
return Container( |
||||
padding: EdgeInsets.fromLTRB(10, 0, 10, 0), |
||||
margin: EdgeInsets.only(bottom: 20, top: 5), |
||||
decoration: BoxDecoration( |
||||
border: Border(bottom: BorderSide(width: 1, color: Color(0xffe5e5e5))), |
||||
), |
||||
child: Row( |
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
children: <Widget>[ |
||||
Text( |
||||
'请完成安全验证', |
||||
style: TextStyle(fontSize: 18), |
||||
), |
||||
IconButton( |
||||
icon: Icon(Icons.highlight_off), |
||||
iconSize: 35, |
||||
color: Colors.black54, |
||||
onPressed: () { |
||||
//退出 |
||||
Navigator.pop(context); |
||||
}), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
||||
|
||||
//校验状态 |
||||
enum ClickWordCaptchaState { |
||||
normal, //默认 可自定义描述 |
||||
success, //成功 |
||||
fail, //失败 |
||||
none, //无状态 用于加载使用 |
||||
} |
||||
|
||||
//请求数据模型 |
||||
class ClickWordCaptchaModel { |
||||
String imgStr; //图表url 目前用base64 data |
||||
String jigsawImageBase64; //图表url 目前用base64 data |
||||
String token; // 获取的token 用于校验 |
||||
List wordList; //显示需要点选的字 |
||||
String wordStr; //显示需要点选的字转换为字符串 |
||||
String secretKey; //加密key |
||||
|
||||
ClickWordCaptchaModel( |
||||
{this.imgStr = "", |
||||
this.jigsawImageBase64 = "", |
||||
this.token = "", |
||||
this.secretKey = "", |
||||
this.wordList = const [], |
||||
this.wordStr = ""}); |
||||
|
||||
//解析数据转换模型 |
||||
static ClickWordCaptchaModel fromMap(Map<String, dynamic> map) { |
||||
ClickWordCaptchaModel captchaModel = ClickWordCaptchaModel(); |
||||
captchaModel.imgStr = map["originalImageBase64"] ?? ""; |
||||
captchaModel.jigsawImageBase64 = map["jigsawImageBase64"] ?? ""; |
||||
captchaModel.token = map["token"] ?? ""; |
||||
captchaModel.secretKey = map["secretKey"] ?? ""; |
||||
captchaModel.wordList = map["wordList"] ?? []; |
||||
|
||||
if (!CaptchaUtil.isListEmpty(captchaModel.wordList)) { |
||||
captchaModel.wordStr = captchaModel.wordList.join(","); |
||||
} |
||||
|
||||
return captchaModel; |
||||
} |
||||
|
||||
//将模型转换 |
||||
Map<String, dynamic> toJson() { |
||||
var map = new Map<String, dynamic>(); |
||||
map['imgStr'] = imgStr; |
||||
map['jigsawImageBase64'] = jigsawImageBase64; |
||||
map['token'] = token; |
||||
map['secretKey'] = token; |
||||
map['wordList'] = wordList; |
||||
map['wordStr'] = wordStr; |
||||
return map; |
||||
} |
||||
|
||||
@override |
||||
String toString() { |
||||
// TODO: implement toString |
||||
return JsonEncoder.withIndent(' ').convert(toJson()); |
||||
} |
||||
} |
@ -0,0 +1,86 @@
|
||||
import 'dart:convert'; |
||||
import 'package:steel_crypt/steel_crypt.dart'; |
||||
import 'package:convert/convert.dart'; |
||||
import 'package:crypto/crypto.dart'; |
||||
|
||||
class CaptchaUtil{ |
||||
///aes加密 |
||||
/// [key]AesCrypt加密key |
||||
/// [content] 需要加密的内容字符串 |
||||
static String aesEncode({String key, String content}) { |
||||
var aesCrypt = AesCrypt( |
||||
key: base64UrlEncode(key.codeUnits), padding: PaddingAES.pkcs7); |
||||
return aesCrypt.ecb.encrypt(inp: content); |
||||
} |
||||
|
||||
///aes解密 |
||||
/// [key]aes解密key |
||||
/// [content] 需要加密的内容字符串 |
||||
static String aesDecode({String key, String content}) { |
||||
var aesCrypt = AesCrypt( |
||||
key: base64UrlEncode(key.codeUnits), padding: PaddingAES.pkcs7); |
||||
return aesCrypt.ecb.decrypt(enc: content); |
||||
} |
||||
/// isEmpty. |
||||
static bool isEmpty(Object value) { |
||||
if (value == null) return true; |
||||
if (value is String && value.isEmpty) { |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
//list length == 0 || list == null |
||||
static bool isListEmpty(Object value) { |
||||
if (value == null) return true; |
||||
if (value is List && value.length == 0) { |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
static String jsonFormat(Map<dynamic, dynamic> map) { |
||||
Map _map = Map<String, Object>.from(map); |
||||
JsonEncoder encoder = JsonEncoder.withIndent(' '); |
||||
return encoder.convert(_map); |
||||
} |
||||
|
||||
|
||||
static String generateMd5(String data){ |
||||
var content = new Utf8Encoder().convert(data); |
||||
var digest = md5.convert(content); |
||||
return hex.encode(digest.bytes); |
||||
} |
||||
|
||||
static signData(Object params, tokenStr) async { |
||||
var time = new DateTime.now().millisecondsSinceEpoch; |
||||
String token = tokenStr; |
||||
Map<String, dynamic> reqData = new Map(); |
||||
Map<String, dynamic> paramsObj = new Map(); |
||||
paramsObj = params as Map<String, dynamic>; |
||||
var arr = []; |
||||
//将字典转成数组 |
||||
paramsObj?.forEach((key, value) => arr.add(key)); |
||||
//进行签名校验 |
||||
Map cr = new Map(); |
||||
cr['token'] = token; |
||||
cr['time'] = time.toString(); |
||||
cr['reqData'] = json.encode(paramsObj); |
||||
var array = []; |
||||
cr.forEach((key, value) => array.add(key)); |
||||
array.sort(); |
||||
var str = ''; |
||||
for (var i = 0; i < array.length; i++) { |
||||
var key = array[i]; |
||||
var value = cr[key]; |
||||
str += key + value; |
||||
} |
||||
|
||||
reqData["time"] = time; |
||||
reqData["token"] = token; |
||||
reqData['reqData'] = params; |
||||
reqData['sign'] = generateMd5(str); |
||||
|
||||
return reqData; |
||||
} |
||||
} |
@ -0,0 +1,133 @@
|
||||
import 'dart:async'; |
||||
|
||||
import 'package:flutter/widgets.dart'; |
||||
|
||||
import 'captcha_util.dart'; |
||||
/** |
||||
* @Author: thl |
||||
* @GitHub: https://github.com/Sky24n |
||||
* @Email: 863764940@qq.com |
||||
* @Email: sky24no@gmail.com |
||||
* @Description: Widget Util. |
||||
* @Date: 2018/9/10 |
||||
*/ |
||||
|
||||
/// Widget Util. |
||||
class WidgetUtil { |
||||
bool _hasMeasured = false; |
||||
double _width; |
||||
double _height; |
||||
|
||||
/// Widget rendering listener. |
||||
/// Widget渲染监听. |
||||
/// context: Widget context. |
||||
/// isOnce: true,Continuous monitoring false,Listen only once. |
||||
/// onCallBack: Widget Rect CallBack. |
||||
void asyncPrepare( |
||||
BuildContext context, bool isOnce, ValueChanged<Rect> onCallBack) { |
||||
if (_hasMeasured) return; |
||||
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { |
||||
RenderObject box = context.findRenderObject(); |
||||
if (box != null) { |
||||
if (isOnce) _hasMeasured = true; |
||||
double width = box.semanticBounds.width; |
||||
double height = box.semanticBounds.height; |
||||
if (_width != width || _height != height) { |
||||
_width = width; |
||||
_height = height; |
||||
if (onCallBack != null) onCallBack(box.semanticBounds); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/// Widget渲染监听. |
||||
void asyncPrepares(bool isOnce, ValueChanged<Rect> onCallBack) { |
||||
if (_hasMeasured) return; |
||||
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { |
||||
if (isOnce) _hasMeasured = true; |
||||
if (onCallBack != null) onCallBack(null); |
||||
}); |
||||
} |
||||
|
||||
///get Widget Bounds (width, height, left, top, right, bottom and so on).Widgets must be rendered completely. |
||||
///获取widget Rect |
||||
static Rect getWidgetBounds(BuildContext context) { |
||||
RenderObject box = context.findRenderObject(); |
||||
return (box != null) ? box.semanticBounds : Rect.zero; |
||||
} |
||||
|
||||
///Get the coordinates of the widget on the screen.Widgets must be rendered completely. |
||||
///获取widget在屏幕上的坐标,widget必须渲染完成 |
||||
static Offset getWidgetLocalToGlobal(BuildContext context) { |
||||
RenderBox box = context.findRenderObject() as RenderBox; |
||||
return box == null ? Offset.zero : box.localToGlobal(Offset.zero); |
||||
} |
||||
|
||||
/// get image width height,load error return Rect.zero.(unit px) |
||||
/// 获取图片宽高,加载错误情况返回 Rect.zero.(单位 px) |
||||
/// image |
||||
/// url network |
||||
/// local url , package |
||||
static Future<Rect> getImageWH( |
||||
{Image image, String url, String localUrl, String package}) { |
||||
if (CaptchaUtil.isEmpty(image) && |
||||
CaptchaUtil.isEmpty(url) && |
||||
CaptchaUtil.isEmpty(localUrl)) { |
||||
return Future.value(Rect.zero); |
||||
} |
||||
Completer<Rect> completer = Completer<Rect>(); |
||||
Image img = image ?? ((url != null && url.isNotEmpty) |
||||
? Image.network(url) |
||||
: Image.asset(localUrl, package: package)); |
||||
img.image |
||||
.resolve(const ImageConfiguration()) |
||||
.addListener(ImageStreamListener( |
||||
(ImageInfo info, bool _) { |
||||
completer.complete(Rect.fromLTWH(0, 0, info.image.width.toDouble(), |
||||
info.image.height.toDouble())); |
||||
}, |
||||
onError: (Object exception, StackTrace stackTrace) { |
||||
completer.completeError(exception, stackTrace); |
||||
}, |
||||
)); |
||||
return completer.future; |
||||
} |
||||
|
||||
/// get image width height, load error throw exception.(unit px) |
||||
/// 获取图片宽高,加载错误会抛出异常.(单位 px) |
||||
/// image |
||||
/// url network |
||||
/// local url (full path/全路径,example:"assets/images/ali_connors.png",""assets/images/3.0x/ali_connors.png"" ); |
||||
/// package |
||||
static Future<Rect> getImageWHE( |
||||
{Image image, |
||||
String url, |
||||
String localUrl, |
||||
String package}) { |
||||
if (CaptchaUtil.isEmpty(image) && |
||||
CaptchaUtil.isEmpty(url) && |
||||
CaptchaUtil.isEmpty(localUrl)) { |
||||
return Future.error("image is null."); |
||||
} |
||||
Completer<Rect> completer = Completer<Rect>(); |
||||
Image img = image != null |
||||
? image |
||||
: ((url != null && url.isNotEmpty) |
||||
? Image.network(url) |
||||
: Image.asset(localUrl, package: package)); |
||||
img.image |
||||
.resolve(const ImageConfiguration()) |
||||
.addListener(ImageStreamListener( |
||||
(ImageInfo info, bool _) { |
||||
completer.complete(Rect.fromLTWH(0, 0, info.image.width.toDouble(), |
||||
info.image.height.toDouble())); |
||||
}, |
||||
onError: (Object exception, StackTrace stackTrace) { |
||||
completer.completeError(exception, stackTrace); |
||||
}, |
||||
)); |
||||
|
||||
return completer.future; |
||||
} |
||||
} |
Loading…
Reference in new issue