slider-captcha-sdk 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +26 -55
- package/dist/index.esm.js +498 -96
- package/dist/index.min.js +1 -1
- package/dist/password-validator.d.ts +83 -4
- package/dist/password-validator.esm.js +223 -23
- package/dist/password-validator.min.js +1 -1
- package/dist/slider-captcha.d.ts +78 -8
- package/dist/slider-captcha.esm.js +263 -67
- package/dist/slider-captcha.min.js +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -33,8 +33,36 @@ class PopupSliderCaptcha {
|
|
|
33
33
|
return `:root{--sc-primary:#409eff;--sc-success:#67c23a;--sc-danger:#f56c6c;--sc-border:#e4e7eb;--sc-bg:linear-gradient(90deg, #f7f9fa 0%, #e8f4fd 100%);--sc-text:#333;--sc-text-light:#999;--sc-shadow:0 4px 20px rgba(0,0,0,.3);--sc-radius:8px;--sc-transition:.3s ease}.slider-captcha-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:9999;display:none;justify-content:center;align-items:center;opacity:0;transition:opacity var(--sc-transition)}.slider-captcha-overlay.show{opacity:1}.slider-captcha-modal{background:#fff;border-radius:var(--sc-radius);padding:20px;box-shadow:var(--sc-shadow);position:relative;max-width:90vw;max-height:90vh;transform:scale(.8) translateY(-20px);opacity:0;transition:all var(--sc-transition)}.slider-captcha-modal.show{transform:scale(1) translateY(0);opacity:1}.slider-captcha-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid var(--sc-border)}.slider-captcha-container{display:flex;align-items:center;position:relative;border-radius:4px;overflow:hidden;margin-bottom:15px;background:#837a7a;justify-content:center}.slider-captcha-track{width:100%;height:40px;line-height:40px;background:var(--sc-bg);border:1px solid var(--sc-border);border-radius:20px;position:relative;margin-bottom:15px;overflow:hidden}.slider-captcha-btn{width:38px;height:38px;background:#fff;border:1px solid #ccc;border-radius:50%;position:absolute;top:0;left:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all var(--sc-transition);user-select:none;z-index:1}.slider-captcha-loading{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,.6);display:flex;align-items:center;justify-content:center;flex-direction:column;color:#666;font-size:14px;z-index:10;border-radius:4px}.slider-captcha-error{color:var(--sc-danger);font-size:12px;text-align:center;margin-top:10px;display:none}.slider-captcha-title{margin:0;font-size:16px;color:var(--sc-text)}.slider-captcha-close,.slider-captcha-refresh{background:none;border:none;cursor:pointer;color:var(--sc-text-light);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all var(--sc-transition);position:relative;font-size:0}.slider-captcha-close::before,.slider-captcha-close::after{content:'';position:absolute;width:16px;height:2px;background-color:var(--sc-text-light);border-radius:1px;transition:all var(--sc-transition)}.slider-captcha-close::before{transform:rotate(45deg)}.slider-captcha-close::after{transform:rotate(-45deg)}.slider-captcha-close:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-close:hover::before,.slider-captcha-close:hover::after{background-color:var(--sc-danger)}.slider-captcha-refresh{margin-left:10px}.slider-captcha-refresh svg{width:20px;height:20px;fill:var(--sc-text-light);transition:all var(--sc-transition)}.slider-captcha-refresh:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-refresh:hover svg{fill:var(--sc-primary);transform:rotate(180deg)}.slider-captcha-floating-time{position:absolute;bottom:-40px;left:50%;transform:translateX(-50%);color:#fff;font-size:12px;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:2px 15px;border-radius:10px}.slider-captcha-floating-time.show{opacity:1;transform:translateX(-50%) translateY(-45px)}.slider-captcha-floating-time.success{color:var(--sc-success)}.slider-captcha-floating-time.fail{color:var(--sc-danger)}.slider-captcha-bg{width:100%;height:100%;object-fit:cover;display:block}.slider-captcha-piece{position:absolute;top:0;left:0;cursor:pointer;transition:none;z-index:2}.slider-captcha-finger{position:absolute;top:50%;left:10px;transform:translateY(-50%);font-size:20px;animation:fingerSlide 2s ease-in-out infinite;pointer-events:none;z-index:1;opacity:.6}.slider-captcha-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--sc-text-light);font-size:14px;pointer-events:none;z-index:1;transition:all var(--sc-transition)}.slider-captcha-header-buttons{display:flex;align-items:center}@keyframes fingerSlide{0%{left:10px;opacity:.6}50%{opacity:1}100%{left:calc(50% - 10px);opacity:.6}}`
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// 优化:添加常量定义,提高可维护性
|
|
37
|
+
static CONSTANTS = {
|
|
38
|
+
CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
|
|
39
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
40
|
+
DEFAULT_TIMEOUT: 30000,
|
|
41
|
+
ANIMATION_DURATION: 300,
|
|
42
|
+
FLOATING_TIME_DURATION: 2500,
|
|
43
|
+
THROTTLE_DELAY: 16
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 优化:添加错误类型枚举
|
|
47
|
+
static ERROR_TYPES = {
|
|
48
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
49
|
+
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
50
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
51
|
+
IMAGE_LOAD_ERROR: 'IMAGE_LOAD_ERROR',
|
|
52
|
+
CAPTCHA_DATA_ERROR: 'CAPTCHA_DATA_ERROR'
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
constructor(options = {}) {
|
|
37
|
-
|
|
56
|
+
// 优化:合并默认配置,使用常量
|
|
57
|
+
this.options = {
|
|
58
|
+
...PopupSliderCaptcha.DEFAULTS,
|
|
59
|
+
...options,
|
|
60
|
+
timeout: options.timeout || PopupSliderCaptcha.CONSTANTS.DEFAULT_TIMEOUT,
|
|
61
|
+
maxRetries: options.maxRetries || PopupSliderCaptcha.CONSTANTS.MAX_RETRY_ATTEMPTS,
|
|
62
|
+
throttleDelay: options.throttleDelay || PopupSliderCaptcha.CONSTANTS.THROTTLE_DELAY
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 优化:初始化所有属性
|
|
38
66
|
this.elements = {};
|
|
39
67
|
this.state = this.createInitialState();
|
|
40
68
|
this.captchaData = null;
|
|
@@ -45,11 +73,18 @@ class PopupSliderCaptcha {
|
|
|
45
73
|
this.rafId = null;
|
|
46
74
|
this.cachedDimensions = null;
|
|
47
75
|
this.imageCache = new Map();
|
|
76
|
+
this.abortController = null;
|
|
48
77
|
|
|
49
78
|
// 优化:使用箭头函数绑定this,避免重复bind调用
|
|
50
79
|
this.throttledHandleMove = this.throttle((e) => this.handleMove(e), this.options.throttleDelay);
|
|
51
80
|
|
|
52
|
-
|
|
81
|
+
// 优化:添加错误处理
|
|
82
|
+
try {
|
|
83
|
+
this.init();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('滑块验证码初始化失败:', error);
|
|
86
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message);
|
|
87
|
+
}
|
|
53
88
|
}
|
|
54
89
|
|
|
55
90
|
// 优化:提取初始状态创建为独立方法
|
|
@@ -376,7 +411,6 @@ class PopupSliderCaptcha {
|
|
|
376
411
|
show() {
|
|
377
412
|
this.state.isVisible = true;
|
|
378
413
|
this.elements.overlay.style.display = "flex";
|
|
379
|
-
this.elements.overlay.offsetHeight; // 强制重绘
|
|
380
414
|
|
|
381
415
|
requestAnimationFrame(() => {
|
|
382
416
|
this.elements.overlay.classList.add("show");
|
|
@@ -400,56 +434,158 @@ class PopupSliderCaptcha {
|
|
|
400
434
|
}, 300);
|
|
401
435
|
}
|
|
402
436
|
|
|
403
|
-
//
|
|
437
|
+
// 优化:添加统一错误处理方法
|
|
438
|
+
handleError(errorType, message, originalError = null) {
|
|
439
|
+
const errorMessages = {
|
|
440
|
+
[PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
|
|
441
|
+
[PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
|
|
442
|
+
[PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]: '验证失败,请重试',
|
|
443
|
+
[PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]: '图片加载失败,请刷新重试',
|
|
444
|
+
[PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]: '验证码数据错误,请刷新重试'
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const errorMessage = errorMessages[errorType] || message || '未知错误';
|
|
448
|
+
|
|
449
|
+
// 调用用户自定义错误处理
|
|
450
|
+
if (this.options.onError) {
|
|
451
|
+
this.options.onError({
|
|
452
|
+
type: errorType,
|
|
453
|
+
message: errorMessage,
|
|
454
|
+
originalError
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.showError(errorMessage);
|
|
459
|
+
console.error(`滑块验证码错误 [${errorType}]:`, errorMessage, originalError);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 优化:简化加载逻辑,增强错误处理
|
|
404
463
|
async loadCaptcha() {
|
|
405
464
|
try {
|
|
406
465
|
this.showLoading();
|
|
407
466
|
this.startTime = Date.now();
|
|
408
467
|
|
|
409
|
-
|
|
410
|
-
|
|
468
|
+
// 取消之前的请求
|
|
469
|
+
if (this.abortController) {
|
|
470
|
+
this.abortController.abort();
|
|
471
|
+
}
|
|
472
|
+
this.abortController = new AbortController();
|
|
411
473
|
|
|
412
474
|
const response = await fetch(this.options.apiUrl, {
|
|
413
475
|
method: "POST",
|
|
414
|
-
headers: {
|
|
415
|
-
|
|
416
|
-
|
|
476
|
+
headers: {
|
|
477
|
+
"Content-Type": "application/json",
|
|
478
|
+
...this.options.headers
|
|
479
|
+
},
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
place: 2,
|
|
482
|
+
timestamp: Date.now(),
|
|
483
|
+
...this.options.requestData
|
|
484
|
+
}),
|
|
485
|
+
signal: this.abortController.signal
|
|
417
486
|
});
|
|
418
487
|
|
|
419
|
-
|
|
488
|
+
if (!response.ok) {
|
|
489
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
490
|
+
}
|
|
420
491
|
|
|
421
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
422
492
|
const data = await response.json();
|
|
423
493
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this.showCaptcha();
|
|
427
|
-
await this.renderCaptcha();
|
|
428
|
-
} else {
|
|
494
|
+
// 优化:更严格的数据验证
|
|
495
|
+
if (!this.validateCaptchaData(data)) {
|
|
429
496
|
throw new Error("验证码数据格式错误")
|
|
430
497
|
}
|
|
498
|
+
|
|
499
|
+
this.captchaData = data.data;
|
|
500
|
+
this.showCaptcha();
|
|
501
|
+
await this.renderCaptcha();
|
|
431
502
|
} catch (error) {
|
|
432
|
-
|
|
433
|
-
|
|
503
|
+
if (error.name === 'AbortError') {
|
|
504
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '请求被取消');
|
|
505
|
+
} else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
|
|
506
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
|
|
507
|
+
} else {
|
|
508
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR, error.message, error);
|
|
509
|
+
}
|
|
434
510
|
}
|
|
435
511
|
}
|
|
436
512
|
|
|
513
|
+
// 优化:添加验证码数据验证方法
|
|
514
|
+
validateCaptchaData(data) {
|
|
515
|
+
if (!data || typeof data !== 'object') return false
|
|
516
|
+
|
|
517
|
+
const requiredFields = ['canvasSrc', 'blockSrc', 'canvasWidth', 'canvasHeight', 'blockWidth', 'blockHeight', 'blockY', 'nonceStr'];
|
|
518
|
+
const dataObj = data.data || data;
|
|
519
|
+
|
|
520
|
+
return requiredFields.every(field => {
|
|
521
|
+
const value = dataObj[field];
|
|
522
|
+
return value !== null && value !== undefined && value !== ''
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
437
526
|
async renderCaptcha() {
|
|
438
527
|
return new Promise((resolve, reject) => {
|
|
439
|
-
let
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
528
|
+
let hasError = false;
|
|
529
|
+
|
|
530
|
+
// 优化:并行加载图片,提高性能
|
|
531
|
+
const loadPromises = [
|
|
532
|
+
this.loadImageAsync(this.elements.backgroundImg, this.captchaData.canvasSrc, {
|
|
533
|
+
width: this.captchaData.canvasWidth,
|
|
534
|
+
height: this.captchaData.canvasHeight
|
|
535
|
+
}),
|
|
536
|
+
this.loadImageAsync(this.elements.sliderImg, this.captchaData.blockSrc, {
|
|
537
|
+
width: this.captchaData.blockWidth,
|
|
538
|
+
height: this.captchaData.blockHeight,
|
|
539
|
+
top: this.captchaData.blockY
|
|
540
|
+
})
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
Promise.all(loadPromises)
|
|
544
|
+
.then(() => {
|
|
545
|
+
if (!hasError) {
|
|
546
|
+
this.hideLoading();
|
|
547
|
+
resolve();
|
|
548
|
+
}
|
|
549
|
+
})
|
|
550
|
+
.catch((error) => {
|
|
551
|
+
if (!hasError) {
|
|
552
|
+
hasError = true;
|
|
553
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR, '图片加载失败', error);
|
|
554
|
+
reject(error);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 优化:添加异步图片加载方法
|
|
561
|
+
async loadImageAsync(imgElement, src, styles) {
|
|
562
|
+
return new Promise((resolve, reject) => {
|
|
563
|
+
// 检查缓存
|
|
564
|
+
if (this.imageCache.has(src)) {
|
|
565
|
+
const cachedImg = this.imageCache.get(src);
|
|
566
|
+
imgElement.src = cachedImg.src;
|
|
567
|
+
this.applyStyles(imgElement, styles);
|
|
568
|
+
resolve();
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const timeoutId = this.safeSetTimeout(() => {
|
|
573
|
+
reject(new Error('图片加载超时'));
|
|
574
|
+
}, 10000); // 10秒超时
|
|
575
|
+
|
|
576
|
+
imgElement.onload = () => {
|
|
577
|
+
this.safeClearTimeout(timeoutId);
|
|
578
|
+
this.imageCache.set(src, imgElement.cloneNode());
|
|
579
|
+
resolve();
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
imgElement.onerror = (error) => {
|
|
583
|
+
this.safeClearTimeout(timeoutId);
|
|
584
|
+
reject(error);
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
imgElement.src = src;
|
|
588
|
+
this.applyStyles(imgElement, styles);
|
|
453
589
|
})
|
|
454
590
|
}
|
|
455
591
|
|
|
@@ -536,38 +672,68 @@ class PopupSliderCaptcha {
|
|
|
536
672
|
}
|
|
537
673
|
|
|
538
674
|
try {
|
|
539
|
-
|
|
540
|
-
|
|
675
|
+
// 取消之前的验证请求
|
|
676
|
+
if (this.abortController) {
|
|
677
|
+
this.abortController.abort();
|
|
678
|
+
}
|
|
679
|
+
this.abortController = new AbortController();
|
|
541
680
|
|
|
542
681
|
const response = await fetch(this.options.verifyUrl, {
|
|
543
682
|
method: "POST",
|
|
544
|
-
headers: {
|
|
683
|
+
headers: {
|
|
684
|
+
"Content-Type": "application/json",
|
|
685
|
+
...this.options.headers
|
|
686
|
+
},
|
|
545
687
|
body: JSON.stringify({
|
|
546
688
|
loginVo: {
|
|
547
689
|
nonceStr: this.captchaData.nonceStr,
|
|
548
690
|
value: this.getPosition()
|
|
549
691
|
},
|
|
550
|
-
dragEventList: [...this.times]
|
|
692
|
+
dragEventList: [...this.times],
|
|
693
|
+
...this.options.verifyData
|
|
551
694
|
}),
|
|
552
|
-
signal:
|
|
695
|
+
signal: this.abortController.signal
|
|
553
696
|
});
|
|
554
697
|
|
|
555
|
-
|
|
698
|
+
if (!response.ok) {
|
|
699
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
700
|
+
}
|
|
556
701
|
|
|
557
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
558
702
|
const data = await response.json();
|
|
559
703
|
|
|
560
|
-
|
|
561
|
-
|
|
704
|
+
// 优化:更灵活的验证结果判断
|
|
705
|
+
if (this.isVerifySuccess(data)) {
|
|
706
|
+
this.onVerifySuccess(data.data || data.result);
|
|
562
707
|
} else {
|
|
563
|
-
this.onVerifyFail(data.message || "验证失败,请重试!");
|
|
708
|
+
this.onVerifyFail(data.message || data.msg || "验证失败,请重试!");
|
|
564
709
|
}
|
|
565
710
|
} catch (error) {
|
|
566
|
-
|
|
567
|
-
|
|
711
|
+
if (error.name === 'AbortError') {
|
|
712
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '验证请求被取消');
|
|
713
|
+
} else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
|
|
714
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
|
|
715
|
+
} else {
|
|
716
|
+
this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message, error);
|
|
717
|
+
}
|
|
568
718
|
}
|
|
569
719
|
}
|
|
570
720
|
|
|
721
|
+
// 优化:添加验证成功判断方法
|
|
722
|
+
isVerifySuccess(data) {
|
|
723
|
+
if (!data || typeof data !== 'object') return false
|
|
724
|
+
|
|
725
|
+
// 支持多种成功标识
|
|
726
|
+
const successIndicators = [
|
|
727
|
+
data.code === "0",
|
|
728
|
+
data.code === 0,
|
|
729
|
+
data.success === true,
|
|
730
|
+
data.status === 'success',
|
|
731
|
+
data.result === true
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
return successIndicators.some(indicator => indicator === true)
|
|
735
|
+
}
|
|
736
|
+
|
|
571
737
|
onVerifySuccess(ticket) {
|
|
572
738
|
const duration = Date.now() - this.startTime;
|
|
573
739
|
const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
|
|
@@ -708,36 +874,66 @@ class PopupSliderCaptcha {
|
|
|
708
874
|
}
|
|
709
875
|
|
|
710
876
|
destroy() {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
877
|
+
try {
|
|
878
|
+
// 取消所有进行中的请求
|
|
879
|
+
if (this.abortController) {
|
|
880
|
+
this.abortController.abort();
|
|
881
|
+
this.abortController = null;
|
|
882
|
+
}
|
|
714
883
|
|
|
715
|
-
|
|
716
|
-
|
|
884
|
+
// 恢复body样式
|
|
885
|
+
if (document.body) {
|
|
886
|
+
document.body.style.userSelect = "";
|
|
887
|
+
document.body.style.cursor = "";
|
|
888
|
+
}
|
|
717
889
|
|
|
718
|
-
|
|
719
|
-
|
|
890
|
+
// 清理所有定时器
|
|
891
|
+
this.clearAllTimers();
|
|
720
892
|
|
|
721
|
-
|
|
722
|
-
|
|
893
|
+
// 移除所有事件监听器
|
|
894
|
+
this.removeAllEventListeners();
|
|
723
895
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
this.elements.overlay.parentNode.removeChild(this.elements.overlay);
|
|
727
|
-
}
|
|
896
|
+
// 清理图片资源
|
|
897
|
+
this.cleanupImages();
|
|
728
898
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
899
|
+
// 移除DOM元素
|
|
900
|
+
if (this.elements?.overlay?.parentNode) {
|
|
901
|
+
this.elements.overlay.parentNode.removeChild(this.elements.overlay);
|
|
902
|
+
}
|
|
734
903
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (
|
|
738
|
-
|
|
904
|
+
// 清理样式表(只有在没有其他实例使用时才删除)
|
|
905
|
+
const styleElement = document.getElementById('slider-captcha-styles');
|
|
906
|
+
if (styleElement && !this.hasOtherInstances()) {
|
|
907
|
+
styleElement.remove();
|
|
739
908
|
}
|
|
740
|
-
|
|
909
|
+
|
|
910
|
+
// 清空缓存
|
|
911
|
+
this.imageCache.clear();
|
|
912
|
+
this.cachedDimensions = null;
|
|
913
|
+
|
|
914
|
+
// 清空所有属性
|
|
915
|
+
Object.keys(this).forEach(key => {
|
|
916
|
+
if (key !== 'constructor') {
|
|
917
|
+
this[key] = null;
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// 调用销毁回调
|
|
922
|
+
if (this.options.onDestroy) {
|
|
923
|
+
this.options.onDestroy();
|
|
924
|
+
}
|
|
925
|
+
} catch (error) {
|
|
926
|
+
console.error('销毁滑块验证码时出错:', error);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// 优化:检查是否有其他实例在使用样式
|
|
931
|
+
hasOtherInstances() {
|
|
932
|
+
// 简单的检查方式,可以通过全局计数器实现
|
|
933
|
+
if (typeof window !== 'undefined') {
|
|
934
|
+
return window.sliderCaptchaInstanceCount > 1
|
|
935
|
+
}
|
|
936
|
+
return false
|
|
741
937
|
}
|
|
742
938
|
|
|
743
939
|
static create(options) {
|
|
@@ -762,27 +958,48 @@ if (typeof module !== "undefined" && module.exports) {
|
|
|
762
958
|
window.SliderCaptcha = PopupSliderCaptcha;
|
|
763
959
|
}
|
|
764
960
|
|
|
961
|
+
// Add ES6 export for modern module systems
|
|
962
|
+
var PopupSliderCaptcha$1 = PopupSliderCaptcha;
|
|
963
|
+
|
|
765
964
|
/**
|
|
766
965
|
* 密码校验工具类
|
|
767
966
|
* 提供密码加密和校验功能
|
|
768
967
|
*/
|
|
769
968
|
class PasswordValidator {
|
|
969
|
+
// 优化:添加常量定义
|
|
970
|
+
static CONSTANTS = {
|
|
971
|
+
DEFAULT_TIMEOUT: 10000,
|
|
972
|
+
CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
|
|
973
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
974
|
+
MIN_PASSWORD_LENGTH: 1
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// 优化:添加错误类型枚举
|
|
978
|
+
static ERROR_TYPES = {
|
|
979
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
980
|
+
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
981
|
+
ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
|
|
982
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
983
|
+
PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR'
|
|
984
|
+
}
|
|
985
|
+
|
|
770
986
|
constructor(options = {}) {
|
|
987
|
+
// 优化:合并配置,使用常量
|
|
771
988
|
this.options = {
|
|
772
|
-
// 获取公钥的接口地址
|
|
773
989
|
publicKeyUrl: options.publicKeyUrl || '/microservice/strongPassword/getPublicKey',
|
|
774
|
-
// 密码校验接口地址
|
|
775
990
|
validateUrl: options.validateUrl || '/microservice/strongPassword/checkPassword',
|
|
776
|
-
|
|
777
|
-
timeout: options.timeout || 10000,
|
|
778
|
-
// 自定义请求头
|
|
991
|
+
timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
|
|
779
992
|
headers: options.headers || {},
|
|
993
|
+
retryAttempts: options.retryAttempts || PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,
|
|
994
|
+
cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
|
|
780
995
|
...options
|
|
781
996
|
};
|
|
782
997
|
|
|
783
998
|
// 缓存公钥,避免重复请求
|
|
784
999
|
this.publicKeyCache = null;
|
|
785
1000
|
this.publicKeyExpiry = null;
|
|
1001
|
+
this.retryCount = 0;
|
|
1002
|
+
this.abortController = null;
|
|
786
1003
|
}
|
|
787
1004
|
|
|
788
1005
|
/**
|
|
@@ -790,31 +1007,94 @@ class PasswordValidator {
|
|
|
790
1007
|
* @returns {Promise<string>} 公钥字符串
|
|
791
1008
|
*/
|
|
792
1009
|
async getPublicKey() {
|
|
793
|
-
//
|
|
1010
|
+
// 检查缓存是否有效
|
|
794
1011
|
if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
|
|
795
1012
|
return this.publicKeyCache
|
|
796
1013
|
}
|
|
797
1014
|
|
|
798
1015
|
try {
|
|
1016
|
+
// 取消之前的请求
|
|
1017
|
+
if (this.abortController) {
|
|
1018
|
+
this.abortController.abort();
|
|
1019
|
+
}
|
|
1020
|
+
this.abortController = new AbortController();
|
|
1021
|
+
|
|
799
1022
|
const response = await this.makeRequest(this.options.publicKeyUrl, {
|
|
800
|
-
method: 'GET'
|
|
1023
|
+
method: 'GET',
|
|
1024
|
+
signal: this.abortController.signal
|
|
801
1025
|
});
|
|
802
1026
|
|
|
803
|
-
|
|
804
|
-
|
|
1027
|
+
// 优化:更灵活的成功判断
|
|
1028
|
+
if (!this.isSuccessResponse(response)) {
|
|
1029
|
+
throw new Error('获取公钥失败:' + (response.message || response.msg || '未知错误'))
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 验证公钥格式
|
|
1033
|
+
const publicKey = response.data || response.result;
|
|
1034
|
+
if (!this.validatePublicKey(publicKey)) {
|
|
1035
|
+
throw new Error('公钥格式无效')
|
|
805
1036
|
}
|
|
806
1037
|
|
|
807
|
-
//
|
|
808
|
-
this.publicKeyCache =
|
|
809
|
-
this.publicKeyExpiry = Date.now() +
|
|
1038
|
+
// 缓存公钥
|
|
1039
|
+
this.publicKeyCache = publicKey;
|
|
1040
|
+
this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
|
|
810
1041
|
|
|
811
1042
|
return this.publicKeyCache
|
|
812
1043
|
} catch (error) {
|
|
813
1044
|
console.error('获取公钥失败:', error);
|
|
1045
|
+
this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
|
|
814
1046
|
throw new Error('获取公钥失败: ' + error.message)
|
|
815
1047
|
}
|
|
816
1048
|
}
|
|
817
1049
|
|
|
1050
|
+
// 优化:添加响应成功判断方法
|
|
1051
|
+
isSuccessResponse(response) {
|
|
1052
|
+
if (!response || typeof response !== 'object') return false
|
|
1053
|
+
|
|
1054
|
+
const successIndicators = [
|
|
1055
|
+
response.code === '0',
|
|
1056
|
+
response.code === 0,
|
|
1057
|
+
response.success === true,
|
|
1058
|
+
response.status === 'success',
|
|
1059
|
+
response.result === true
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
return successIndicators.some(indicator => indicator === true)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 优化:添加公钥验证方法
|
|
1066
|
+
validatePublicKey(publicKey) {
|
|
1067
|
+
if (!publicKey || typeof publicKey !== 'string') return false
|
|
1068
|
+
|
|
1069
|
+
// 基本的RSA公钥格式检查
|
|
1070
|
+
const rsaPattern = /^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/;
|
|
1071
|
+
return rsaPattern.test(publicKey.trim())
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 优化:添加统一错误处理方法
|
|
1075
|
+
handleError(errorType, message, originalError = null) {
|
|
1076
|
+
const errorMessages = {
|
|
1077
|
+
[PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
|
|
1078
|
+
[PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
|
|
1079
|
+
[PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
|
|
1080
|
+
[PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
|
|
1081
|
+
[PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败'
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const errorMessage = errorMessages[errorType] || message || '未知错误';
|
|
1085
|
+
|
|
1086
|
+
// 调用用户自定义错误处理
|
|
1087
|
+
if (this.options.onError) {
|
|
1088
|
+
this.options.onError({
|
|
1089
|
+
type: errorType,
|
|
1090
|
+
message: errorMessage,
|
|
1091
|
+
originalError
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
818
1098
|
/**
|
|
819
1099
|
* 使用RSA公钥加密密码
|
|
820
1100
|
* @param {string} password 原始密码
|
|
@@ -823,9 +1103,22 @@ class PasswordValidator {
|
|
|
823
1103
|
*/
|
|
824
1104
|
encryptPassword(password, publicKey) {
|
|
825
1105
|
try {
|
|
1106
|
+
// 优化:输入验证
|
|
1107
|
+
if (!password || typeof password !== 'string') {
|
|
1108
|
+
throw new Error('密码不能为空')
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
|
|
1112
|
+
throw new Error('密码长度不足')
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (!publicKey || typeof publicKey !== 'string') {
|
|
1116
|
+
throw new Error('公钥不能为空')
|
|
1117
|
+
}
|
|
1118
|
+
|
|
826
1119
|
// 使用导入的JSEncrypt进行RSA加密
|
|
827
|
-
if (
|
|
828
|
-
throw new Error('JSEncrypt
|
|
1120
|
+
if (typeof JSEncrypt === 'undefined') {
|
|
1121
|
+
throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
|
|
829
1122
|
}
|
|
830
1123
|
|
|
831
1124
|
const encrypt = new JSEncrypt();
|
|
@@ -833,12 +1126,13 @@ class PasswordValidator {
|
|
|
833
1126
|
const encrypted = encrypt.encrypt(password);
|
|
834
1127
|
|
|
835
1128
|
if (!encrypted) {
|
|
836
|
-
throw new Error('
|
|
1129
|
+
throw new Error('密码加密失败,可能是公钥格式不正确')
|
|
837
1130
|
}
|
|
838
1131
|
|
|
839
1132
|
return encrypted
|
|
840
1133
|
} catch (error) {
|
|
841
1134
|
console.error('密码加密失败:', error);
|
|
1135
|
+
this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
|
|
842
1136
|
throw new Error('密码加密失败: ' + error.message)
|
|
843
1137
|
}
|
|
844
1138
|
}
|
|
@@ -846,12 +1140,24 @@ class PasswordValidator {
|
|
|
846
1140
|
/**
|
|
847
1141
|
* 校验密码
|
|
848
1142
|
* @param {string} password 原始密码
|
|
849
|
-
* @param {string} userName
|
|
1143
|
+
* @param {string} userName 用户名
|
|
850
1144
|
* @param {Object} additionalData 额外的校验数据
|
|
851
1145
|
* @returns {Promise<Object>} 校验结果
|
|
852
1146
|
*/
|
|
853
1147
|
async validatePassword(password, userName, additionalData = {}) {
|
|
854
1148
|
try {
|
|
1149
|
+
// 优化:输入验证
|
|
1150
|
+
if (!password || typeof password !== 'string') {
|
|
1151
|
+
throw new Error('密码不能为空')
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (!userName || typeof userName !== 'string') {
|
|
1155
|
+
throw new Error('用户名不能为空')
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// 重置重试计数
|
|
1159
|
+
this.retryCount = 0;
|
|
1160
|
+
|
|
855
1161
|
// 1. 获取公钥
|
|
856
1162
|
const publicKey = await this.getPublicKey();
|
|
857
1163
|
|
|
@@ -869,15 +1175,34 @@ class PasswordValidator {
|
|
|
869
1175
|
})
|
|
870
1176
|
});
|
|
871
1177
|
|
|
1178
|
+
// 优化:统一返回格式
|
|
872
1179
|
return {
|
|
873
|
-
|
|
1180
|
+
success: this.isSuccessResponse(response),
|
|
1181
|
+
data: response.data || response.result,
|
|
1182
|
+
message: response.message || response.msg || '验证完成',
|
|
1183
|
+
code: response.code || response.status,
|
|
1184
|
+
originalResponse: response
|
|
874
1185
|
}
|
|
875
1186
|
} catch (error) {
|
|
876
1187
|
console.error('密码校验失败:', error);
|
|
1188
|
+
|
|
1189
|
+
// 优化:根据错误类型返回不同的错误信息
|
|
1190
|
+
let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
|
|
1191
|
+
if (error.message.includes('网络') || error.message.includes('fetch')) {
|
|
1192
|
+
errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
|
|
1193
|
+
} else if (error.message.includes('超时') || error.message.includes('timeout')) {
|
|
1194
|
+
errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
|
|
1195
|
+
} else if (error.message.includes('加密')) {
|
|
1196
|
+
errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
this.handleError(errorType, error.message, error);
|
|
1200
|
+
|
|
877
1201
|
return {
|
|
878
1202
|
success: false,
|
|
879
|
-
message: error.
|
|
880
|
-
code:
|
|
1203
|
+
message: error.message || '密码校验失败',
|
|
1204
|
+
code: errorType,
|
|
1205
|
+
error: error
|
|
881
1206
|
}
|
|
882
1207
|
}
|
|
883
1208
|
}
|
|
@@ -889,8 +1214,15 @@ class PasswordValidator {
|
|
|
889
1214
|
* @returns {Promise<Object>} 响应结果
|
|
890
1215
|
*/
|
|
891
1216
|
async makeRequest(url, options = {}) {
|
|
892
|
-
|
|
893
|
-
|
|
1217
|
+
// 优化:使用实例的abortController
|
|
1218
|
+
if (this.abortController) {
|
|
1219
|
+
this.abortController.abort();
|
|
1220
|
+
}
|
|
1221
|
+
this.abortController = new AbortController();
|
|
1222
|
+
|
|
1223
|
+
const timeoutId = setTimeout(() => {
|
|
1224
|
+
this.abortController.abort();
|
|
1225
|
+
}, this.options.timeout);
|
|
894
1226
|
|
|
895
1227
|
try {
|
|
896
1228
|
const response = await fetch(url, {
|
|
@@ -900,7 +1232,7 @@ class PasswordValidator {
|
|
|
900
1232
|
...this.options.headers,
|
|
901
1233
|
...options.headers
|
|
902
1234
|
},
|
|
903
|
-
signal:
|
|
1235
|
+
signal: this.abortController.signal
|
|
904
1236
|
});
|
|
905
1237
|
|
|
906
1238
|
clearTimeout(timeoutId);
|
|
@@ -909,14 +1241,27 @@ class PasswordValidator {
|
|
|
909
1241
|
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
|
|
910
1242
|
}
|
|
911
1243
|
|
|
912
|
-
|
|
1244
|
+
const data = await response.json();
|
|
1245
|
+
|
|
1246
|
+
// 优化:记录请求日志(开发环境)
|
|
1247
|
+
if (this.options.debug) {
|
|
1248
|
+
console.log('密码校验器请求:', { url, options, response: data });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return data
|
|
913
1252
|
} catch (error) {
|
|
914
1253
|
clearTimeout(timeoutId);
|
|
915
1254
|
|
|
916
1255
|
if (error.name === 'AbortError') {
|
|
1256
|
+
this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
|
|
917
1257
|
throw new Error('请求超时')
|
|
918
1258
|
}
|
|
919
1259
|
|
|
1260
|
+
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
|
|
1261
|
+
this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
|
|
1262
|
+
throw new Error('网络连接失败')
|
|
1263
|
+
}
|
|
1264
|
+
|
|
920
1265
|
throw error
|
|
921
1266
|
}
|
|
922
1267
|
}
|
|
@@ -938,8 +1283,65 @@ class PasswordValidator {
|
|
|
938
1283
|
// 清除缓存,使用新配置重新获取
|
|
939
1284
|
this.clearCache();
|
|
940
1285
|
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* 取消当前请求
|
|
1289
|
+
*/
|
|
1290
|
+
cancelRequest() {
|
|
1291
|
+
if (this.abortController) {
|
|
1292
|
+
this.abortController.abort();
|
|
1293
|
+
this.abortController = null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* 检查是否正在请求中
|
|
1299
|
+
* @returns {boolean} 是否正在请求
|
|
1300
|
+
*/
|
|
1301
|
+
isRequesting() {
|
|
1302
|
+
return this.abortController && !this.abortController.signal.aborted
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* 获取缓存状态
|
|
1307
|
+
* @returns {Object} 缓存状态信息
|
|
1308
|
+
*/
|
|
1309
|
+
getCacheStatus() {
|
|
1310
|
+
return {
|
|
1311
|
+
hasCache: !!this.publicKeyCache,
|
|
1312
|
+
isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
|
|
1313
|
+
expiryTime: this.publicKeyExpiry,
|
|
1314
|
+
remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* 销毁实例,清理资源
|
|
1320
|
+
*/
|
|
1321
|
+
destroy() {
|
|
1322
|
+
try {
|
|
1323
|
+
// 取消所有进行中的请求
|
|
1324
|
+
this.cancelRequest();
|
|
1325
|
+
|
|
1326
|
+
// 清除缓存
|
|
1327
|
+
this.clearCache();
|
|
1328
|
+
|
|
1329
|
+
// 重置状态
|
|
1330
|
+
this.retryCount = 0;
|
|
1331
|
+
|
|
1332
|
+
// 调用销毁回调
|
|
1333
|
+
if (this.options.onDestroy) {
|
|
1334
|
+
this.options.onDestroy();
|
|
1335
|
+
}
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
console.error('销毁密码校验器时出错:', error);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
941
1340
|
}
|
|
942
1341
|
|
|
1342
|
+
// 导出类和创建实例的工厂函数
|
|
1343
|
+
var PasswordValidator$1 = PasswordValidator;
|
|
1344
|
+
|
|
943
1345
|
/**
|
|
944
1346
|
* 创建密码校验器实例的工厂函数
|
|
945
1347
|
* @param {Object} options 配置选项
|
|
@@ -967,19 +1369,19 @@ async function validatePassword(password, options = {}, additionalData = {}) {
|
|
|
967
1369
|
|
|
968
1370
|
// 默认导出(向后兼容)
|
|
969
1371
|
var index = {
|
|
970
|
-
PopupSliderCaptcha,
|
|
971
|
-
PasswordValidator,
|
|
1372
|
+
PopupSliderCaptcha: PopupSliderCaptcha$1,
|
|
1373
|
+
PasswordValidator: PasswordValidator$1,
|
|
972
1374
|
createPasswordValidator,
|
|
973
1375
|
validatePassword
|
|
974
1376
|
};
|
|
975
1377
|
|
|
976
1378
|
// 全局注册(用于UMD构建)
|
|
977
1379
|
if (typeof window !== 'undefined') {
|
|
978
|
-
window.SliderCaptcha = PopupSliderCaptcha;
|
|
979
|
-
window.PopupSliderCaptcha = PopupSliderCaptcha;
|
|
980
|
-
window.PasswordValidator = PasswordValidator;
|
|
1380
|
+
window.SliderCaptcha = PopupSliderCaptcha$1;
|
|
1381
|
+
window.PopupSliderCaptcha = PopupSliderCaptcha$1;
|
|
1382
|
+
window.PasswordValidator = PasswordValidator$1;
|
|
981
1383
|
window.createPasswordValidator = createPasswordValidator;
|
|
982
1384
|
window.validatePassword = validatePassword;
|
|
983
1385
|
}
|
|
984
1386
|
|
|
985
|
-
export { PasswordValidator, PopupSliderCaptcha, createPasswordValidator, index as default, validatePassword };
|
|
1387
|
+
export { PasswordValidator$1 as PasswordValidator, PopupSliderCaptcha$1 as PopupSliderCaptcha, createPasswordValidator, index as default, validatePassword };
|