网站验证码的原理及PHP实现

蛰伏已久 蛰伏已久 2018-01-10

在网站开发中,为了防止有人恶意频繁调用我们的接口,经常会用到验证码,比如防止有人通过获取发送短信的接口恶意刷短信,或者防止密码的暴力破解等等,今天小编花了一天时间研究了一下验证码的原理并用PHP实现,给大家分享一下。


验证码的种类:

     我们用到最多的就是字符图片验证码(输入图片中的字符),运算验证码(图片显示一个数学计算题,用户输入计算结果),滑动拼图,图片点选等等,后面这两个是基于用户操作行为的,太难了,因此小编这次没有实现,主要是实现了字符图片验证码和运算验证码。


1.png

2.png

3.png

验证码的原理:

前台请求一张图片验证码,后台随即生成不同字符组合的图片返回给前端,并通过session保存验证码的数值;

当用户提交数据时,通过比对表单中用户输入的验证码和session中保存的验证码是否一致,来判断输入是否正确


验证码的实现:

主要用到PHP的图片绘制功能,生成一个验证码图片我们分这几步骤进行

  1. 生成一个随即字符

  2. 绘制图片区域及背景

  3. 绘制第一步生成的随即字符

  4. 添加一些干扰线或者噪点

  5. 输入图片

  6. 在session中保存验证码


先上代码,以下为PHP YII框架下代码,可以根据情况自行修改

namespace common\helpers;

class ValidateCode{
    private $charset='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';//随即字符库
    private $code;//验证码
    private $codelen=4;//验证码长度
    private $width=130;//宽度
    private $height=50;//高度
    private $img;//图形资源句柄
    private $font;//指定的字体
    private $fontsize=20;//指定字体大小
    private $fontcolor;//指定字体颜色
    private $linecount=3;//横线数量
    private $snowcount=20;//雪花数量
    private $addcode;//加法运算要绘制的字符

    public function __construct($config){
        $this->font = dirname(dirname(__DIR__)).'/frontend/web/font/11.ttf';//注意字体路径要写对,否则显示不了图片

        if(isset($config['charset'])){
            $this->charset=$config['charset'];
        }

        if(isset($config['codelen'])){
            $this->codelen=$config['codelen'];
        }
        if(isset($config['width'])){
            $this->width=$config['width'];
        }
        if(isset($config['height'])){
            $this->height=$config['height'];
        }
        if(isset($config['fontsize'])){
            $this->fontsize=$config['fontsize'];
        }

        if(isset($config['linecount'])){
            $this->linecount=$config['linecount'];
        }
        if(isset($config['snowcount'])){
            $this->snowcount=$config['snowcount'];
        }

    }
    
    // 生成随即验证码
    private function createCode(){
        $len=strlen($this->charset)-1;
        $this->code='';
        for($i=0;$i<$this->codelen;$i++){
            $this->code.=$this->charset[mt_rand(0,$len)];
        }
    }

    //加法运算,字符放入addcode,结果放入code
    private function createAddCode(){
        $first=mt_rand(0,9);
        $second=mt_rand(0,9);
        $this->addcode=$first.'+'.$second.'=';
        $this->code=$first+$second;
    }

    //生成背景
    private function createBg(){
        $this->img=imagecreatetruecolor($this->width,$this->height);
        $color=imagecolorallocate($this->img,mt_rand(157,255),mt_rand(157,255),mt_rand(157,255));
        imagefilledrectangle($this->img,0,0,$this->width,$this->height,$color);
    }

    //生成白色背景
    private function createWhiteBg(){
        $this->img=imagecreatetruecolor($this->width,$this->height);
        $color=imagecolorallocate($this->img,255,255,255);
        imagefilledrectangle($this->img,0,0,$this->width,$this->height,$color);
    }

    //生成文字
    private function createFont(){
        $_x=($this->width-10)/$this->codelen;
        for($i=0;$i<$this->codelen;$i++){
            $this->fontcolor=imagecolorallocate($this->img,mt_rand(0,200),mt_rand(0,200),mt_rand(0,200));
            imagettftext($this->img,$this->fontsize,mt_rand(-30,30),$_x*$i+10+mt_rand(-5,15),$this->height / 1.4,$this->fontcolor,$this->font,$this->code[$i]);
        }
    }

