背景
验证码就是把一串随机产品的数字动态生成一幅图片,再加上干扰元素。此时用户可以通过肉眼能识别里面的数字或者字符,但是可以屏蔽机器的自动识别。
很多地方需要用到验证码,因为Web网站经常会遇到身份欺骗的攻击,攻击者通过在客户端脚本写入代码,写请求消耗远远大于读请求。大量写请求的情况,会严重耗费系统的资源。所以说验证码主要是防止有人利用机器人进行自动批量注册等行为。
笔者看过了http://www.imooc.com/learn/115
的视频教程以后非常有感触,想把实现验证码的步骤一步一步的记录下来。同时最后的时候他封装成类,可以在之后的工作中进行反复的利用。
其实验证码的代码已经研究了很多次了,但是大部分网上找到的教程都是一次性给出所有的代码,这样阅读的时候很难把握住笔者的开发思路,本教程希望一步一步的截图上代码,帮助新人理清思路。
验证码的核心技术
首先需要对实现验证码的步骤进行分解。主要可以分为如下几步:
- 生成验证码底图
- 生成随机字符,然后和底图合并
- 当用户输入的时候,和原内容进行对比校验。
接下来我们分不同的章节介绍如何实现每一个步骤。
环境
项目 | 内容 |
---|---|
操作系统 | windows |
环境 | xamp |
需要启动Apache,验证方法是在浏览器框里输入127.0.0.1
如果出现如下的图片,则启动成功。
实现验证码底图
首先是通过PHP实现100×30px大小的图片。
主要使用的函数方法:resource imagecreatetruecolor(int $width, int $height)
在D:\xampp\htdocs下建立project文件夹。新建captcha.php
输入:
<?php $image = imagecreatetruecolor(100,30);//生成资源 $bgcolor = imagecolorallocate($image , 255,255,255);//#ffffff,为一幅图像分配颜色 imagefill($image , 0 , 0 , $bgcolor);//左上角,区域填充 header("content-type:image/png"); imagepng($image); imagedestroy($image); ?>
在浏览器输入框输入http://127.0.0.1/project/captcha.php
结果是一片大白。
下面解释一下里面涉及到的函数:
项目 | 内容 |
---|---|
imagecreatetruecolor | 生成空白的画布 |
imagecolorallocate | 分配颜色 |
imagefill | 填充颜色 |
header | 发送HTTP头 |
imagepng | 以png格式进行导出 |
imagedestory | 输出结束的时候记得回收资源 |
为了表明各函数用法,特将PHP手册中的详细用法摘录如下:
- imagecreatetruecolor — 新建一个真彩色图像
resource imagecreatetruecolor ( int $width , int $height )
- imagecolorallocate — 为一幅图像分配颜色
int imagecolorallocate ( resource $image , int $red , int $green , int $blue )
第一次对 imagecolorallocate() 的调用会给基于调色板的图像填充背景色,即用 imagecreate() 建立的图像。
其他red,green,blue对应RGB值 - imagefill — 区域填充
bool imagefill ( resource $image , int $x , int $y , int $color )
imagefill() 在 image 图像的坐标 x,y(图像左上角为 0, 0)处用 color 颜色执行区域填充(即与 x, y 点颜色相同且相邻的点都会被填充)。 - header — 发送原生 HTTP 头
void header ( string $string [, bool $replace = true [, int $http_response_code ]] )
请注意 header() 必须在任何实际输出之前调用 - imagepng — 以 PNG 格式将图像输出到浏览器或文件
bool imagepng ( resource $image [, string $filename ] )
- imagedestroy — 销毁图片资源
bool imagedestroy ( resource $image )
注意事项:
- 依赖于GD扩展,输出图片前必须提前输出图片header信息
- 默认输出为黑色背景。
实现数字验证码
在上一章里面我们已经新建了一张画布,相当于把整个框架搭起来了,本章的主要目的是在上面加上随机的数字。
给图片加上数字,主要使用imagestring()方法。
imagestring — Draw a string horizontally
bool imagestring ( resource $image , int $font , int $x , int $y , string $string , int $color )
- font:表示字体,
- x,y:表示位置
从上面这个函数的参数可以看出,我们需要给出生成随机验证码数字的字体,位置,内容和颜色。
字体是死的,可以直接规定大小。
因为要生成4个随机数字,可以使用一个for循环for ($i = 0 ; $i < 4 ; $i++)
。
下面讨论怎么获得位置信息:
可以把长度100分成4份,所以每个数字的位置是($i * 100 / 4 )
,然后加上随机的offset,可以得到
$x = ($i * 100 / 4 ) + rand (5,10);
内容通过:$fontContent = rand (0,9);
获得。
最后是怎么获得随机的颜色:
对于imagecolorallocate()这个函数而言,只需要获得随机的RGB值,就可以实现随机的颜色了。为了更加醒目,我们限定随机RGB值的范围为0~120,因为是深色区间。
$fontColor = imagecolorallocate($image , rand(0,120) , rand(0,120), rand(0,120));//0~120是深色区间。
下面加上生成随机数字的代码段,在imagefill()那行下面加入:
<?php $image = imagecreatetruecolor(100,30);//生成资源 $bgcolor = imagecolorallocate($image , 255,255,255);//#ffffff,为一幅图像分配颜色 imagefill($image , 0 , 0 , $bgcolor);//左上角,区域填充 // 生成随机数字 for ($i = 0 ; $i < 4 ; $i++){ $fontSize = 6; $fontColor = imagecolorallocate($image , rand(0,120) , rand(0,120), rand(0,120));//0~120是深色区间。 $fontContent = rand (0,9); // 坐标 $x = ($i * 100 / 4 ) + rand (5,10);//总宽度100,放4个数字。 $y = rand (5 , 10); imagestring ($image,$fontSize,$x,$y,$fontContent,$fontColor); } header("content-type:image/png"); imagepng($image); imagedestroy($image); ?>
在地址栏里面输入127.0.0.1/project/captcha.php
可以看出生成了4个随机的数字。
增加干扰元素
为了防止被机器轻易的读出来,可以增加干扰的元素。
先增加干扰的小点,用到函数
bool imagesetpixel(resource $image , int $x , int $y , int $color)
首先看点的位置怎么得到。
既然是随机生成用来干扰的点,那么可以在整个画布随机的跑,所以x = rand (1 , 99); y = rand (1 , 29)
而点的颜色注意要比字的浅,所以在50~200中选。
上代码,增加200个点。
<?php $image = imagecreatetruecolor(100,30);//生成资源 $bgcolor = imagecolorallocate($image , 255,255,255);//#ffffff,为一幅图像分配颜色 imagefill($image , 0 , 0 , $bgcolor);//左上角,区域填充 for ($i = 0 ; $i < 4 ; $i++){ $fontSize = 6; $fontColor = imagecolorallocate($image , rand(0,120) , rand(0,120), rand(0,120));//0~120是深色区间。 $fontContent = rand (0,9); // 坐标 $x = ($i * 100 / 4 ) + rand (5,10);//总宽度100,放4个数字。 $y = rand (5 , 10); imagestring ($image,$fontSize,$x,$y,$fontContent,$fontColor); } for ($i = 0 ; $i < 200 ; $i++){ $pointColor = imagecolorallocate ($image , rand (50,200),rand(50,200),rand(50,200));//颜色要比数字的浅。 imagesetpixel ($image , rand (1,99),rand(1,29),$pointColor);//生成的点不要超过画布。 } header("content-type:image/png"); imagepng($image); imagedestroy($image); ?>
依旧看看效果:
再加点随机的线:
使用方法:bool imageline (resource $image , int $x1 , int $y1 , int $x2 , int $y2 , int $color)
手册上的解释是:
Draws a line between the two given points.
也就是说在给出的两点间画线。
注意随机线的颜色比随机点的颜色要更浅,所以在80~220里面选。
下面给出加随机线的代码:
// 生成干扰线 for ( $i = 0 ; $i < 3 ; $i++){ $lineColor = imagecolorallocate($image , rand (80,220),rand(80,220),rand(80,220)); imageline($image , rand (1,99),rand(1,29),rand(1,99),rand(1,29),$lineColor); }
结果如图:
生成随机的字母和数字
之前生成的内容只有随机的数字,还是容易被机器识别出来,所谓道高一尺魔高一丈,对手在加码,我们也得加码撒。下面我们在随机的内容里面加上字母,这样组合更多,可以更好的屏蔽那些机器人。
首先我们把可能用到的字母和数字放到变量$data里面去,注意可以把容易混淆的字母和数字去掉,比如l和数字1容易混,所以直接干掉。
然后每次随机的从字符串里面抽一个出来,使用substr函数。
string substr ( string $string , int $start [, int $length ] )
从$string里$start位置抽出$length长的字符。
这样对原有的代码进行简单的修改就可以了。
<?php $image = imagecreatetruecolor(100,30);//生成资源 $bgcolor = imagecolorallocate($image , 255,255,255);//#ffffff,为一幅图像分配颜色 imagefill($image , 0 , 0 , $bgcolor);//左上角,区域填充 // 生成随机数字 for ($i = 0 ; $i < 4 ; $i++){ $fontSize = 6; $fontColor = imagecolorallocate($image , rand(0,120) , rand(0,120), rand(0,120));//0~120是深色区间。 // $fontContent = rand (0,9); $data = "abcdefghijkmnopqrstuvwxyz23456789"; // 生成随机的字母和数字,从$data字符串里面,任意一个位置,取一个字母 $fontContent = substr($data , rand(0,strlen($data)), 1); // 坐标 $x = ($i * 100 / 4 ) + rand (5,10);//总宽度100,放4个数字。 $y = rand (5 , 10); imagestring ($image,$fontSize,$x,$y,$fontContent,$fontColor); } for ($i = 0 ; $i < 200 ; $i++){ $pointColor = imagecolorallocate ($image , rand (50,200),rand(50,200),rand(50,200));//颜色要比数字的浅。 imagesetpixel ($image , rand (1,99),rand(1,29),$pointColor);//生成的点不要超过画布。 } // 生成干扰线 for ( $i = 0 ; $i < 3 ; $i++){ $lineColor = imagecolorallocate($image , rand (80,220),rand(80,220),rand(80,220)); imageline($image , rand (1,99),rand(1,29),rand(1,99),rand(1,29),$lineColor); } header("content-type:image/png"); imagepng($image); imagedestroy($image); ?>
效果如图
通过session存储验证信息
session是存储的服务器端的信息,可以把生成的内容保存在服务器端的$_SESSION["authcode"]字段里面。当前端的用户输入相应的验证字符后,和$_SESSION["authcode"]进行对比,如果相同,自然证明输入正确、
需要在代码的最顶部加上session_start()
现在要考虑如何保存保存验证码的信息,先定义一个$captch_code 的空字符串,每生成一的随机的数字或者字符,都拼接到上面去。
其实在真实的开发环境下,我们都是用服务器的集群来保存session的,也就是说有可能第一次请求的时候建立的session保存在A服务器上,但是下次需要来拿数据的时候会被分配到B服务器上,所以校验会失败。在多服务器的情况下,我们需要考虑集中管理session,比如使用memcache来保存session信息。
<?php session_start(); $image = imagecreatetruecolor(100,30);//生成资源 $bgcolor = imagecolorallocate($image , 255,255,255);//#ffffff,为一幅图像分配颜色 imagefill($image , 0 , 0 , $bgcolor);//左上角,区域填充 // 生成随机数字 $captch_code = ""; for ($i = 0 ; $i < 4 ; $i++){ $fontSize = 6; $fontColor = imagecolorallocate($image , rand(0,120) , rand(0,120), rand(0,120));//0~120是深色区间。 // $fontContent = rand (0,9); $data = "abcdefghijkmnopqrstuvwxyz23456789"; // 生成随机的字母和数字,从$data字符串里面,任意一个位置,取一个字母 $fontContent = substr($data , rand(0,strlen($data)), 1); $captch_code .= $fontContent; // 坐标 $x = ($i * 100 / 4 ) + rand (5,10);//总宽度100,放4个数字。 $y = rand (5 , 10); imagestring ($image,$fontSize,$x,$y,$fontContent,$fontColor); } // 将循环生成的随机数字保存在$_SESSION里面 $_SESSION['authcode'] = $captch_code; for ($i = 0 ; $i < 200 ; $i++){ $pointColor = imagecolorallocate ($image , rand (50,200),rand(50,200),rand(50,200));//颜色要比数字的浅。 imagesetpixel ($image , rand (1,99),rand(1,29),$pointColor);//生成的点不要超过画布。 } // 生成干扰线 for ( $i = 0 ; $i < 3 ; $i++){ $lineColor = imagecolorallocate($image , rand (80,220),rand(80,220),rand(80,220)); imageline($image , rand (1,99),rand(1,29),rand(1,99),rand(1,29),$lineColor); } header("content-type:image/png"); imagepng($image); imagedestroy($image); ?>
验证码通过表单进行提交
在project文件夹里面新建form.php
输入
<pre>
<?php?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>确认验证码</title>
</head>
</pre>
<body> <form action="./form.php" method="post"> <p>验证图片:![](./captcha.php)</p> <p>请输入图片中的内容:<input type="text" name="authcode" value=""/></p> <p><input type="submit" value="提交" style="padding:6px 20px;"/></p> </form> </body> </html>
表单提交的内容都会存到$_REQUEST['authcode']里面,所以需要加上验证表单提交和$_SESSION[‘authcode’]是否相同的代码。
<pre>
<?php
if (isset($_REQUEST['authcode'])){
session_start();
if($_REQUEST['authcode'] == $_SESSION['authcode']){
echo '<font color="#0000CC">输入正确</font>';
}else{
echo '<font color = "#CC0000"><b>输入错误</b></font>';
}
exit();
}
?>
</pre>
此时在地址栏输入127.0.0.1/project/form.php,然后填入验证码字符
点击提交按钮。
但是如果是输入全大写的字母,会输出“输入错误”的标志。
因为我们在判断的时候是把原始输入和服务器保存的进行比较,所以自然需要完全一致才行。
可以把用户的输入全部转换为小写
<pre>
<?php
if (isset($_REQUEST['authcode'])){
session_start();
if(strtolower(trim($_REQUEST['authcode'])) == $_SESSION['authcode']){
echo '<font color="#0000CC">输入正确</font>';
}else{
echo '<font color = "#CC0000"><b>输入错误</b></font>';
}
exit();
}
?>
</pre>
动态验证
之前的代码存在一个问题,如果出来的验证码图片用户不能完全分辨得出,就没有办法了。在生活中,一般的验证码图片旁边都会有“看不清?”这样的提示,点击的话,会刷新验证码。
可以通过三个步骤进行:
- 增加可点击的“换一个”文案
- 用JS选择器选取图片
- 用js修改验证码图片地址
首先增加文案
<pre>
<a href="javascript:void(0)">换一个?</a>
</pre>
为了方便选取图片,给img加上id属性,使用document.getElementById('captch_img')获得图片,然后用新的图片对他进行赋值,注意可以加上一段随机数字。
<pre>
<?php
if (isset($_REQUEST['authcode'])){
session_start();
if(strtolower(trim($_REQUEST['authcode'])) == $_SESSION['authcode']){
echo '<font color="#0000CC">输入正确</font>';
}else{
echo '<font color = "#CC0000"><b>输入错误</b></font>';
}
exit();
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>确认验证码</title>
</head>
<body>
<form action="./form.php" method="post">
<p>验证图片:<img id="captcha_img" border="1" src="./captcha.php?r=<?php echo rand();?>" width="100" heigh="30">
<a href="javascript:void(0)" onclick="document.getElementById('captcha_img').src='./captcha.php?r='+Math.random()">换一个?</a>
</p>
<p>请输入图片中的内容:<input type="text" name="authcode" value=""/></p>
<p><input type="submit" value="提交" style="padding:6px 20px;"/></p>
</form>
</body>
</html>
</pre>
至此验证码的功能基本都实现了,下面主要进行类的封装,以后可以多次的复用。
验证码类的封装
首先考虑通用的验证码类里面有那些元素:
<pre>
class Vcode {
private $width; //验证码图片的宽度
private $height; //验证码图片的高度
private $codeNum; //验证码字符的个数
private $disturbColorNum; //干扰元素数量
private $checkCode; //验证码字符
private $image; //验证码资源
}
</pre>
然后写构造方法,主要用来实例化验证码对象,并为一些成员属性初使化 。
在写构造方法之前,我们新定义一个内部私有方法createCheckCode(),随机生成用户指定个数的字符串,去掉了容易混淆的字符oOLlz和数字012
<pre>
private function createCheckCode(){
$data="3456789abcdefghijkmnpqrstuvwxy";
for($i=0; $i<$this->codeNum; $i++) {
// $char = $code{rand(0,strlen($code)-1)};
$fontContent = substr($data , rand(0,strlen($data)), 1);
$captch_code .= $fontContent;
}
return $captch_code;
}
</pre>
然后开始写构造函数:
<pre>
/**
* 构造方法用来实例化验证码对象,并为一些成员属性初使化
* @param int $width 设置验证码图片的宽度,默认宽度值为80像素
* @param int $height 设置验证码图片的高度,默认高度值为20像素
* @param int $codeNum 设置验证码中字母和数字的个数,默认个数为4个
/
function __construct($width=80, $height=20, $codeNum=4) {
$this->width = $width;
$this->height = $height;
$this->codeNum = $codeNum;
$number = floor($height$width/15);
//如果验证码字符过多,则需要减少干扰点的数量
if($number > 240-$codeNum)
$this->disturbColorNum = 240-$codeNum;
else
$this->disturbColorNum = $number;
$this->checkCode = $this->createCheckCode();
}
</pre>
我们之前讲过输出图像有几个步骤,首先新建画布,然后生成干扰点,再加上随机的字符,最后输出。可以把每个步骤包装成一个方法,然后在outImg()里面统一调用。
-
新建画布
<pre>
/* 内部使用的私有方法,用来创建图像资源,并初使化背影 */
private function getCreateImage(){
$this->image = imagecreatetruecolor($this->width,$this->height);$backColor = imagecolorallocate($this->image, rand(225,255),rand(225,255),rand(225,255)); @imagefill($this->image, 0, 0, $backColor); }
</pre>
设置干扰元素
<pre>
/* 内部使用的私有方法,设置干扰像素,向图像中输出不同颜色的点 */
private function setDisturbColor() {
// 画随机点
for($i=0; $i <= $this->disturbColorNum; $i++) {
$color = imagecolorallocate($this->image, rand(50,200), rand(50,200), rand(50,200));
imagesetpixel($this->image,rand(1,$this->width-2),rand(1,$this->height-2),$color);
}
// 画随机线
for($i=0; $i<10; $i++){
$color=imagecolorallocate($this->image,rand(80,220),rand(80,220),rand(80,220));
imageline($this->image,rand(-10,$this->width),rand(-10,$this->height),rand(30,300),rand(20,200),55,44,$color);
}
}
</pre>在图片上写字
<pre>
/* 内部使用的私有方法,随机颜色、随机摆放、随机字符串向图像中输出 /
private function outputText() {
for ($i=0; $i<=$this->codeNum; $i++) {
$fontcolor = imagecolorallocate($this->image, rand(0,128), rand(0,128), rand(0,128));
$fontSize = rand(3,5);
$x = floor($this->width/$this->codeNum)$i+3;
$y = rand(0,$this->height-imagefontheight($fontSize));
imagechar($this->image, $fontSize, $x, $y, $this->checkCode{$i}, $fontcolor);
}
}
</pre>
- 输出图像,
<pre>
/* 内部使用的私有方法,自动检测GD支持的图像类型,并输出图像 */
private function outputImage(){
if(imagetypes() & IMG_GIF){
header("Content-type: image/gif");
imagegif($this->image);
}elseif(imagetypes() & IMG_JPG){
header("Content-type: image/jpeg");
imagejpeg($this->image, "", 0.5);
}elseif(imagetypes() & IMG_PNG){
header("Content-type: image/png");
imagepng($this->image);
}elseif(imagetypes() & IMG_WBMP){
header("Content-type: image/vnd.wap.wbmp");
imagewbmp($this->image);
}else{
die("PHP不支持图像创建!");
}
}
</pre>
- 使用outImg()函数统一起来
<pre>
/* 内部使用的私有方法,用于输出图像 */
private function outImg(){
$this->getCreateImage();
$this->setDisturbColor();
$this->outputText();
$this->outputImage();
}
</pre>
向服务器SESSION中保存验证码
<pre>
/**
* 用于输出验证码图片,也向服务器的SESSION中保存了验证码,使用echo 输出对象即可
/
function __toString(){
/ 加到session中, 存储下标为code */
$_SESSION["code"] = strtoupper($this->checkCode);
$this->outImg();
return '';
}
</pre>析构函数
<pre>
/* 析构方法,在对象结束之前自动销毁图像资源释放内存 */
function __destruct(){
imagedestroy($this->image);
}
</pre>
至此验证码类已经完成,只要直接输出对象,就可以向浏览器中输出图片,可以在表单中使用。
在提交验证码到服务器的时候,已经转为了大写,需要在验证的时候把浏览器提交的也转换为大写。
应用验证码类
新建imgcode.php文件,使用session_start()开启回话控制,同时创建类的对象。
此时如果使用echo输出,同时会自动将验证码字符串保存在服务器中。
<pre>
<?php
/**
file:imgcode.php
用于请求时,通过验证码类的对象向客户端输出图片
*/
session_start(); //开启SESSION,会使用$_SESSION["code"]在服务器中保存验证码
require_once('captch.class.php'); //包含验证码所在的类文件
echo new Vcode(); //创建验证码对象,并直接被输出自动调用魔术__toString()方法
</pre>
构造表单,应用验证码
在form.php中,包含输入表单和匹配验证码两部分。
<pre>
<?php
/** form.php 用于输出用户操作表单和验证用户的输入 /
session_start(); //开启SESSION
if(isset($_POST['submit'])){ //判断用户提交后执行
/ 判断用户在表单中输入的字符串和验证码图片中的字符串是否相同 /
if(strtoupper(trim($_POST["code"])) == $_SESSION['code']){ //如果验证码输出成功
echo '验证码输入成功
'; //输出成功的提示信息
}else{ //如果验证码输入失败
echo '<font color="red">验证码输入错误!!</font>
'; //输出失败的输入信息
}
}
?>
<html>
<head>
<title>Image</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<script>
/ 定义一个JavaScript函数,当单击验证码时被调用,将重新请求并获取一个新的图片 /
function newgdcode(obj,url) {
/ 后面传递一个随机参数,否则在IE7和火狐下,不刷新图片 */
obj.src = url+ '?nowtime=' + new Date().getTime();
}
</script>
</head>
<body> ![](imgcode.php) <form method="POST" action="form.php"> <input type="text" size="4" name="code" /> <input type="submit" name="submit" value="提交"> </form> </body> </html>