现在的验证码真是越来越高级了,12306 的找图验证码,极验的拖动式验证码,还有国外的一些黑科技,能智能判断你是不是机器人的验证码。
验证码的更新迭代让我突然对传统验证码一下子不满足了,出于挑战自我和对自己技能的修炼,我用了一周的时间写了一个简单的 demo ,然后又花了一周时间将其优化成插件的形式,于是 就诞生了。
简单介绍下 Clicaptcha ,它是由 click 和 captcha 这两个单子合并而成,顾名思义,这是一个点击验证码,那怎么个点击验证呢?整个操作流程只需根据提示文字信息,点击图中文字所在位置,即可完成验证,效果图下图:
具体的功能实现这里就不一步步给大家回顾了,感兴趣的可以直接上 oschina 或者 github ,搜索 就可以看到这个项目。
下面我主要是记录一下在开发过程中几个难点,以及我的解决思路,如果你有更好的,希望你能和我交流交流。
难点一:文字随机布局
首先我们要做一些准备工作:
- 背景图片
- 中文字体
- 随机文字
- 字体所占范围(因为是 php 生成,所以借助 GD 库里的 imagettfbbox 方法)
准备好这些后,就可以开始考虑我们的随机布局算法了,其实并不复杂,如果有看过我之前写的《》,其实思路是差不多的。
可以看下上面这张图,假设中间带背景色的区域是已经固定的一个区域,当第2个区域要进行随机生成的时候,大概会有4种情况,也就是图中的这4种,我们只需依次判断以下4种条件,只要有一项符合,则这个随机生成的x,y坐标就可以使用。
x2 + w2 < x1
x1 + w1 < x2
y2 + h2 < y1
y1 + h1 < y2
难点二:字体大小有偏差
其实这倒不算个难点,就是个小细节。
GD 库里的 imagefttext 方法中,设置字体大小并不是以像素(px)为单位的,而是以磅(point)为单位。所以在具体使用的时候,需要进行转换,也就是乘以 0.75 ,比如你需要在图片上展示 50px 大小的字体,则需要 50px * 0.75 = 37.5point 。至于为什么是乘以 0.75 ,可以见下表:
八号 = 5磅(7px) = (5/72)*96 = 6.67 = 6px
七号 = 5.5磅 = (5.5/72)*96 = 7.3 = 7px
小六 = 6.5磅 = (6.5/72)*96 = 8.67 = 8px
六号 = 7.5磅 = (7.5/72)*96 = 10px
小五 = 9磅 = (9/72)*96 = 12px
五号 = 10.5磅 = (10.5/72)*96 = 14px
小四 = 12磅 = (12/72)*96 = 16px
四号 = 14磅 = (14/72)*96 = 18.67 = 18px
小三 = 15磅 = (15/72)*96 = 20px
三号 = 16磅 = (16/72)*96 = 21.3 = 21px
小二 = 18磅 = (18/72)*96 = 24px
二号 = 22磅 = (22/72)*96 = 29.3 = 29px
小一 = 24磅 = (24/72)*96 = 32px
一号 = 26磅 = (26/72)*96 = 34.67 = 34px
小初 = 36磅 = (36/72)*96 = 48px
初号 = 42磅 = (42/72)*96 = 56px
难点三:如何将图片和文字同时输出
我们都知道,在 PHP 中,通过设置 header 的参数,可以输出各种文件类型,但一次只能输出一种数据格式到客户端。
在我这个项目中,除了图片需要输出,同时还需要将提示文字也输出,不然用户就不知道依次点哪些文字进行验证了。
解决这个问题我想到有两种解决方案:
- 将图片保存,把图片地址和提示文字一并输出到前端
- 只输出图片到前端,同时将提示文件放入 cookie 中,前端调取 cookie 显示提示文字
最终我是选择了第二套方案,因为这是个验证码插件,如果每次生成的验证图片都保存下来,对服务器硬盘资源占用将是个大问题。
难点四:如何保证验证信息的安全
在我将后端代码全部开发完成,前端也封装好了一个 jQuery 插件后,发现了一个大问题,就是如果用户通过特殊手段跳过验证码验证,直接提交表单或者相关业务操作怎么办?
因为验证码是以插件的形式存在,所以在调用的参数里有一个 callback 参数,用于验证成功后执行网站本身业务逻辑的代码。这样就可能会有个问题,我用 chrome 按 F12 打开开发者工具,直接在任务台里输入了提交表单的代码并回车执行,然后表单顺利提交了,完完全全跳过了验证。
解决这个问题也不复杂,我思考了传统验证码的验证流程,核心一点就是它是随表单一起提交并做验证的,但由于我这个验证码的特殊性,所以只能增加一个后端二次验证,也就是前端初步验证后,将验证信息随表单提交到后端进行二次验证即可,同时,后端的二次验证成功后,将 session 清除,避免重复刷新提交表单造成能跳过二次验证的问题。
以上就是我对这个项目的难点总结,如果你看到这了,希望对感兴趣的你有点启发,这个项目我同时放在的 和 上,,有兴趣的可以关注下。
以下是针对前两个难点写的一个小demo,如果对完整的源码一时半会难理解的话,可以 copy 以下代码到本地,替换下字体和图片,然后运行一下看看效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | header( "Content-type: text/html; charset=utf-8" ); error_reporting (E_ERROR | E_WARNING | E_PARSE); $imagePath = 'bg.jpg' ; $fontPath = 'msyh.ttc' ; //为什么要乘0.75?因为 imagefttext 方法里的 size 参数使用磅(point)做为单位的,所以需要进行转换,转换为像素 $fontSize = 50 * 0.75; //以“博客园”三个字举例,将文字、尺寸等信息存入数组 foreach ( array ( '博' , '客' , '园' ) as $v ){ $fontarea = imagettfbbox( $fontSize , 0, $fontPath , $v ); $textWidth = $fontarea [2] - $fontarea [0]; $textHeight = $fontarea [1] - $fontarea [7]; $tmp [ 'text' ] = $v ; $tmp [ 'size' ] = $fontSize ; $tmp [ 'width' ] = $textWidth ; $tmp [ 'height' ] = $textHeight ; $textArr [] = $tmp ; } //获取背景底图宽高和类型信息 list( $imageWidth , $imageHeight , $imageType ) = getimagesize ( $imagePath ); //随机生成汉字位置,并附加存入数组 foreach ( $textArr as & $v ){ list( $x , $y ) = randPosition( $textArr , $imageWidth , $imageHeight , $v [ 'width' ], $v [ 'height' ]); $v [ 'x' ] = $x ; $v [ 'y' ] = $y ; } unset( $v ); //创建图片的实例 $image = imagecreatefromstring( file_get_contents ( $imagePath )); //字体颜色 $color = imagecolorallocate( $image , 0, 0, 0); //绘画文字 foreach ( $textArr as $v ){ imagefttext( $image , $v [ 'size' ], 0, $v [ 'x' ], $v [ 'y' ], $color , $fontPath , $v [ 'text' ]); } //生成图片 switch ( $imageType ){ case 1: //GIF header( 'Content-Type: image/gif' ); imagegif( $image ); break ; case 2: //JPG header( 'Content-Type: image/jpeg' ); imagejpeg( $image ); break ; case 3: //PNG header( 'Content-Type: image/png' ); imagepng( $image ); break ; default : break ; } imagedestroy( $image ); //随机生成位置布局 function randPosition( $textArr , $imgW , $imgH , $fontW , $fontH ){ $return = array (); $x = rand(0, $imgW - $fontW ); $y = rand( $fontH , $imgH ); if (!checkPosition( $textArr , $x , $y , $fontW , $fontH )){ $return = randPosition( $textArr , $imgW , $imgH , $fontW , $fontH ); } else { $return = array ( $x , $y ); } return $return ; } function checkPosition( $textArr , $x , $y , $w , $h ){ $flag = true; foreach ( $textArr as $v ){ if (isset( $v [ 'x' ]) && isset( $v [ 'y' ])){ //分别判断X和Y是否都有交集,如果都有交集,则判断为覆盖 $flagX = true; if ( $v [ 'x' ] > $x ){ if ( $x + $w > $v [ 'x' ]){ $flagX = false; } } else if ( $x > $v [ 'x' ]){ if ( $v [ 'x' ] + $v [ 'width' ] > $x ){ $flagX = false; } } else { $flagX = false; } $flagY = true; if ( $v [ 'y' ] > $y ){ if ( $y + $h > $v [ 'y' ]){ $flagY = false; } } else if ( $y > $v [ 'y' ]){ if ( $v [ 'y' ] + $v [ 'height' ] > $y ){ $flagY = false; } } else { $flagY = false; } if (! $flagX && ! $flagY ){ $flag = false; } } } return $flag ; } |
参考资料:
1、
2、
3、