    //生成加法字符
    private function createAddFont(){

        $_x=($this->width-10)/4;
        for($i=0;$i<4;$i++){
            $this->fontcolor=imagecolorallocate($this->img,mt_rand(0,200),mt_rand(0,200),mt_rand(0,200));
            imagettftext($this->img,$this->fontsize,0,$_x*$i+10+mt_rand(-5,15),$this->height / 1.4,$this->fontcolor,$this->font,$this->addcode[$i]);
        }
    }

    //生成线条、雪花
    private function createLine() {
        //线条
        for ($i=0;$i<$this->linecount;$i++) {
            $color = imagecolorallocate($this->img,mt_rand(0,156),mt_rand(0,156),mt_rand(0,156));
            imageline($this->img,mt_rand(0,$this->width),mt_rand(0,$this->height),mt_rand(0,$this->width),mt_rand(0,$this->height),$color);
        }
        //雪花
        for ($i=0;$i<$this->snowcount;$i++) {
            $color = imagecolorallocate($this->img,mt_rand(200,255),mt_rand(200,255),mt_rand(200,255));
            imagestring($this->img,mt_rand(1,5),mt_rand(0,$this->width),mt_rand(0,$this->height),'*',$color);
        }
    }
    //输出
    private function outPut() {
        header('Content-type:image/png');
        imagepng($this->img);
        imagedestroy($this->img);
    }
    //对外生成随即字符验证码
    public function doimg() {
        $this->createBg();
        $this->createCode();
        $this->createLine();
        $this->createFont();
        $this->outPut();
    }
    //对外生成加法运算验证码
    public function doimgAdd() {
        $this->createBg();
        $this->createAddCode();
        $this->createLine();
        $this->createAddFont();
        $this->outPut();
    }
    //对外生成白色背景验证码
    public function doimgWhiteBg() {
        $this->createWhiteBg();
        $this->createCode();
        $this->createLine();
        $this->createFont();
        $this->outPut();
    }
    //对外生成加法运算验证码
    public function doimgAddWhiteBg() {
        $this->createWhiteBg();
        $this->createAddCode();
        $this->createLine();
        $this->createAddFont();
        $this->outPut();
    }
    //获取验证码
    public function getCode() {
        return strtolower($this->code);
    }
}


第一步:生成随即字符串

    从我们提供的字符串库(小写字母,大写字母,数字)中生成一定数量的字符串

private function createCode(){
    $len=strlen($this->charset)-1;
    $this->code='';
    for($i=0;$i<$this->codelen;$i++){
        $this->code.=$this->charset[mt_rand(0,$len)];
    }
}


第二步:绘制背景

    绘制一个图片背景颜色随即的背景,如果想要绘制白色背景,就把3个随机数改为255

private function createBg(){
    $this->img=imagecreatetruecolor($this->width,$this->height);
    $color=imagecolorallocate($this->img,mt_rand(157,255),mt_rand(157,255),mt_rand(157,255));
    imagefilledrectangle($this->img,0,0,$this->width,$this->height,$color);
}


第三步:绘制字符

    这里注意,需要提前设置字体ttf文件的路径

private function createFont(){
    $_x=($this->width-10)/$this->codelen;
    for($i=0;$i<$this->codelen;$i++){
        $this->fontcolor=imagecolorallocate($this->img,mt_rand(0,200),mt_rand(0,200),mt_rand(0,200));
        imagettftext($this->img,$this->fontsize,mt_rand(-30,30),$_x*$i+10+mt_rand(-5,15),$this->height / 1.4,$this->fontcolor,$this->font,$this->code[$i]);
    }
}


第四步:加入干扰因素,增加破解难度

   验证码干扰太多了,会影响用户的输入,总不能让别人输入好几次才能搞对一次,那样的验证码本末倒置了,我们应该在保护良好用户体验的情况下,保证网站安全,不应该通过过度为难用户来提升安全

