PHP 实现“贴吧神兽”验证码

Posted Meadows of Heaven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PHP 实现“贴吧神兽”验证码相关的知识,希望对你有一定的参考价值。

最早看到 “贴吧神兽” 验证码是在百度贴吧,吧主防止挖坟贴,放出了究极神兽验证码

例如:

地址:http://tieba.baidu.com/p/3320323440

 

可以用 php + javascript 实现该种类型的验证码。

使用 jQuery 版本:jQuery 1.9.1

框架使用 ThinkPHP 3.2.3,自定义的验证码类基于 TP 的验证码类

 

最终效果图:

 

自定义验证码类路径:/Application/Home/Common/VerivyPostBar.class.php

控制器:/Application/Home/Controller/PostBarController.class.php

视图:/Applicable/Home/View/PostBarVerify/index.html

 

自定义验证码类 /Application/Home/Common/VerivyPostBar.class.php

<?php

namespace Home\\Common;
use Think\\Verify;

class VerifyPostBar extends Verify {

  private $_image   = NULL;     // 验证码图片实例
  private $_color   = NULL;     // 验证码字体颜色

 public function entryProcess($id = \'\') {
    // 图片宽(px)
    $this->imageW || $this->imageW = $this->length*$this->fontSize*1.5 + $this->length*$this->fontSize/2;
    // 图片高(px)
    $this->imageH || $this->imageH = $this->fontSize * 2.5;

    // 建立一幅 $this->imageW x $this->imageH 的透明图像
    $this->_image = imagecreatetruecolor($this->imageW, $this->imageH); 
    imagesavealpha($this->_image, true);
    $trans_colour = imagecolorallocatealpha($this->_image, 0, 0, 0, 127);
    imagefill($this->_image, 0, 0, $trans_colour);    

    // 验证码字体随机颜色
    $this->_color = imagecolorallocate($this->_image, mt_rand(1,150), mt_rand(1,150), mt_rand(1,150));
    // 验证码使用随机字体
    $ttfPath =  $_SERVER[\'DOCUMENT_ROOT\'].\'/ThinkPHP/Library/Think/Verify/\' . ($this->useZh ? \'zhttfs\' : \'ttfs\') . \'/\';

    if(empty($this->fontttf)){
        $dir = dir($ttfPath);
        $ttfs = array();		
        while (false !== ($file = $dir->read())) {
            if($file[0] != \'.\' && substr($file, -4) == \'.ttf\') {
                $ttfs[] = $file;
            }
        }
        $dir->close();
        $this->fontttf = $ttfs[array_rand($ttfs)];
    } 
    $this->fontttf = $ttfPath . $this->fontttf;
    
    if($this->useImgBg) {
        $this->_background();
    }
    
    if ($this->useNoise) {
        // 绘杂点
        // $this->_writeNoise();
    } 
    if ($this->useCurve) {
        // 绘干扰线
        $this->_writeCurve();
    }
    
    // 绘验证码
    $code = array(); // 验证码
    $codeNX = 0; // 验证码第N个字符的左边距

    if($this->useZh){ // 中文验证码
        for ($i = 0; $i<$this->length; $i++) {
            $code[$i] = iconv_substr($this->zhSet, $i, 1, \'utf-8\');
            imagettftext($this->_image, $this->fontSize, mt_rand(-40, 40), $this->fontSize*($i+1)*1.5, $this->fontSize + mt_rand(10, 20), $this->_color, $this->fontttf, $code[$i]);
        }

        // 备选验证码区域(9个汉字)
        $len_pre_row = $this->area_length / $this->rows; // 每行的字数
        for($r = 0; $r < $this->rows; $r++) {
          $flag = 1;
          $start = $r * $len_pre_row;
          $end = $r * $len_pre_row + $len_pre_row - 1;
          $code_ = array();
          for ($i = $start; $i<$end + 1; $i++) {   
            $code_[$i] = iconv_substr($this->code_area, $i, 1, \'utf-8\');
            // @param image
            // @param size
            // @param angle
            // @param x
            // @param y
            // @param color
            // @param fontfile
            imagettftext($this->_image, $this->fontSize, mt_rand(-20, 20), $this->fontSize*2 * $flag, $this->fontSize + 50 * $r + 120, $this->_color, $this->fontttf, $code_[$i]);
            $flag += 2; // 控制验证码备选字符的x坐标
          }
        }
    }

    // 保存验证码
    $key        =   $this->authcode($this->seKey);
    $code       =   $this->authcode(strtoupper(implode(\'\', $code)));
    $secode     =   array();
    $secode[\'verify_code\'] = $code; // 把校验码保存到session
    $secode[\'verify_time\'] = NOW_TIME;  // 验证码创建时间
    session($key.$id, $secode);
       
    header(\'Cache-Control: private, max-age=0, no-store, no-cache, must-revalidate\');
    header(\'Cache-Control: post-check=0, pre-check=0\', false);		
    header(\'Pragma: no-cache\');
    header("content-type: image/png");

    // 保存图像至硬盘
    imagepng($this->_image, \'Public/Home/Images/verifyimage.png\');
    // 输出图像
    // imagepng($this->_image);
    readfile(\'Public/Home/Images/verifyimage.png\');
    imagedestroy($this->_image);	
	}

  /**
   * 画杂点
   * 往图片上写不同颜色的字母或数字
   */
  private function _writeNoise() {
      $codeSet = \'2345678abcdefhijkmnpqrstuvwxyz\';
      for($i = 0; $i < 10; $i++){
          //杂点颜色
          $noiseColor = imagecolorallocate($this->_image, mt_rand(150,225), mt_rand(150,225), mt_rand(150,225));
          for($j = 0; $j < 5; $j++) {
              // 绘杂点
              imagestring($this->_image, 5, mt_rand(-10, $this->imageW),  mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor);
          }
      }
  }	

  /** 
   * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数) 
   *      
   *      高中的数学公式咋都忘了涅,写出来
   *		正弦型函数解析式:y=Asin(ωx+φ)+b
   *      各常数值对函数图像的影响:
   *        A:决定峰值(即纵向拉伸压缩的倍数)
   *        b:表示波形在Y轴的位置关系或纵向移动距离(上加下减)
   *        φ:决定波形与X轴位置关系或横向移动距离(左加右减)
   *        ω:决定周期(最小正周期T=2π/∣ω∣)
   *
   */
  private function _writeCurve() {
    $px = $py = 0;
    
    // 曲线前部分
    $A = mt_rand(1, $this->imageVerifyH/2);                  // 振幅
    $b = mt_rand(-$this->imageVerifyH/4, $this->imageVerifyH/4);   // Y轴方向偏移量
    $f = mt_rand(-$this->imagimageVerifyHeH/4, $this->imageVerifyH/4);   // X轴方向偏移量
    $T = mt_rand($this->imageVerifyH, $this->imageW*2);  // 周期
    $w = (2* M_PI)/$T;
                    
    $px1 = 0;  // 曲线横坐标起始位置
    $px2 = mt_rand($this->imageW/2, $this->imageW * 0.8);  // 曲线横坐标结束位置

    for ($px=$px1; $px<=$px2; $px = $px + 1) {
        if ($w!=0) {
            $py = $A * sin($w*$px + $f)+ $b + $this->imageVerifyH/2;  // y = Asin(ωx+φ) + b
            $i = (int) ($this->fontSize/5);
            while ($i > 0) {	
                imagesetpixel($this->_image, $px + $i , $py + $i, $this->_color);  // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多				
                $i--;
            }
        }
    }
  } 

  /* 加密验证码 */
  private function authcode($str){
    $key = substr(md5($this->seKey), 5, 8);
    $str = substr(md5($str), 8, 10);
    return md5($key . $str);
  }  

  /**
   * 绘制背景图片
   * 注:如果验证码输出图片比较大,将占用比较多的系统资源
   */
  private function _background() {
      $path = dirname(__FILE__).\'/Verify/bgs/\';
      $dir = dir($path);

      $bgs = array();		
      while (false !== ($file = $dir->read())) {
          if($file[0] != \'.\' && substr($file, -4) == \'.jpg\') {
              $bgs[] = $path . $file;
          }
      }
      $dir->close();

      $gb = $bgs[array_rand($bgs)];

      list($width, $height) = @getimagesize($gb);
      // Resample
      $bgImage = @imagecreatefromjpeg($gb);
      @imagecopyresampled($this->_image, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height);
      @imagedestroy($bgImage);
  }   
}

  

控制器 /Application/Home/Controller/PostBarController.class.php

<?php

namespace Home\\Controller;
use Think\\Controller;
use Home\\Common\\VerifyPostBar;

class PostBarVerifyController extends Controller {

	// 界面
	public function index() {
		header(\'Content-type:text/html;charset=utf-8\');
		$this->display();
	}

	// 验证
	public function check_verify($code) {

		$verify = new VerifyPostBar();
    if(!$verify->check($code)) {
        return 400;
    } else {
    	return 200;
    }
	}

	// 准备验证码字符
	public function prepare_code() {
  	// 验证码的长度
  	$length = 4;
  	// 验证码选区长度
  	$selects = 12;

  	// 相近的汉字为一组,从6组36个汉字中抽出4组12个汉字组成验证码图片组
  	$zhSet = array(
  		array(
  			\'已\',\'己\',\'乙\',\'巳\',\'九\',\'走\'
  		),
  		array(
  			\'田\',\'由\',\'甲\',\'申\',\'白\',\'日\'
  		),
  		array(
  			\'鱼\',\'渔\',\'俞\',\'喻\',\'瑜\',\'愈\'
  		),
  		array(
  			\'请\',\'清\',\'情\',\'青\',\'晴\',\'蜻\'
  		),
  		array(
  			\'宝\',\'玉\',\'穴\',\'必\',\'空\',\'控\'
  		),
  		array(
  			\'子\',\'仔\',\'籽\',\'孜\',\'吱\',\'资\'
  		)
  	);

  	$tmp = array();
  	$count = count($zhSet);
  	$tmp = $this->rand(0, $count - 1, 4); // 随机生成4个不重复的数(0-5组里面选出4组)作为下标
  	$chars = array();
  	foreach($tmp as $key => $val) {
  		$chars[] = $this->choose($zhSet, $val, 3);// 每组3个数
  	}

  	// 从每组一维数组中选出一个组成长度为4的验证码
  	foreach($chars as $key => $val) {
  		$k = mt_rand(0, count($val) - 1);
  		$code[] = $val[$k]; // 验证码
  		unset($chars[$key][$k]);
  	}

		// dump($code);
		// dump($chars);die;

  	// 把数组合并成一维数组
  	$characters = array();
  	foreach($chars as $key => $val) {
  		foreach($val as $k => $v) {
  			$characters[] = $v;
  		}
  	}

  	// 备选验证码区数组
  	$code_area_array = array_merge($code, $characters);
  	
  	shuffle($code_area_array);
  	// 备选验证码区字符串
  	$code_area = implode(\'\', $code_area_array);
  	$code = implode(\'\', $code);

  	$codes[\'code_area\'] = $code_area;
  	$codes[\'code_area_array\'] = $code_area_array;
  	$codes[\'code\'] = $code;
  	$codes[\'characters\'] = $characters;
  	$codes[\'length\'] = $length;

  	$_SESSION[\'code_area_array\'] = $code_area_array;

  	return $codes;

	}

  // 显示验证码
  public function verify() {

  	$codes = $this->prepare_code();

		$conf = array(
			\'useZh\' 		=> 	true,
			\'zhSet\' 		=>	$codes[\'code\'],
			\'code_area\'	=>  $codes[\'code_area\'],
			\'length\'		=>	$codes[\'length\'], // 验证码长度(汉字个数)
			\'rows\'			=> 3, //备选区域3行
			\'area_length\'=> mb_strlen($codes[\'code_area\'], \'utf-8\'), // 备选区域汉字个数
			\'fontSize\'	=>	20,
			\'imageW\'		=>	320,
			\'imageH\'		=>	600,
			\'imageVerifyH\' => 45, // 4字验证码区域高度
		);
		$verify = new VerifyPostBar($conf);
		$verify->entryProcess();
  }	


  // 从一组连续的数字中选出不重复的个数
  // @param $start 数字的开始值
  // @param $end 数字的结束值
  // @param $count 选出的个数
  public function rand($start, $end, $count) {
  	$tmp = range($start, $end);
  	$tmp = array_rand($tmp, $count);
  	return $tmp;
  }

  // 从每组汉字(一组6个)中选出n(4)个
  // @param $array 二维数组
  // @param $key 数组 $array 的下标
  // @param $n 选出几个
  public function choose($array, $key, $n) {
  	$arr = $array[$key];
  	$count = count($arr);
  	$tmp_key = $this->rand(0, $count - 1, $n);
  	$chars = array();
  	foreach($tmp_key as $val) {
  		$chars[] = $arr[$val];
  	}
  	return $chars;
  }

  // 记录点击次数,如果次数达到4次就做出判断,验证码输入是否正确
  public function count_ckick() {
  	
  	session_start();

  	// 坐标数组
  	$codes = $_SESSION[\'code_area_array\'];

  	$xy = array(
  		\'line1_y\'=>array(
  			\'x1\'=>0,
  			\'x2\'=>1,
  			\'x3\'=>2,
  			\'x4\'=>3,
  		),
   		\'line2_y\'=>array(
  			\'x1\'=>4,
  			\'x2\'=>5,
  			\'x3\'=>6,
  			\'x4\'=>7,
  		),
  		\'line3_y\'=>array(
  			\'x1\'=>8,
  			\'x2\'=>9,
  			\'x3\'=>10,
  			\'x4\'=>11,
  		) 		
  	);

  	if(! isset($_POST[\'clear\']) || $_POST[\'clear\'] != 1) {
	  	$_SESSION[\'count\'] = $count = $_POST[\'count\'];

	  	if($count > 4) {
	  		$_SESSION[\'count\'] = 4;
	  	} else {
		  	// 记录选择的验证码文字
		  	$x = $_POST[\'x\'];
		  	$y = $_POST[\'y\'];

		  	foreach($xy as $key => $val) {
		  		foreach($val as $k => $v) {
		  			if($y == $key) {
		  				if($x == $k) {
		  					$code_key = $codes[$v];
		  				}
		  			}
		  		}
		  	}
	  	}
	  	if(! isset($_SESSION[\'input_code\'])) {
	  		$_SESSION[\'input_code\'] = $code_key;
	  	} else {
	  		$_SESSION[\'input_code\'] .= $code_key;
	  	}

	  	$return = \'点击了 \'.$_SESSION[\'count\'].\' 次, 选中的汉字是: \'.$code_key.\' 输入的验证码是: \'.$_SESSION[\'input_code\'];

	  	if($count == 4) {
	  		$code = $this->check_verify($_SESSION[\'input_code\']);
	  		if($code == 200) {
	  			$return .= \' 输入正确\';
	  		} else {
	  			$return .= \' 输入错误\';
	  		}
	  	}
			echo $return;
  	} else { // 清除点击次数
  		$_SESSION[\'count\'] = 0;
  		unset($_SESSION[\'input_code\']);
  		echo \'成功清除了点击次数,点击次数为\',$_SESSION[\'count\'],\'次\';
  	}
  }

  // 获取session中记录的点击次数
  public function record_click() {
  	session_start();
  	if(! isset($_SESSION[\'count\'])) {
  		$_SESSION[\'count\'] = 0;
  	}
  	echo $_SESSION[\'count\'];
  }
  
  // 修改点击记录数
  public function update_click() {
  	session_start();
  	if(! isset($_SESSION[\'count\'])) {
  		$_SESSION[\'count\'] = 0;
  	} else {
  		$newcount = $_SESSION[\'count\'] + $_POST[\'times\'];
  		if($newcount < 0) {
  			$_SESSION[\'count\'] = 0;
  			unset($_SEIION[\'input_code\']);
  		} else {
  			$_SESSION[\'count\'] = $newcount;
  			$_SESSION[\'input_code\'] = mb_substr($_SESSION[\'input_code\'], 0, -1, \'utf-8\');
  		}
  	}
  	echo \'点击数是:\'.$_SESSION[\'count\'].\' 验证码是:\'.$_SESSION[\'input_code\'];
  } 
}

  

视图 /Applicable/Home/View/PostBarVerify/index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<style>
		#verify_area {
			width:600px;
			height: 400px;
			position: relative;
			padding:10px;
		}
		h1 { 
			font-size:16px;
			font-family: "微软雅黑";
			color: #999;
			text-indent: 30px;
		}
		#notice {
			position: relative;
			top: 95px;
			left: 30px;
			color: #666;
			display: block;
			z-index: 2;
		}
		#buttons {
			width: 350px;
			height: 150px;
			position: relative;
			top: 110px;
			left: 20px;
			z-index: 2;
			padding: 0;
		}
		#verify_pic {
			position: relative;
			top: -150px;	
		}		
		.button {
			display: inline-block;
			cursor: pointer;
	    margin: -15px 12px 15px 5px;
	    width: 60px;
	    height: 45px;
	    border: 1px solid #E0E0E0;
	    border-bottom-color: #BFBFBF;
	    outline: 0;			
	    background: -ms-linear-gradient(top,#fff,#f5f5f5);
	    background: -webkit-gradient(linear,left top,left bottom,from(#fff),to(#f5f5f5));
	    background: -moz-linear-gradient(top,#fff,#fafafa);
	    filter: progid:DXImageTransform.Microsoft.Gradient(gradientType=0, startColorStr=#FFFFFF, endColorStr=#F5F5F5);
	    -webkit-opacity: 0.3; 
	    -moz-opacity: 0.3;
	    -khtml-opacity: 0.3;   
	    opacity: .3;  
	    filter:alpha(opacity=30); 
	    -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
	    filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=30);    
	    zoom: 1;	    
		}	
		#verify_title {
			text-indent: 30px;
			position: relative;
			top: 25px;
			font-family: "微软雅黑";
			color: #999;			
		}	
		#verify {
			width: 200px;
			height: 34px;
			position: relative;
			top: 0px;
			left: 80px;
			border: 1px solid #ccc;
		}
		.verify {
			width: 40px;
			height: 34px;
			border-right: 1px solid #ccc;
			float: left;
			background-repeat: no-repeat;
		}
		.verify_last { border-right: 0 }
		.cls { clear: both }
		.hid { display: none; }
		#delete {
			position: relative;
			left: 162px;
			width: 39px;
			height: 34px;
			background: url(\'__PUBLIC__/Images/delete.png\') 0 0 no-repeat;
			cursor: pointer;
		}
		.addbg {
			background-image: url("__PUBLIC__/Images/verifyImage.png")
		}
	</style>
	<script src="__PUBLIC__/Js/jquery-1.9.1.min.js"></script>
</head>
<body>
	<div id="verify_area">
		<form action="{:U(\'Home/PostBarVerify/check_verify\',\'\',\'\')}" method="post" id="form">
			<h1>点击验证码图片换一张</h1>
			<div>
				<div id="verify_title">验证码</div>
				<div id="verify">
					<div id="verify1" class="verify"></div>
					<div id="verify2" class="verify"></div>
					<div id="verify3" class="verify"></div>
					<div id="verify4" class="verify verify_last"></div>
					<div id="delete"></div>	
					<div class="cls"></div>				
				</div>
			</div>
			<p id="notice">点击框内文字输入上图中汉字</p>
			<div id="buttons">
			<for start="0" end="3" name="i">
				<br />
				<for start="0" end="4" name="j">
					<div class="button" x="x{$j+1}" y="line{$i+1}_y"></div>
				</for>
			</for>
			</div>
			<img id="verify_pic" src=\'\' style="cursor: pointer;" alt="">	
			<span class="hid" id="verify_url">{:U(\'Home/PostBarVerify/verify\',\'\',\'\')}</span>
		</form>
	</div>
</body>
<script>
	$(function(){

		// 加载或刷新时清空session计数
		$count = 0;
		clear_count();

		$xy = {
			"line1_y" : -112, // 备选验证码第一行y坐标
			"line2_y" : -165, // 备选验证码第二行y坐标
			"line3_y" : -212, // 备选验证码第三行y坐标
			"x1" : -35,  // 备选验证码每行第一个字x坐标
			"x2" : -114, // 备选验证码每行第二个字x坐标
			"x3" : -195, // 备选验证码每行第三个字x坐标
			"x4" : -276  // 备选验证码每行第四个字x坐标
		};

		$verify_url = $("#verify_url").html(); // 验证码请求地址
		$src = $verify_url + \'?\' + Math.random();
		change_verify($src); // 载入页面时加载验证码
	
		$("#verify_pic").click(function(){ // 点击验证码时更换验证码
			change_verify($src);
			// 清除验证码
			$(".verify").css(\'background-image\', \'\');
			clear_count($count);
		});

		function clear_count() {
			// 清空session计数
			$.ajax({
				url: \'{:U("/Home/PostBarVerify/count_ckick")}\',
				type: \'post\',
				data: {
					"clear": 1
				},
				success: function(data) {
					console.log(data);
				}
			});	
		}

		// 更换验证码
		function change_verify($src) { 

			var flag = 0;
			// 请求验证码地址生成验证码图片
			$.ajax({
				url: $src,
				async: false,			
				success: function() {
					flag = 1;
				}
			});

			if(flag == 1) {
				$(\'#verify_pic\').attr(\'src\', $src); // 验证码图片图片
			}		
		}

		// 点击备选区选择验证码	
		$(\'.button\').click(function() {

			// 查询session中保存的count
			$.ajax({
				url: \'{:U("/Home/PostBarVerify/record_click")}\',
				async: false,
				success: function(data) {
					$count = data;
				}
			});

			$count++;
			if($count > 4) {
				// return false;
			}

			$.ajax({ // 记录点击次数以及点击的文字
				url: \'{:U("/Home/PostBarVerify/count_ckick")}\',
				type: \'post\',
				data: {
					"count": $count,
					"x": $(this).attr("x"),
					"y": $(this).attr("y")
				},
				success: function(data) {
					if(data != \'\') {
						console.log(data);
					}
				}
			});

			$x = $(this).attr(\'x\');
			$y = $(this).attr(\'y\');
							
			choose_verify($xy, $x, $y, $count);
		});
		
		// 生成每次点击验证码的坐标,填充到验证码
		function choose_verify($xy, $x, $y) {

			$x = $xy[$x] + \'px\';
			$y = $xy[$y] + \'px\';

			// 填充验证码
			$(\'#verify\'+$count).css({
				\'background-position-x\': $x,
				\'background-position-y\': $y
			}).addClass(\'addbg\');
		}

		// 删除验证码
		$(\'#delete\').click(function(){

			$.ajax({
				url: \'{:U("Home/PostBarVerify/record_click")}\',
				type: \'post\',
				async: false,
				success: function(data) {
					if(data == 0) {data = 1;}
					$i = data;
				}
			});

			$(\'#verify\'+$i).removeClass(\'addbg\');console.log(\'#verify\'+$i);
			$i--;
			// 修改计数,清除session
			$.ajax({
				url: \'{:U("Home/PostBarVerify/update_click")}\',
				type: \'post\',
				data: {
					\'times\': -1
				},
				success: function(data) {
					console.log(data);
				}
			});
			$count--;
		});		
	});
</script>
</html>

  

演示

初始状态:

 

输入验证码:

 

输入完成,返回结果,错误时:

 

输入完成,返回结果,正确时:

 

删除验证码:

 

刷新验证码:

 

参考:

PHP生成不重复随机数的方法汇总

CSS透明opacity和IE各版本透明度滤镜filter的最准确用法

以上是关于PHP 实现“贴吧神兽”验证码的主要内容,如果未能解决你的问题,请参考以下文章

我的php代码中登陆界面加一个验证码,如何实现

代码实现PHP生成各种随机验证码

PHP实现随机生成验证码功能

php实现验证码(数字字母汉字)

PHP算式验证码和汉字验证码的实现方法

php教程 CURL实现带有验证码网站的模拟登录的方法