完整代码:GraphicCaptchaDemo
在应用登录、注册或敏感操作中,图形验证码是防止机器人恶意刷接口的常用手段。本文将完整实现一个图形验证码组件,支持随机字母+数字、可变的干扰线、随机噪点以及字符倾斜与偏移,并提供外部刷新和验证回调。
一、效果预览
组件效果如下图所示:
- 背景浅灰色,4位随机字符(排除易混淆字符
0/O/I/l/1)。 - 6~10条彩色二次贝塞尔曲线作为干扰线。
- 60~120个彩色小噪点。
- 每个字符随机偏移位置、随机旋转角度,且使用中等亮度字体保证人眼可读。
- 点击验证码图片即可刷新,也可通过按钮调用控制器刷新。
二、核心技术点
- 真随机数生成:使用
cryptoFramework生成硬件真随机数,避免Math.random()的可预测性。 - Canvas绘图:利用
CanvasRenderingContext2D绘制背景、曲线、点阵和文字。 - 字符集处理:剔除易混淆字符(0、O、I、l、1),最终转为小写用于比较。
- 外部控制器:通过自定义控制器
ImageCodeController对外暴露refresh方法,实现组件外刷新。 - 响应式状态:通过
@State管理画布尺寸,通过onCodeChange回调向上传递验证码字符串。
三、代码实现
1. 控制器文件:ImageCodeController.ets
exportclassImageCodeController{refresh:()=>void=()=>{};}2. 组件文件:ImageCode.ets
背景先绘制之后的噪点 线条 文字 绘制顺序决定了识别难度。如果文字最后绘制会特别清晰、如果文字先绘制由噪点和线条覆盖则增加识别难度。
import{cryptoFramework}from'@kit.CryptoArchitectureKit';import{ImageCodeController}from'../controller/ImageCodeController';@Componentexportstruct ImageCode{// Canvas 画布上下文privatesettings:RenderingContextSettings=newRenderingContextSettings(true);privatectx:CanvasRenderingContext2D=newCanvasRenderingContext2D(this.settings);// 组件尺寸(可自定义)@State canvasWidth:number=140;@State canvasHeight:number=44;// 回调:每次生成新验证码时把字符串(小写)传给父组件onCodeChange?:(code:string)=>void;// 外部控制器controller?:ImageCodeController;aboutToAppear():void{if(this.controller){this.controller.refresh=()=>{this.generateAndDraw();};}}// 生成随机验证码并绘制privategenerateAndDraw():void{constcodeStr=this.generateRandomCode();if(this.onCodeChange){this.onCodeChange(codeStr);}this.drawCodeToCanvas(codeStr);}// 生成随机验证码(字符集:大写+小写+数字,排除0/O/I/l/1等易混淆字符)privategenerateRandomCode():string{constcharset='ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';constlen=4;letresult='';for(leti=0;i<len;i++){constrandomIndex=this.getRandomInt(0,charset.length);result+=charset.charAt(randomIndex);}returnresult.toLowerCase();}// 真随机整数 [min, max)privategetRandomInt(min:number,max:number):number{try{constrand=cryptoFramework.createRandom();constrandData=rand.generateRandomSync(4);letrandomValue=(randData.data[0]<<24|randData.data[1]<<16|randData.data[2]<<8|randData.data[3])>>>0;randomValue=randomValue/0xFFFFFFFF;returnMath.floor(randomValue*(max-min)+min);}catch(error){// 降级使用 Math.random()returnMath.floor(Math.random()*(max-min)+min);}}// 随机颜色privategetRandomColor():string{constr=this.getRandomInt(80,220);constg=this.getRandomInt(80,220);constb=this.getRandomInt(80,220);return`rgb(${r},${g},${b})`;}privatedrawCodeToCanvas(code:string):void{constw=this.canvasWidth;consth=this.canvasHeight;constctx=this.ctx;ctx.clearRect(0,0,w,h);// 1. 背景色ctx.fillStyle='#F8F9FA';ctx.fillRect(0,0,w,h);// 2. 先绘制文字(在最底层,之后会被噪点和线条覆盖部分)constcharCount=code.length;constbaseX=w*0.2;conststep=(w*0.6)/charCount;ctx.font=`bold${Math.floor(h*0.5*3.5)}px "Courier New", "Fira Code", monospace`;ctx.textAlign='center';ctx.textBaseline='middle';for(leti=0;i<charCount;i++){constchar=code.charAt(i).toUpperCase();constx=baseX+i*step+this.getRandomInt(-6,6);consty=h/2+this.getRandomInt(-h*0.18,h*0.18);constangle=(this.getRandomInt(-28,28)*Math.PI)/180;ctx.save();ctx.translate(x,y);ctx.rotate(angle);// 文字颜色constmidR=this.getRandomInt(90,170);constmidG=this.getRandomInt(90,170);constmidB=this.getRandomInt(90,170);ctx.fillStyle=`rgb(${midR},${midG},${midB})`;ctx.shadowBlur=1;ctx.shadowColor='rgba(0,0,0,0.25)';ctx.fillText(char,0,0);ctx.shadowBlur=0;ctx.restore();}// 3. 再绘制噪点(覆盖部分文字,增加干扰)constdotCount=this.getRandomInt(60,120);for(leti=0;i<dotCount;i++){ctx.fillStyle=this.getRandomColor();ctx.fillRect(this.getRandomInt(0,w),this.getRandomInt(0,h),1,1);}// 4. 最后绘制自由线条(干扰线,覆盖文字和噪点)constlineCount=this.getRandomInt(6,10);for(leti=0;i<lineCount;i++){ctx.beginPath();conststartX=this.getRandomInt(0,w);conststartY=this.getRandomInt(0,h);constendX=this.getRandomInt(0,w);constendY=this.getRandomInt(0,h);ctx.moveTo(startX,startY);constcpX=this.getRandomInt(0,w);constcpY=this.getRandomInt(0,h);ctx.quadraticCurveTo(cpX,cpY,endX,endY);ctx.strokeStyle=this.getRandomColor();ctx.lineWidth=this.getRandomInt(1,2);ctx.stroke();}// 5. 边框(最上层,无干扰)ctx.beginPath();ctx.strokeStyle='#CCCCCC';ctx.lineWidth=1;ctx.strokeRect(2,2,w-4,h-4);}build(){Canvas(this.ctx).width(this.canvasWidth).height(this.canvasHeight).borderRadius(8).onClick(()=>{this.generateAndDraw();}).onReady(()=>{this.generateAndDraw();});}}3. 使用示例:Index.ets
import{promptAction}from'@kit.ArkUI';import{ImageCode}from'../components/ImageCode';import{ImageCodeController}from'../controller/ImageCodeController';@Entry @Component struct Index{@State inputCode:string='';@State currentCode:string='';privatecodeController:ImageCodeController=newImageCodeController();// 验证用户输入privatecheckCode(){if(this.inputCode.toLowerCase()===this.currentCode){promptAction.showToast({message:'验证成功'});}else{promptAction.showToast({message:'验证码错误'});}}build(){Column({space:20}){Text('图形验证码示例').fontSize(20).fontWeight(FontWeight.Bold)Row({space:12}){ImageCode({onCodeChange:(code)=>{this.currentCode=code;},controller:this.codeController})Button('刷新').onClick(()=>{this.codeController.refresh();})}Row({space:12}){TextInput({placeholder:'请输入验证码'}).layoutWeight(1).onChange((val)=>{this.inputCode=val;})Button('验证').onClick(()=>{this.checkCode()})}}.padding(20).width('100%').height('100%')}}四、关键细节解析
- 真随机性:使用
cryptoFramework生成4字节随机数,转换为0~1之间的浮点数,再映射到指定范围,比Math.random()更难以预测。 - 干扰元素设计:
- 干扰线采用二次贝塞尔曲线(
quadraticCurveTo),比直线更难被程序识别。 - 噪点密度可调,颜色鲜艳,增加了OCR识别难度。
- 干扰线采用二次贝塞尔曲线(
- 字符样式:
- 每个字符独立设置中等亮度颜色(RGB分量90~170),确保与浅色背景和彩色线条有足够对比,同时避免过深导致过于清晰。
- 随机偏移(-66px)和随机旋转(-28°28°),使字符位置错落有致。
- 易用性:
- 组件通过
onCodeChange自动向上传递当前验证码(小写)。 - 外部通过
ImageCodeController可随时刷新验证码。 - 用户输入验证码时自动转为小写进行比较,提升体验。
- 组件通过
五、总结与扩展
本文实现了一个功能完整、视觉丰富的图形验证码组件,可作为鸿蒙Next应用的基础库。您可以根据实际需求轻松扩展:
- 修改字符位数:调整
generateRandomCode中的循环次数。 - 更换字符集:增减
charset字符串(注意保留易混淆字符处理)。 - 调整画布尺寸:通过
@State canvasWidth/Height或传入属性动态设置。 - 增加算术验证码:例如显示“23 + 7 = ?”,只需修改
generateRandomCode逻辑。
希望本篇实战对您有所帮助。如果您在集成中遇到任何问题,欢迎在评论区交流!