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的最准确用法

posted @ 2016-06-04 19:48  nemo20  阅读(1102)  评论(0编辑  收藏  举报
访客数:AmazingCounters.com
2016/05/17 起统计