private function createLine() {
    //线条
    for ($i=0;$i<$this->linecount;$i++) {
        $color = imagecolorallocate($this->img,mt_rand(0,156),mt_rand(0,156),mt_rand(0,156));
        imageline($this->img,mt_rand(0,$this->width),mt_rand(0,$this->height),mt_rand(0,$this->width),mt_rand(0,$this->height),$color);
    }
    //雪花
    for ($i=0;$i<$this->snowcount;$i++) {
        $color = imagecolorallocate($this->img,mt_rand(200,255),mt_rand(200,255),mt_rand(200,255));
        imagestring($this->img,mt_rand(1,5),mt_rand(0,$this->width),mt_rand(0,$this->height),'*',$color);
    }
}


第5步:输出图片

private function outPut() {
    header('Content-type:image/png');
    imagepng($this->img);
    imagedestroy($this->img);
}



在控制器文件中,我们这么调用

将验证码存入session

$validate_code=new ValidateCode([]);
$validate_code->doimgAddWhiteBg();
\Yii::$app->session['validate_code']=$validate_code->getCode();


检验验证码是否正确,可以通过比对用户输入和session进行比对

public function actionCheck(){
    $get=\Yii::$app->request->get();
    if(isset($get['validate_code'])){
        if(strtolower($get['validate_code'])== \Yii::$app->session['validate_code']){
            echo "right";
        }else{
            echo "error";
        }
    }
}


注意:由于我们生成的验证码全都是强制存为小写,这里也要先把用户输入的验证码改为小写



前端验证码的调用

/verify/code是我们生成验证码图片的地址

点击图片的时候,更新图片地址,记得在图片地址后面加一个随机数,这样可以防止图片被缓存,而造成不更新的现象

<form method="get" action="/verify/check">
    <input type="text" name="validate_code"><img src="/verify/code" onclick="this.src='/verify/code?'+Math.random()" alt="点击刷新" title="点击刷新">
    <input type="submit">
</form>


最后看下我们的效果,还不错哈哈

4.png



数学运算验证码的原理和这个稍微有一些区别

简单起见,我只实现了个位数的加法,有兴趣的朋友可以做一些复杂的,

要实现数学运算,首先生成两个随即的个位数a和b,图片上面绘制的字符为a+b=

session中实际存储的为a+b的数值


生成一个数学运算的字符串addcode 和 结果code

private function createAddCode(){
    $first=mt_rand(0,9);
    $second=mt_rand(0,9);
    $this->addcode=$first.'+'.$second.'=';
    $this->code=$first+$second;
}

绘制addcode,前面我们绘制字符串的时候,让字符串随即倾斜了一个角度,而这里我就把倾斜角度设为0了,以免+号 被人看成了 乘法号,其他和图片验证码操作都一样

private function createAddFont(){

    $_x=($this->width-10)/4;
    for($i=0;$i<4;$i++){
        $this->fontcolor=imagecolorallocate($this->img,mt_rand(0,200),mt_rand(0,200),mt_rand(0,200));
        imagettftext($this->img,$this->fontsize,0,$_x*$i+10+mt_rand(-5,15),$this->height / 1.4,$this->fontcolor,$this->font,$this->addcode[$i]);
    }
}


来看看效果


5.png


我的结论

图片验证码和数学运算验证码,相比之下,我更喜欢数学运算,因为用户输入很简单,只需输入1-2为数字即可,也不容易出错,而图片验证码一般要输入4位,特别是手机,感觉很麻烦


存在的问题

由于采用session存储验证码,会存在这样的情况,用户打开了两个注册页面,由于是同一个session,第一个页面的验证码显示的数值其实已经和当前的session不一致了,会造成用户输入验证错误,当然问题不算大, 只需点击图片更新即可,我也看了一下中国移动和中国联通的验证码,也都是存在这个情况。


有兴趣的朋友可以试着解决这个问题,比如请求验证码的时候带一个时间参数,存储session的时候每个验证码存的session key不同







分享到

点赞(0)