slider-captcha-sdk 1.0.12 → 1.0.14
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 +502 -100
- package/dist/index.min.js +1 -1
- package/dist/password-validator.d.ts +83 -4
- package/dist/password-validator.esm.js +224 -24
- package/dist/password-validator.min.js +1 -1
- package/dist/slider-captcha.d.ts +78 -8
- package/dist/slider-captcha.esm.js +265 -69
- package/dist/slider-captcha.min.js +1 -1
- package/package.json +1 -1
package/dist/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jsencrypt")):"function"==typeof define&&define.amd?define(["exports","jsencrypt"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptchaSDK={},t.JSEncrypt)}(this,function(t,e){"use strict";function i(t){return new PasswordValidator(t)}async function s(t,e={},i={}){const s=new PasswordValidator(e),a=i.userName||"";return await s.validatePassword(t,a,i)}class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/api/captcha",verifyUrl:"/api/captcha/verify",throttleDelay:16,clickMaskClose:!1};static CSS_CLASSES={overlay:"slider-captcha-overlay",modal:"slider-captcha-modal",header:"slider-captcha-header",container:"slider-captcha-container",track:"slider-captcha-track",btn:"slider-captcha-btn",hint:"slider-captcha-hint",loading:"slider-captcha-loading",error:"slider-captcha-error"};static getStyles(){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}}"}constructor(t={}){this.options={...PopupSliderCaptcha.DEFAULTS,...t},this.elements={},this.state=this.createInitialState(),this.captchaData=null,this.times=[],this.startTime=null,this.eventListeners=[],this.timers=new Set,this.rafId=null,this.cachedDimensions=null,this.imageCache=new Map,this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay),this.init()}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=document.createElement("style");t.id="slider-captcha-styles",t.textContent=PopupSliderCaptcha.getStyles(),document.head.appendChild(t)}createElements(){const{elements:t,options:e}=this;[["overlay","div",PopupSliderCaptcha.CSS_CLASSES.overlay],["modal","div",PopupSliderCaptcha.CSS_CLASSES.modal],["header","div",PopupSliderCaptcha.CSS_CLASSES.header],["title","h3","slider-captcha-title","安全验证"],["closeBtn","button","slider-captcha-close"],["refreshBtn","button","slider-captcha-refresh"],["container","div",PopupSliderCaptcha.CSS_CLASSES.container],["backgroundImg","img","slider-captcha-bg"],["sliderImg","img","slider-captcha-piece"],["loadingText","div",PopupSliderCaptcha.CSS_CLASSES.loading,"加载中..."],["floatingTime","div","slider-captcha-floating-time"],["track","div",PopupSliderCaptcha.CSS_CLASSES.track],["fingerAnimation","div","slider-captcha-finger","👉"],["btn","div",PopupSliderCaptcha.CSS_CLASSES.btn],["icon","div","","→"],["hint","div",PopupSliderCaptcha.CSS_CLASSES.hint,"向右滑动完成验证"],["error","div",PopupSliderCaptcha.CSS_CLASSES.error]].forEach(([e,i,s,a])=>{t[e]=this.createElement(i,s,a)}),t.container.style.cssText=`width:${e.width}px;height:${e.height}px`,t.refreshBtn.innerHTML='<svg viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>',this.assembleDOM(),this.setInitialState()}createElement(t,e="",i=""){const s=document.createElement(t);return e&&(s.className=e),i&&(s.textContent=i),s}assembleDOM(){const{elements:t}=this,e=this.createElement("div","slider-captcha-header-buttons");e.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,e),t.container.append(t.backgroundImg,t.sliderImg,t.loadingText,t.floatingTime),t.btn.appendChild(t.icon),t.track.append(t.fingerAnimation,t.btn,t.hint),t.modal.append(t.header,t.container,t.track,t.error),t.overlay.appendChild(t.modal),document.body.appendChild(t.overlay)}setInitialState(){Object.assign(this.elements.container.style,{display:"none"}),Object.assign(this.elements.track.style,{display:"none"})}bindEvents(){const{elements:t}=this;this.addEventListener(t.closeBtn,"click",()=>this.hide()),this.addEventListener(t.refreshBtn,"click",()=>this.refresh()),this.addEventListener(t.overlay,"click",e=>{e.target===t.overlay&&this.options.clickMaskClose&&this.hide()}),this.addEventListener(document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}),this.addEventListener(document,"visibilitychange",()=>this.handleVisibilityChange()),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};this.addEventListener(t.btn,"mousedown",e.start),this.addEventListener(t.btn,"touchstart",e.start),this.addEventListener(t.sliderImg,"mousedown",e.start),this.addEventListener(t.sliderImg,"touchstart",e.start),this.addEventListener(document,"mousemove",e.move,{passive:!1}),this.addEventListener(document,"touchmove",e.move,{passive:!1}),this.addEventListener(document,"mouseup",e.end),this.addEventListener(document,"touchend",e.end)}addEventListener(t,e,i,s={}){t&&"function"==typeof i&&(t.addEventListener(e,i,s),this.eventListeners.push({element:t,event:e,handler:i,options:s}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:e,handler:i,options:s})=>{try{t?.removeEventListener?.(e,i,s)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const t=this.elements.track.offsetWidth,e=this.elements.btn.offsetWidth;this.cachedDimensions={trackWidth:t,btnWidth:e,maxX:t-e}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),e=this.state.currentX/t;return Math.round(e*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),this.state.isDragging=!0,this.state.startX=this.getClientX(t)-this.state.currentX,this.startTime=this.startTime||Date.now(),this.times=[{time:Date.now(),position:this.getPosition()}],this.setTransition(!1),this.updateUIState("dragging"),this.cachedDimensions=null)}handleMove(t){if(!this.state.isDragging)return;t.preventDefault();const e=this.getClientX(t)-this.state.startX,{maxX:i}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(e,i)),this.times.push({time:Date.now(),position:this.getPosition()}),this.rafId&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(()=>this.updateSliderPosition())}handleEnd(){this.state.isDragging&&(this.times.push({time:Date.now(),position:this.getPosition()}),this.state.isDragging=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.verify())}handleVisibilityChange(){const t=document.hidden?"paused":"running";this.elements.fingerAnimation&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX(t){return t.type.includes("touch")?t.touches[0].clientX:t.clientX}setTransition(t){const e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{this.elements.btn.style.transition=e,this.elements.sliderImg.style.transition=e})}updateUIState(t){const{elements:e}=this,i={dragging:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none"},success:()=>{Object.assign(e.btn.style,{background:"var(--sc-success)"}),Object.assign(e.icon.style,{innerHTML:"✓",color:"white"}),e.icon.innerHTML="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{innerHTML:"✗",color:"white"}),e.icon.innerHTML="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.innerHTML="→",e.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none",Object.assign(e.track.style,{pointerEvents:"none",opacity:"0.6"})}};i[t]&&requestAnimationFrame(()=>{i[t](),"loading"!==t&&Object.assign(e.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,e){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:e,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:e,state:i}=this,{maxX:s}=this.getDimensions(),a=i.currentX/s*(e.width-e.sliderSize),n=i.currentX/s;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${i.currentX}px)`,t.sliderImg.style.transform=`translateX(${a}px)`,t.fingerAnimation.style.opacity=.8>n?"0.6":"0"})}show(){this.state.isVisible=!0,this.elements.overlay.style.display="flex",this.elements.overlay.offsetHeight,requestAnimationFrame(()=>{this.elements.overlay.classList.add("show"),this.elements.modal.classList.add("show")}),this.loadCaptcha()}hide(){this.state.isVisible=!1,this.elements.overlay.classList.remove("show"),this.elements.modal.classList.remove("show"),this.safeSetTimeout(()=>{this.elements.overlay.style.display="none",document.body.removeChild(this.elements.overlay),this.reset(),this.options.onClose?.(),this.elements.overlay=null},300)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now();const t=new AbortController,e=this.safeSetTimeout(()=>t.abort(),this.options.timeout),i=await fetch(this.options.apiUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({place:2,timestamp:Date.now()}),signal:t.signal});if(this.safeClearTimeout(e),!i.ok)throw Error("HTTP "+i.status);const s=await i.json();if(!s.data?.canvasSrc||!s.data?.blockSrc)throw Error("验证码数据格式错误");this.captchaData=s.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){const e="AbortError"===t.name?"请求超时,请重试":"加载验证码失败: "+t.message;this.showError(e)}}async renderCaptcha(){return new Promise((t,e)=>{let i=0;const s=()=>2===++i&&(this.hideLoading(),t()),a=()=>e(Error("图片加载失败"));this.loadImage(this.elements.backgroundImg,this.captchaData.canvasSrc,{width:this.captchaData.canvasWidth,height:this.captchaData.canvasHeight},s,a),this.loadImage(this.elements.sliderImg,this.captchaData.blockSrc,{width:this.captchaData.blockWidth,height:this.captchaData.blockHeight,top:this.captchaData.blockY},s,a)})}loadImage(t,e,i,s,a){if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,i),void s()}t.onload=()=>{this.imageCache.set(e,t.cloneNode()),s()},t.onerror=a,t.src=e,this.applyStyles(t,i)}applyStyles(t,e){Object.entries(e).forEach(([e,i])=>{t.style[e]="number"==typeof i?i+"px":i})}showLoading(){this.state.isLoading=!0,this.batchUpdateStyles({container:{display:"block"},loadingText:{display:"flex"},error:{display:"none"}}),this.updateUIState("loading")}hideLoading(){this.state.isLoading=!1,this.batchUpdateStyles({loadingText:{display:"none"}}),this.updateUIState("reset")}showCaptcha(){this.batchUpdateStyles({container:{display:"block"},track:{display:"block"},error:{display:"none"}})}showError(t){this.hideLoading(),this.batchUpdateStyles({error:{display:"block",textContent:t}})}batchUpdateStyles(t){requestAnimationFrame(()=>{Object.entries(t).forEach(([t,e])=>{const i=this.elements[t];i&&Object.entries(e).forEach(([t,e])=>{"textContent"===t?i.textContent=e:i.style[t]=e})})})}async verify(){if(this.captchaData)try{const t=new AbortController,e=this.safeSetTimeout(()=>t.abort(),this.options.timeout),i=await fetch(this.options.verifyUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times]}),signal:t.signal});if(this.safeClearTimeout(e),!i.ok)throw Error("HTTP "+i.status);const s=await i.json();"0"===s.code||!0===s.success?this.onVerifySuccess():this.onVerifyFail(s.message||"验证失败,请重试!")}catch(t){const e="AbortError"===t.name?"验证超时,请重试":"网络错误";this.onVerifyFail(e)}else this.onVerifyFail("验证码数据丢失")}onVerifySuccess(){const t=Date.now()-this.startTime,e=`验证成功!耗时:${(t/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(e,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({captchaId:this.captchaData.captchaId,timestamp:Date.now(),duration:t}),this.hide()},2e3)}showFloatingTime(t,e="success"){const{elements:i}=this;i.floatingTime.textContent=t,i.floatingTime.className="slider-captcha-floating-time "+e,this.safeSetTimeout(()=>i.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{i.floatingTime.className="slider-captcha-floating-time"},2500)}onVerifyFail(t){this.state.retryCount++,this.updateUIState("fail"),this.showFloatingTime(t,"fail"),this.safeSetTimeout(()=>{this.state.retryCount<this.options.maxRetries?this.reset():this.refresh()},2500)}reset(){this.clearAllTimers(),Object.assign(this.state,{isDragging:!1,currentX:0,startX:0,isLoading:!1}),this.times=[],this.startTime=null,this.cachedDimensions=null,requestAnimationFrame(()=>{this.setTransition(!0),this.elements.btn.style.transform="translateX(0px)",this.elements.sliderImg.style.transform="translateX(0px)",this.updateUIState("reset"),this.elements.error.style.display="none"})}refresh(){this.reset(),this.state.retryCount=0,this.loadCaptcha()}safeSetTimeout(t,e){const i=setTimeout(()=>{this.timers.delete(i),t()},e);return this.timers.add(i),i}safeClearTimeout(t){t&&(clearTimeout(t),this.timers.delete(t))}clearAllTimers(){this.timers.forEach(t=>{clearTimeout(t),clearInterval(t)}),this.timers.clear(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null)}cleanupImages(){this.elements.backgroundImg&&(this.elements.backgroundImg.src="",this.elements.backgroundImg.onload=null,this.elements.backgroundImg.onerror=null),this.elements.sliderImg&&(this.elements.sliderImg.src="",this.elements.sliderImg.onload=null,this.elements.sliderImg.onerror=null),this.imageCache.clear()}throttle(t,e){let i=0;return function(...s){const a=Date.now();if(a-i>=e)return i=a,t.apply(this,s)}}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout(()=>t.apply(this,s),e)}}destroy(){document.body.style.userSelect="",document.body.style.cursor="",this.clearAllTimers(),this.removeAllEventListeners(),this.cleanupImages(),this.elements.overlay?.parentNode&&this.elements.overlay.parentNode.removeChild(this.elements.overlay);const t=document.getElementById("slider-captcha-styles");t&&t.remove(),Object.keys(this).forEach(t=>{"constructor"!==t&&(this[t]=null)})}static create(t){return new PopupSliderCaptcha(t)}static show(t){const e=new PopupSliderCaptcha(t);return e.show(),e}}"undefined"!=typeof module&&module.exports?(module.exports=PopupSliderCaptcha,module.exports.default=PopupSliderCaptcha):"function"==typeof define&&define.amd?define([],()=>PopupSliderCaptcha):"undefined"!=typeof window&&(window.PopupSliderCaptcha=PopupSliderCaptcha,window.SliderCaptcha=PopupSliderCaptcha);class PasswordValidator{constructor(t={}){this.options={publicKeyUrl:t.publicKeyUrl||"/microservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/microservice/strongPassword/checkPassword",timeout:t.timeout||1e4,headers:t.headers||{},...t},this.publicKeyCache=null,this.publicKeyExpiry=null}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET"});if("0"!==t.code)throw Error("获取公钥失败:"+(t.message||"未知错误"));return this.publicKeyCache=t.data,this.publicKeyExpiry=Date.now()+3e5,this.publicKeyCache}catch(t){throw Error("获取公钥失败: "+t.message)}}encryptPassword(t,i){try{if(!e)throw Error("JSEncrypt库未正确加载");const s=new e;s.setPublicKey(i);const a=s.encrypt(t);if(!a)throw Error("密码加密失败");return a}catch(t){throw Error("密码加密失败: "+t.message)}}async validatePassword(t,e,i={}){try{const s=await this.getPublicKey(),a=this.encryptPassword(t,s);return{...await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:e,password:a,timestamp:Date.now(),...i})})}}catch(t){return{success:!1,message:t.msg,code:"VALIDATION_ERROR"}}}async makeRequest(t,e={}){const i=new AbortController,s=setTimeout(()=>i.abort(),this.options.timeout);try{const a=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:i.signal});if(clearTimeout(s),!a.ok)throw Error(`HTTP错误: ${a.status} ${a.statusText}`);return await a.json()}catch(t){if(clearTimeout(s),"AbortError"===t.name)throw Error("请求超时");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}}var a={PopupSliderCaptcha:PopupSliderCaptcha,PasswordValidator:PasswordValidator,createPasswordValidator:i,validatePassword:s};"undefined"!=typeof window&&(window.SliderCaptcha=PopupSliderCaptcha,window.PopupSliderCaptcha=PopupSliderCaptcha,window.PasswordValidator=PasswordValidator,window.createPasswordValidator=i,window.validatePassword=s),t.PasswordValidator=PasswordValidator,t.PopupSliderCaptcha=PopupSliderCaptcha,t.createPasswordValidator=i,t.default=a,t.validatePassword=s,Object.defineProperty(t,"__esModule",{value:!0})});
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptchaSDK={})}(this,function(t){"use strict";function e(t){return new PasswordValidator(t)}async function s(t,e={},s={}){const i=new PasswordValidator(e),a=s.userName||"";return await i.validatePassword(t,a,s)}var i,a,r;class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/api/captcha",verifyUrl:"/api/captcha/verify",throttleDelay:16,clickMaskClose:!1};static CSS_CLASSES={overlay:"slider-captcha-overlay",modal:"slider-captcha-modal",header:"slider-captcha-header",container:"slider-captcha-container",track:"slider-captcha-track",btn:"slider-captcha-btn",hint:"slider-captcha-hint",loading:"slider-captcha-loading",error:"slider-captcha-error"};static getStyles(){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}}"}static CONSTANTS={CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,DEFAULT_TIMEOUT:3e4,ANIMATION_DURATION:300,FLOATING_TIME_DURATION:2500,THROTTLE_DELAY:16};static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",IMAGE_LOAD_ERROR:"IMAGE_LOAD_ERROR",CAPTCHA_DATA_ERROR:"CAPTCHA_DATA_ERROR"};constructor(t={}){this.options={...PopupSliderCaptcha.DEFAULTS,...t,timeout:t.timeout||PopupSliderCaptcha.CONSTANTS.DEFAULT_TIMEOUT,maxRetries:t.maxRetries||PopupSliderCaptcha.CONSTANTS.MAX_RETRY_ATTEMPTS,throttleDelay:t.throttleDelay||PopupSliderCaptcha.CONSTANTS.THROTTLE_DELAY},this.elements={},this.state=this.createInitialState(),this.captchaData=null,this.times=[],this.startTime=null,this.eventListeners=[],this.timers=new Set,this.rafId=null,this.cachedDimensions=null,this.imageCache=new Map,this.abortController=null,this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay);try{this.init()}catch(t){this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR,t.message)}}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=document.createElement("style");t.id="slider-captcha-styles",t.textContent=PopupSliderCaptcha.getStyles(),document.head.appendChild(t)}createElements(){const{elements:t,options:e}=this;[["overlay","div",PopupSliderCaptcha.CSS_CLASSES.overlay],["modal","div",PopupSliderCaptcha.CSS_CLASSES.modal],["header","div",PopupSliderCaptcha.CSS_CLASSES.header],["title","h3","slider-captcha-title","安全验证"],["closeBtn","button","slider-captcha-close"],["refreshBtn","button","slider-captcha-refresh"],["container","div",PopupSliderCaptcha.CSS_CLASSES.container],["backgroundImg","img","slider-captcha-bg"],["sliderImg","img","slider-captcha-piece"],["loadingText","div",PopupSliderCaptcha.CSS_CLASSES.loading,"加载中..."],["floatingTime","div","slider-captcha-floating-time"],["track","div",PopupSliderCaptcha.CSS_CLASSES.track],["fingerAnimation","div","slider-captcha-finger","👉"],["btn","div",PopupSliderCaptcha.CSS_CLASSES.btn],["icon","div","","→"],["hint","div",PopupSliderCaptcha.CSS_CLASSES.hint,"向右滑动完成验证"],["error","div",PopupSliderCaptcha.CSS_CLASSES.error]].forEach(([e,s,i,a])=>{t[e]=this.createElement(s,i,a)}),t.container.style.cssText=`width:${e.width}px;height:${e.height}px`,t.refreshBtn.innerHTML='<svg viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>',this.assembleDOM(),this.setInitialState()}createElement(t,e="",s=""){const i=document.createElement(t);return e&&(i.className=e),s&&(i.textContent=s),i}assembleDOM(){const{elements:t}=this,e=this.createElement("div","slider-captcha-header-buttons");e.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,e),t.container.append(t.backgroundImg,t.sliderImg,t.loadingText,t.floatingTime),t.btn.appendChild(t.icon),t.track.append(t.fingerAnimation,t.btn,t.hint),t.modal.append(t.header,t.container,t.track,t.error),t.overlay.appendChild(t.modal),document.body.appendChild(t.overlay)}setInitialState(){Object.assign(this.elements.container.style,{display:"none"}),Object.assign(this.elements.track.style,{display:"none"})}bindEvents(){const{elements:t}=this;this.addEventListener(t.closeBtn,"click",()=>this.hide()),this.addEventListener(t.refreshBtn,"click",()=>this.refresh()),this.addEventListener(t.overlay,"click",e=>{e.target===t.overlay&&this.options.clickMaskClose&&this.hide()}),this.addEventListener(document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}),this.addEventListener(document,"visibilitychange",()=>this.handleVisibilityChange()),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};this.addEventListener(t.btn,"mousedown",e.start),this.addEventListener(t.btn,"touchstart",e.start),this.addEventListener(t.sliderImg,"mousedown",e.start),this.addEventListener(t.sliderImg,"touchstart",e.start),this.addEventListener(document,"mousemove",e.move,{passive:!1}),this.addEventListener(document,"touchmove",e.move,{passive:!1}),this.addEventListener(document,"mouseup",e.end),this.addEventListener(document,"touchend",e.end)}addEventListener(t,e,s,i={}){t&&"function"==typeof s&&(t.addEventListener(e,s,i),this.eventListeners.push({element:t,event:e,handler:s,options:i}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:e,handler:s,options:i})=>{try{t?.removeEventListener?.(e,s,i)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const t=this.elements.track.offsetWidth,e=this.elements.btn.offsetWidth;this.cachedDimensions={trackWidth:t,btnWidth:e,maxX:t-e}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),e=this.state.currentX/t;return Math.round(e*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),this.state.isDragging=!0,this.state.startX=this.getClientX(t)-this.state.currentX,this.startTime=this.startTime||Date.now(),this.times=[{time:Date.now(),position:this.getPosition()}],this.setTransition(!1),this.updateUIState("dragging"),this.cachedDimensions=null)}handleMove(t){if(!this.state.isDragging)return;t.preventDefault();const e=this.getClientX(t)-this.state.startX,{maxX:s}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(e,s)),this.times.push({time:Date.now(),position:this.getPosition()}),this.rafId&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(()=>this.updateSliderPosition())}handleEnd(){this.state.isDragging&&(this.times.push({time:Date.now(),position:this.getPosition()}),this.state.isDragging=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.verify())}handleVisibilityChange(){const t=document.hidden?"paused":"running";this.elements.fingerAnimation&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX(t){return t.type.includes("touch")?t.touches[0].clientX:t.clientX}setTransition(t){const e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{this.elements.btn.style.transition=e,this.elements.sliderImg.style.transition=e})}updateUIState(t){const{elements:e}=this,s={dragging:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none"},success:()=>{Object.assign(e.btn.style,{background:"var(--sc-success)"}),Object.assign(e.icon.style,{innerHTML:"✓",color:"white"}),e.icon.innerHTML="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{innerHTML:"✗",color:"white"}),e.icon.innerHTML="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.innerHTML="→",e.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none",Object.assign(e.track.style,{pointerEvents:"none",opacity:"0.6"})}};s[t]&&requestAnimationFrame(()=>{s[t](),"loading"!==t&&Object.assign(e.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,e){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:e,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:e,state:s}=this,{maxX:i}=this.getDimensions(),a=s.currentX/i*(e.width-e.sliderSize),r=s.currentX/i;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${s.currentX}px)`,t.sliderImg.style.transform=`translateX(${a}px)`,t.fingerAnimation.style.opacity=.8>r?"0.6":"0"})}show(){this.state.isVisible=!0,this.elements.overlay.style.display="flex",requestAnimationFrame(()=>{this.elements.overlay.classList.add("show"),this.elements.modal.classList.add("show")}),this.loadCaptcha()}hide(){this.state.isVisible=!1,this.elements.overlay.classList.remove("show"),this.elements.modal.classList.remove("show"),this.safeSetTimeout(()=>{this.elements.overlay.style.display="none",document.body.removeChild(this.elements.overlay),this.reset(),this.options.onClose?.(),this.elements.overlay=null},300)}handleError(t,e,s=null){const i={[PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]:"验证失败,请重试",[PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]:"图片加载失败,请刷新重试",[PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]:"验证码数据错误,请刷新重试"}[t]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:i,originalError:s}),this.showError(i)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now(),this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await fetch(this.options.apiUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({place:2,timestamp:Date.now(),...this.options.requestData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const e=await t.json();if(!this.validateCaptchaData(e))throw Error("验证码数据格式错误");this.captchaData=e.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"请求被取消"):t.message.includes("Failed to fetch")||t.message.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败"):this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}}validateCaptchaData(t){if(!t||"object"!=typeof t)return!1;const e=t.data||t;return["canvasSrc","blockSrc","canvasWidth","canvasHeight","blockWidth","blockHeight","blockY","nonceStr"].every(t=>{const s=e[t];return null!=s&&""!==s})}async renderCaptcha(){return new Promise((t,e)=>{let s=!1;const i=[this.loadImageAsync(this.elements.backgroundImg,this.captchaData.canvasSrc,{width:this.captchaData.canvasWidth,height:this.captchaData.canvasHeight}),this.loadImageAsync(this.elements.sliderImg,this.captchaData.blockSrc,{width:this.captchaData.blockWidth,height:this.captchaData.blockHeight,top:this.captchaData.blockY})];Promise.all(i).then(()=>{s||(this.hideLoading(),t())}).catch(t=>{s||(s=!0,this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR,"图片加载失败",t),e(t))})})}async loadImageAsync(t,e,s){return new Promise((i,a)=>{if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,s),void i()}const r=this.safeSetTimeout(()=>{a(Error("图片加载超时"))},1e4);t.onload=()=>{this.safeClearTimeout(r),this.imageCache.set(e,t.cloneNode()),i()},t.onerror=t=>{this.safeClearTimeout(r),a(t)},t.src=e,this.applyStyles(t,s)})}loadImage(t,e,s,i,a){if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,s),void i()}t.onload=()=>{this.imageCache.set(e,t.cloneNode()),i()},t.onerror=a,t.src=e,this.applyStyles(t,s)}applyStyles(t,e){Object.entries(e).forEach(([e,s])=>{t.style[e]="number"==typeof s?s+"px":s})}showLoading(){this.state.isLoading=!0,this.batchUpdateStyles({container:{display:"block"},loadingText:{display:"flex"},error:{display:"none"}}),this.updateUIState("loading")}hideLoading(){this.state.isLoading=!1,this.batchUpdateStyles({loadingText:{display:"none"}}),this.updateUIState("reset")}showCaptcha(){this.batchUpdateStyles({container:{display:"block"},track:{display:"block"},error:{display:"none"}})}showError(t){this.hideLoading(),this.batchUpdateStyles({error:{display:"block",textContent:t}})}batchUpdateStyles(t){requestAnimationFrame(()=>{Object.entries(t).forEach(([t,e])=>{const s=this.elements[t];s&&Object.entries(e).forEach(([t,e])=>{"textContent"===t?s.textContent=e:s.style[t]=e})})})}async verify(){if(this.captchaData)try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await fetch(this.options.verifyUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times],...this.options.verifyData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const e=await t.json();this.isVerifySuccess(e)?this.onVerifySuccess(e.data||e.result):this.onVerifyFail(e.message||e.msg||"验证失败,请重试!")}catch(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"验证请求被取消"):t.message.includes("Failed to fetch")||t.message.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败"):this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR,t.message,t)}else this.onVerifyFail("验证码数据丢失")}isVerifySuccess(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}onVerifySuccess(t){const e=Date.now()-this.startTime,s=`验证成功!耗时:${(e/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(s,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:e}),this.hide()},2e3)}showFloatingTime(t,e="success"){const{elements:s}=this;s.floatingTime.textContent=t,s.floatingTime.className="slider-captcha-floating-time "+e,this.safeSetTimeout(()=>s.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{s.floatingTime.className="slider-captcha-floating-time"},2500)}onVerifyFail(t){this.state.retryCount++,this.updateUIState("fail"),this.showFloatingTime(t,"fail"),this.safeSetTimeout(()=>{this.state.retryCount<this.options.maxRetries?this.reset():this.refresh()},2500)}reset(){this.clearAllTimers(),Object.assign(this.state,{isDragging:!1,currentX:0,startX:0,isLoading:!1}),this.times=[],this.startTime=null,this.cachedDimensions=null,requestAnimationFrame(()=>{this.setTransition(!0),this.elements.btn.style.transform="translateX(0px)",this.elements.sliderImg.style.transform="translateX(0px)",this.updateUIState("reset"),this.elements.error.style.display="none"})}refresh(){this.reset(),this.state.retryCount=0,this.loadCaptcha()}safeSetTimeout(t,e){const s=setTimeout(()=>{this.timers.delete(s),t()},e);return this.timers.add(s),s}safeClearTimeout(t){t&&(clearTimeout(t),this.timers.delete(t))}clearAllTimers(){this.timers.forEach(t=>{clearTimeout(t),clearInterval(t)}),this.timers.clear(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null)}cleanupImages(){this.elements.backgroundImg&&(this.elements.backgroundImg.src="",this.elements.backgroundImg.onload=null,this.elements.backgroundImg.onerror=null),this.elements.sliderImg&&(this.elements.sliderImg.src="",this.elements.sliderImg.onload=null,this.elements.sliderImg.onerror=null),this.imageCache.clear()}throttle(t,e){let s=0;return function(...i){const a=Date.now();if(a-s>=e)return s=a,t.apply(this,i)}}debounce(t,e){let s;return function(...i){clearTimeout(s),s=setTimeout(()=>t.apply(this,i),e)}}destroy(){try{this.abortController&&(this.abortController.abort(),this.abortController=null),document.body&&(document.body.style.userSelect="",document.body.style.cursor=""),this.clearAllTimers(),this.removeAllEventListeners(),this.cleanupImages(),this.elements?.overlay?.parentNode&&this.elements.overlay.parentNode.removeChild(this.elements.overlay);const t=document.getElementById("slider-captcha-styles");t&&!this.hasOtherInstances()&&t.remove(),this.imageCache.clear(),this.cachedDimensions=null,Object.keys(this).forEach(t=>{"constructor"!==t&&(this[t]=null)}),this.options.onDestroy&&this.options.onDestroy()}catch(t){}}hasOtherInstances(){return"undefined"!=typeof window&&window.sliderCaptchaInstanceCount>1}static create(t){return new PopupSliderCaptcha(t)}static show(t){const e=new PopupSliderCaptcha(t);return e.show(),e}}"undefined"!=typeof module&&module.exports?(module.exports=PopupSliderCaptcha,module.exports.default=PopupSliderCaptcha):"function"==typeof define&&define.amd?define([],()=>PopupSliderCaptcha):"undefined"!=typeof window&&(window.PopupSliderCaptcha=PopupSliderCaptcha,window.SliderCaptcha=PopupSliderCaptcha),i=PopupSliderCaptcha;class PasswordValidator{static CONSTANTS={DEFAULT_TIMEOUT:1e4,CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,MIN_PASSWORD_LENGTH:1};static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",ENCRYPTION_ERROR:"ENCRYPTION_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",PUBLIC_KEY_ERROR:"PUBLIC_KEY_ERROR"};constructor(t={}){this.options={publicKeyUrl:t.publicKeyUrl||"/microservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/microservice/strongPassword/checkPassword",timeout:t.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:t.headers||{},retryAttempts:t.retryAttempts||PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,cacheDuration:t.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...t},this.publicKeyCache=null,this.publicKeyExpiry=null,this.retryCount=0,this.abortController=null}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET",signal:this.abortController.signal});if(!this.isSuccessResponse(t))throw Error("获取公钥失败:"+(t.message||t.msg||"未知错误"));const e=t.data||t.result;if(!this.validatePublicKey(e))throw Error("公钥格式无效");return this.publicKeyCache=e,this.publicKeyExpiry=Date.now()+this.options.cacheDuration,this.publicKeyCache}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR,t.message,t),Error("获取公钥失败: "+t.message)}}isSuccessResponse(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}validatePublicKey(t){return!(!t||"string"!=typeof t)&&/^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/.test(t.trim())}handleError(t,e,s=null){const i={[PasswordValidator.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]:"密码加密失败",[PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]:"密码验证失败",[PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]:"获取公钥失败"}[t]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:i,originalError:s})}encryptPassword(t,e){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!e||"string"!=typeof e)throw Error("公钥不能为空");if("undefined"==typeof JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");const s=new JSEncrypt;s.setPublicKey(e);const i=s.encrypt(t);if(!i)throw Error("密码加密失败,可能是公钥格式不正确");return i}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,t.message,t),Error("密码加密失败: "+t.message)}}async validatePassword(t,e,s={}){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(!e||"string"!=typeof e)throw Error("用户名不能为空");this.retryCount=0;const i=await this.getPublicKey(),a=this.encryptPassword(t,i),r=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:e,password:a,timestamp:Date.now(),...s})});return{success:this.isSuccessResponse(r),data:r.data||r.result,message:r.message||r.msg||"验证完成",code:r.code||r.status,originalResponse:r}}catch(t){let e=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?e=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?e=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(e=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(e,t.message,t),{success:!1,message:t.message||"密码校验失败",code:e,error:t}}}async makeRequest(t,e={}){this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const s=setTimeout(()=>{this.abortController.abort()},this.options.timeout);try{const i=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:this.abortController.signal});if(clearTimeout(s),!i.ok)throw Error(`HTTP错误: ${i.status} ${i.statusText}`);const a=await i.json();return this.options.debug,a}catch(t){if(clearTimeout(s),"AbortError"===t.name)throw this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR,"请求超时",t),Error("请求超时");if(t.message.includes("Failed to fetch")||t.message.includes("NetworkError"))throw this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t),Error("网络连接失败");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}cancelRequest(){this.abortController&&(this.abortController.abort(),this.abortController=null)}isRequesting(){return this.abortController&&!this.abortController.signal.aborted}getCacheStatus(){return{hasCache:!!this.publicKeyCache,isExpired:!this.publicKeyExpiry||Date.now()>this.publicKeyExpiry,expiryTime:this.publicKeyExpiry,remainingTime:this.publicKeyExpiry?Math.max(0,this.publicKeyExpiry-Date.now()):0}}destroy(){try{this.cancelRequest(),this.clearCache(),this.retryCount=0,this.options.onDestroy&&this.options.onDestroy()}catch(t){}}}r={PopupSliderCaptcha:i,PasswordValidator:a=PasswordValidator,createPasswordValidator:e,validatePassword:s},"undefined"!=typeof window&&(window.SliderCaptcha=i,window.PopupSliderCaptcha=i,window.PasswordValidator=a,window.createPasswordValidator=e,window.validatePassword=s),t.PasswordValidator=a,t.PopupSliderCaptcha=i,t.createPasswordValidator=e,t.default=r,t.validatePassword=s,Object.defineProperty(t,"__esModule",{value:!0})});
|
|
@@ -1,27 +1,106 @@
|
|
|
1
1
|
declare module 'slider-captcha-sdk/password-validator' {
|
|
2
|
+
// 优化:添加更详细的类型定义
|
|
2
3
|
export interface PasswordValidatorOptions {
|
|
4
|
+
// API配置
|
|
3
5
|
publicKeyUrl?: string
|
|
4
6
|
validateUrl?: string
|
|
7
|
+
|
|
8
|
+
// 请求配置
|
|
5
9
|
timeout?: number
|
|
6
10
|
headers?: Record<string, string>
|
|
11
|
+
retryAttempts?: number
|
|
12
|
+
cacheDuration?: number
|
|
13
|
+
|
|
14
|
+
// 调试配置
|
|
15
|
+
debug?: boolean
|
|
16
|
+
|
|
17
|
+
// 回调函数
|
|
18
|
+
onError?: (error: ErrorInfo) => void
|
|
19
|
+
onDestroy?: () => void
|
|
7
20
|
}
|
|
8
21
|
|
|
9
22
|
export interface ValidationResult {
|
|
10
23
|
success: boolean
|
|
11
24
|
message: string
|
|
12
25
|
data?: any
|
|
13
|
-
code: string
|
|
26
|
+
code: string | number
|
|
27
|
+
originalResponse?: any
|
|
28
|
+
error?: Error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ErrorInfo {
|
|
32
|
+
type: string
|
|
33
|
+
message: string
|
|
34
|
+
originalError?: Error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CacheStatus {
|
|
38
|
+
hasCache: boolean
|
|
39
|
+
isExpired: boolean
|
|
40
|
+
expiryTime?: number
|
|
41
|
+
remainingTime: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PublicKeyResponse {
|
|
45
|
+
code: string | number
|
|
46
|
+
data?: string
|
|
47
|
+
result?: string
|
|
48
|
+
message?: string
|
|
49
|
+
msg?: string
|
|
50
|
+
success?: boolean
|
|
51
|
+
status?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ValidateResponse {
|
|
55
|
+
code: string | number
|
|
56
|
+
data?: any
|
|
57
|
+
result?: any
|
|
58
|
+
message?: string
|
|
59
|
+
msg?: string
|
|
60
|
+
success?: boolean
|
|
61
|
+
status?: string
|
|
14
62
|
}
|
|
15
63
|
|
|
16
64
|
export default class PasswordValidator {
|
|
17
65
|
constructor(options?: PasswordValidatorOptions)
|
|
66
|
+
|
|
67
|
+
// 核心方法
|
|
18
68
|
getPublicKey(): Promise<string>
|
|
19
69
|
encryptPassword(password: string, publicKey: string): string
|
|
20
|
-
validatePassword(password: string, userName: string, additionalData?: any): Promise<ValidationResult>
|
|
70
|
+
validatePassword(password: string, userName: string, additionalData?: Record<string, any>): Promise<ValidationResult>
|
|
71
|
+
|
|
72
|
+
// 缓存管理
|
|
21
73
|
clearCache(): void
|
|
22
|
-
|
|
74
|
+
getCacheStatus(): CacheStatus
|
|
75
|
+
|
|
76
|
+
// 配置管理
|
|
77
|
+
updateOptions(newOptions: Partial<PasswordValidatorOptions>): void
|
|
78
|
+
|
|
79
|
+
// 请求控制
|
|
80
|
+
cancelRequest(): void
|
|
81
|
+
isRequesting(): boolean
|
|
82
|
+
|
|
83
|
+
// 生命周期
|
|
84
|
+
destroy(): void
|
|
85
|
+
|
|
86
|
+
// 工具方法
|
|
87
|
+
isSuccessResponse(response: any): boolean
|
|
88
|
+
validatePublicKey(publicKey: string): boolean
|
|
89
|
+
handleError(errorType: string, message: string, originalError?: Error): void
|
|
90
|
+
|
|
91
|
+
// 常量
|
|
92
|
+
static readonly CONSTANTS: Record<string, number>
|
|
93
|
+
static readonly ERROR_TYPES: Record<string, string>
|
|
23
94
|
}
|
|
24
95
|
|
|
96
|
+
// 工厂函数
|
|
25
97
|
export function createPasswordValidator(options?: PasswordValidatorOptions): PasswordValidator
|
|
26
|
-
|
|
98
|
+
|
|
99
|
+
// 便捷函数
|
|
100
|
+
export function validatePassword(
|
|
101
|
+
password: string,
|
|
102
|
+
userName: string,
|
|
103
|
+
options?: PasswordValidatorOptions,
|
|
104
|
+
additionalData?: Record<string, any>
|
|
105
|
+
): Promise<ValidationResult>
|
|
27
106
|
}
|
|
@@ -1,26 +1,44 @@
|
|
|
1
|
-
import JSEncrypt from 'jsencrypt'
|
|
1
|
+
// import JSEncrypt from 'jsencrypt'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 密码校验工具类
|
|
5
5
|
* 提供密码加密和校验功能
|
|
6
6
|
*/
|
|
7
7
|
class PasswordValidator {
|
|
8
|
+
// 优化:添加常量定义
|
|
9
|
+
static CONSTANTS = {
|
|
10
|
+
DEFAULT_TIMEOUT: 10000,
|
|
11
|
+
CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
|
|
12
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
13
|
+
MIN_PASSWORD_LENGTH: 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 优化:添加错误类型枚举
|
|
17
|
+
static ERROR_TYPES = {
|
|
18
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
19
|
+
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
20
|
+
ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
|
|
21
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
22
|
+
PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR'
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
constructor(options = {}) {
|
|
26
|
+
// 优化:合并配置,使用常量
|
|
9
27
|
this.options = {
|
|
10
|
-
// 获取公钥的接口地址
|
|
11
28
|
publicKeyUrl: options.publicKeyUrl || '/microservice/strongPassword/getPublicKey',
|
|
12
|
-
// 密码校验接口地址
|
|
13
29
|
validateUrl: options.validateUrl || '/microservice/strongPassword/checkPassword',
|
|
14
|
-
|
|
15
|
-
timeout: options.timeout || 10000,
|
|
16
|
-
// 自定义请求头
|
|
30
|
+
timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
|
|
17
31
|
headers: options.headers || {},
|
|
32
|
+
retryAttempts: options.retryAttempts || PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,
|
|
33
|
+
cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
|
|
18
34
|
...options
|
|
19
35
|
};
|
|
20
36
|
|
|
21
37
|
// 缓存公钥,避免重复请求
|
|
22
38
|
this.publicKeyCache = null;
|
|
23
39
|
this.publicKeyExpiry = null;
|
|
40
|
+
this.retryCount = 0;
|
|
41
|
+
this.abortController = null;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
/**
|
|
@@ -28,31 +46,94 @@ class PasswordValidator {
|
|
|
28
46
|
* @returns {Promise<string>} 公钥字符串
|
|
29
47
|
*/
|
|
30
48
|
async getPublicKey() {
|
|
31
|
-
//
|
|
49
|
+
// 检查缓存是否有效
|
|
32
50
|
if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
|
|
33
51
|
return this.publicKeyCache
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
try {
|
|
55
|
+
// 取消之前的请求
|
|
56
|
+
if (this.abortController) {
|
|
57
|
+
this.abortController.abort();
|
|
58
|
+
}
|
|
59
|
+
this.abortController = new AbortController();
|
|
60
|
+
|
|
37
61
|
const response = await this.makeRequest(this.options.publicKeyUrl, {
|
|
38
|
-
method: 'GET'
|
|
62
|
+
method: 'GET',
|
|
63
|
+
signal: this.abortController.signal
|
|
39
64
|
});
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
// 优化:更灵活的成功判断
|
|
67
|
+
if (!this.isSuccessResponse(response)) {
|
|
68
|
+
throw new Error('获取公钥失败:' + (response.message || response.msg || '未知错误'))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 验证公钥格式
|
|
72
|
+
const publicKey = response.data || response.result;
|
|
73
|
+
if (!this.validatePublicKey(publicKey)) {
|
|
74
|
+
throw new Error('公钥格式无效')
|
|
43
75
|
}
|
|
44
76
|
|
|
45
|
-
//
|
|
46
|
-
this.publicKeyCache =
|
|
47
|
-
this.publicKeyExpiry = Date.now() +
|
|
77
|
+
// 缓存公钥
|
|
78
|
+
this.publicKeyCache = publicKey;
|
|
79
|
+
this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
|
|
48
80
|
|
|
49
81
|
return this.publicKeyCache
|
|
50
82
|
} catch (error) {
|
|
51
83
|
console.error('获取公钥失败:', error);
|
|
84
|
+
this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
|
|
52
85
|
throw new Error('获取公钥失败: ' + error.message)
|
|
53
86
|
}
|
|
54
87
|
}
|
|
55
88
|
|
|
89
|
+
// 优化:添加响应成功判断方法
|
|
90
|
+
isSuccessResponse(response) {
|
|
91
|
+
if (!response || typeof response !== 'object') return false
|
|
92
|
+
|
|
93
|
+
const successIndicators = [
|
|
94
|
+
response.code === '0',
|
|
95
|
+
response.code === 0,
|
|
96
|
+
response.success === true,
|
|
97
|
+
response.status === 'success',
|
|
98
|
+
response.result === true
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
return successIndicators.some(indicator => indicator === true)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 优化:添加公钥验证方法
|
|
105
|
+
validatePublicKey(publicKey) {
|
|
106
|
+
if (!publicKey || typeof publicKey !== 'string') return false
|
|
107
|
+
|
|
108
|
+
// 基本的RSA公钥格式检查
|
|
109
|
+
const rsaPattern = /^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/;
|
|
110
|
+
return rsaPattern.test(publicKey.trim())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 优化:添加统一错误处理方法
|
|
114
|
+
handleError(errorType, message, originalError = null) {
|
|
115
|
+
const errorMessages = {
|
|
116
|
+
[PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
|
|
117
|
+
[PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
|
|
118
|
+
[PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
|
|
119
|
+
[PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
|
|
120
|
+
[PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const errorMessage = errorMessages[errorType] || message || '未知错误';
|
|
124
|
+
|
|
125
|
+
// 调用用户自定义错误处理
|
|
126
|
+
if (this.options.onError) {
|
|
127
|
+
this.options.onError({
|
|
128
|
+
type: errorType,
|
|
129
|
+
message: errorMessage,
|
|
130
|
+
originalError
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
|
|
135
|
+
}
|
|
136
|
+
|
|
56
137
|
/**
|
|
57
138
|
* 使用RSA公钥加密密码
|
|
58
139
|
* @param {string} password 原始密码
|
|
@@ -61,9 +142,22 @@ class PasswordValidator {
|
|
|
61
142
|
*/
|
|
62
143
|
encryptPassword(password, publicKey) {
|
|
63
144
|
try {
|
|
145
|
+
// 优化:输入验证
|
|
146
|
+
if (!password || typeof password !== 'string') {
|
|
147
|
+
throw new Error('密码不能为空')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
|
|
151
|
+
throw new Error('密码长度不足')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!publicKey || typeof publicKey !== 'string') {
|
|
155
|
+
throw new Error('公钥不能为空')
|
|
156
|
+
}
|
|
157
|
+
|
|
64
158
|
// 使用导入的JSEncrypt进行RSA加密
|
|
65
|
-
if (
|
|
66
|
-
throw new Error('JSEncrypt
|
|
159
|
+
if (typeof JSEncrypt === 'undefined') {
|
|
160
|
+
throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
|
|
67
161
|
}
|
|
68
162
|
|
|
69
163
|
const encrypt = new JSEncrypt();
|
|
@@ -71,12 +165,13 @@ class PasswordValidator {
|
|
|
71
165
|
const encrypted = encrypt.encrypt(password);
|
|
72
166
|
|
|
73
167
|
if (!encrypted) {
|
|
74
|
-
throw new Error('
|
|
168
|
+
throw new Error('密码加密失败,可能是公钥格式不正确')
|
|
75
169
|
}
|
|
76
170
|
|
|
77
171
|
return encrypted
|
|
78
172
|
} catch (error) {
|
|
79
173
|
console.error('密码加密失败:', error);
|
|
174
|
+
this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
|
|
80
175
|
throw new Error('密码加密失败: ' + error.message)
|
|
81
176
|
}
|
|
82
177
|
}
|
|
@@ -84,12 +179,24 @@ class PasswordValidator {
|
|
|
84
179
|
/**
|
|
85
180
|
* 校验密码
|
|
86
181
|
* @param {string} password 原始密码
|
|
87
|
-
* @param {string} userName
|
|
182
|
+
* @param {string} userName 用户名
|
|
88
183
|
* @param {Object} additionalData 额外的校验数据
|
|
89
184
|
* @returns {Promise<Object>} 校验结果
|
|
90
185
|
*/
|
|
91
186
|
async validatePassword(password, userName, additionalData = {}) {
|
|
92
187
|
try {
|
|
188
|
+
// 优化:输入验证
|
|
189
|
+
if (!password || typeof password !== 'string') {
|
|
190
|
+
throw new Error('密码不能为空')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!userName || typeof userName !== 'string') {
|
|
194
|
+
throw new Error('用户名不能为空')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 重置重试计数
|
|
198
|
+
this.retryCount = 0;
|
|
199
|
+
|
|
93
200
|
// 1. 获取公钥
|
|
94
201
|
const publicKey = await this.getPublicKey();
|
|
95
202
|
|
|
@@ -107,15 +214,34 @@ class PasswordValidator {
|
|
|
107
214
|
})
|
|
108
215
|
});
|
|
109
216
|
|
|
217
|
+
// 优化:统一返回格式
|
|
110
218
|
return {
|
|
111
|
-
|
|
219
|
+
success: this.isSuccessResponse(response),
|
|
220
|
+
data: response.data || response.result,
|
|
221
|
+
message: response.message || response.msg || '验证完成',
|
|
222
|
+
code: response.code || response.status,
|
|
223
|
+
originalResponse: response
|
|
112
224
|
}
|
|
113
225
|
} catch (error) {
|
|
114
226
|
console.error('密码校验失败:', error);
|
|
227
|
+
|
|
228
|
+
// 优化:根据错误类型返回不同的错误信息
|
|
229
|
+
let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
|
|
230
|
+
if (error.message.includes('网络') || error.message.includes('fetch')) {
|
|
231
|
+
errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
|
|
232
|
+
} else if (error.message.includes('超时') || error.message.includes('timeout')) {
|
|
233
|
+
errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
|
|
234
|
+
} else if (error.message.includes('加密')) {
|
|
235
|
+
errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.handleError(errorType, error.message, error);
|
|
239
|
+
|
|
115
240
|
return {
|
|
116
241
|
success: false,
|
|
117
|
-
message: error.
|
|
118
|
-
code:
|
|
242
|
+
message: error.message || '密码校验失败',
|
|
243
|
+
code: errorType,
|
|
244
|
+
error: error
|
|
119
245
|
}
|
|
120
246
|
}
|
|
121
247
|
}
|
|
@@ -127,8 +253,15 @@ class PasswordValidator {
|
|
|
127
253
|
* @returns {Promise<Object>} 响应结果
|
|
128
254
|
*/
|
|
129
255
|
async makeRequest(url, options = {}) {
|
|
130
|
-
|
|
131
|
-
|
|
256
|
+
// 优化:使用实例的abortController
|
|
257
|
+
if (this.abortController) {
|
|
258
|
+
this.abortController.abort();
|
|
259
|
+
}
|
|
260
|
+
this.abortController = new AbortController();
|
|
261
|
+
|
|
262
|
+
const timeoutId = setTimeout(() => {
|
|
263
|
+
this.abortController.abort();
|
|
264
|
+
}, this.options.timeout);
|
|
132
265
|
|
|
133
266
|
try {
|
|
134
267
|
const response = await fetch(url, {
|
|
@@ -138,7 +271,7 @@ class PasswordValidator {
|
|
|
138
271
|
...this.options.headers,
|
|
139
272
|
...options.headers
|
|
140
273
|
},
|
|
141
|
-
signal:
|
|
274
|
+
signal: this.abortController.signal
|
|
142
275
|
});
|
|
143
276
|
|
|
144
277
|
clearTimeout(timeoutId);
|
|
@@ -147,14 +280,27 @@ class PasswordValidator {
|
|
|
147
280
|
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
|
|
148
281
|
}
|
|
149
282
|
|
|
150
|
-
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
|
|
285
|
+
// 优化:记录请求日志(开发环境)
|
|
286
|
+
if (this.options.debug) {
|
|
287
|
+
console.log('密码校验器请求:', { url, options, response: data });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return data
|
|
151
291
|
} catch (error) {
|
|
152
292
|
clearTimeout(timeoutId);
|
|
153
293
|
|
|
154
294
|
if (error.name === 'AbortError') {
|
|
295
|
+
this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
|
|
155
296
|
throw new Error('请求超时')
|
|
156
297
|
}
|
|
157
298
|
|
|
299
|
+
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
|
|
300
|
+
this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
|
|
301
|
+
throw new Error('网络连接失败')
|
|
302
|
+
}
|
|
303
|
+
|
|
158
304
|
throw error
|
|
159
305
|
}
|
|
160
306
|
}
|
|
@@ -176,6 +322,60 @@ class PasswordValidator {
|
|
|
176
322
|
// 清除缓存,使用新配置重新获取
|
|
177
323
|
this.clearCache();
|
|
178
324
|
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 取消当前请求
|
|
328
|
+
*/
|
|
329
|
+
cancelRequest() {
|
|
330
|
+
if (this.abortController) {
|
|
331
|
+
this.abortController.abort();
|
|
332
|
+
this.abortController = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 检查是否正在请求中
|
|
338
|
+
* @returns {boolean} 是否正在请求
|
|
339
|
+
*/
|
|
340
|
+
isRequesting() {
|
|
341
|
+
return this.abortController && !this.abortController.signal.aborted
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 获取缓存状态
|
|
346
|
+
* @returns {Object} 缓存状态信息
|
|
347
|
+
*/
|
|
348
|
+
getCacheStatus() {
|
|
349
|
+
return {
|
|
350
|
+
hasCache: !!this.publicKeyCache,
|
|
351
|
+
isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
|
|
352
|
+
expiryTime: this.publicKeyExpiry,
|
|
353
|
+
remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 销毁实例,清理资源
|
|
359
|
+
*/
|
|
360
|
+
destroy() {
|
|
361
|
+
try {
|
|
362
|
+
// 取消所有进行中的请求
|
|
363
|
+
this.cancelRequest();
|
|
364
|
+
|
|
365
|
+
// 清除缓存
|
|
366
|
+
this.clearCache();
|
|
367
|
+
|
|
368
|
+
// 重置状态
|
|
369
|
+
this.retryCount = 0;
|
|
370
|
+
|
|
371
|
+
// 调用销毁回调
|
|
372
|
+
if (this.options.onDestroy) {
|
|
373
|
+
this.options.onDestroy();
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error('销毁密码校验器时出错:', error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
179
379
|
}
|
|
180
380
|
|
|
181
381
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).PasswordValidator={})}(this,function(t){"use strict";class PasswordValidator{static CONSTANTS={DEFAULT_TIMEOUT:1e4,CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,MIN_PASSWORD_LENGTH:1};static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",ENCRYPTION_ERROR:"ENCRYPTION_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",PUBLIC_KEY_ERROR:"PUBLIC_KEY_ERROR"};constructor(t={}){this.options={publicKeyUrl:t.publicKeyUrl||"/microservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/microservice/strongPassword/checkPassword",timeout:t.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:t.headers||{},retryAttempts:t.retryAttempts||PasswordValidator.CONSTANTS.MAX_RETRY_ATTEMPTS,cacheDuration:t.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...t},this.publicKeyCache=null,this.publicKeyExpiry=null,this.retryCount=0,this.abortController=null}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET",signal:this.abortController.signal});if(!this.isSuccessResponse(t))throw Error("获取公钥失败:"+(t.message||t.msg||"未知错误"));const e=t.data||t.result;if(!this.validatePublicKey(e))throw Error("公钥格式无效");return this.publicKeyCache=e,this.publicKeyExpiry=Date.now()+this.options.cacheDuration,this.publicKeyCache}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR,t.message,t),Error("获取公钥失败: "+t.message)}}isSuccessResponse(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}validatePublicKey(t){return!(!t||"string"!=typeof t)&&/^-----BEGIN PUBLIC KEY-----[\s\S]*-----END PUBLIC KEY-----$/.test(t.trim())}handleError(t,e,r=null){const s={[PasswordValidator.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]:"密码加密失败",[PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]:"密码验证失败",[PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]:"获取公钥失败"}[t]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:s,originalError:r})}encryptPassword(t,e){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!e||"string"!=typeof e)throw Error("公钥不能为空");if("undefined"==typeof JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");const r=new JSEncrypt;r.setPublicKey(e);const s=r.encrypt(t);if(!s)throw Error("密码加密失败,可能是公钥格式不正确");return s}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,t.message,t),Error("密码加密失败: "+t.message)}}async validatePassword(t,e,r={}){try{if(!t||"string"!=typeof t)throw Error("密码不能为空");if(!e||"string"!=typeof e)throw Error("用户名不能为空");this.retryCount=0;const s=await this.getPublicKey(),o=this.encryptPassword(t,s),i=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:e,password:o,timestamp:Date.now(),...r})});return{success:this.isSuccessResponse(i),data:i.data||i.result,message:i.message||i.msg||"验证完成",code:i.code||i.status,originalResponse:i}}catch(t){let e=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?e=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?e=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(e=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(e,t.message,t),{success:!1,message:t.message||"密码校验失败",code:e,error:t}}}async makeRequest(t,e={}){this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const r=setTimeout(()=>{this.abortController.abort()},this.options.timeout);try{const s=await fetch(t,{...e,headers:{"Content-Type":"application/json",...this.options.headers,...e.headers},signal:this.abortController.signal});if(clearTimeout(r),!s.ok)throw Error(`HTTP错误: ${s.status} ${s.statusText}`);const o=await s.json();return this.options.debug,o}catch(t){if(clearTimeout(r),"AbortError"===t.name)throw this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR,"请求超时",t),Error("请求超时");if(t.message.includes("Failed to fetch")||t.message.includes("NetworkError"))throw this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t),Error("网络连接失败");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}cancelRequest(){this.abortController&&(this.abortController.abort(),this.abortController=null)}isRequesting(){return this.abortController&&!this.abortController.signal.aborted}getCacheStatus(){return{hasCache:!!this.publicKeyCache,isExpired:!this.publicKeyExpiry||Date.now()>this.publicKeyExpiry,expiryTime:this.publicKeyExpiry,remainingTime:this.publicKeyExpiry?Math.max(0,this.publicKeyExpiry-Date.now()):0}}destroy(){try{this.cancelRequest(),this.clearCache(),this.retryCount=0,this.options.onDestroy&&this.options.onDestroy()}catch(t){}}}t.createPasswordValidator=function(t){return new PasswordValidator(t)},t.default=PasswordValidator,t.validatePassword=async function(t,e={},r={}){const s=new PasswordValidator(e),o=r.userName||"";return await s.validatePassword(t,o,r)},Object.defineProperty(t,"__esModule",{value:!0})});
|