前言
是这次要说的极验验证
,在这个万众创新的时代,工具类产品能做到这样,也是很不错的~
源码来源
来自于官网提供的PHP SDK
https://github.com/GeeTeam/gt-php-sdk
官方在http://www.geetest.com/install/sections/idx-basic-introduction.html 页面对整个通讯流程进行了简要说明,所以我这次的侧重点则是实现部分
本次下载的提交版本为 commit fd4b1d8cc6aa30f9c2dc5671ebfddc18e39e892e
源码分析
demo页面
基本的展示界面,文件位置 /static/login.html
下面是截取的验证码对于div和内嵌的js代码
<div class="box" id="div_geetest_lib"> <div id="div_id_embed"></div> <script type="text/javascript"> //这里就是宕机回滚机制 var gtFailbackFrontInitial = function(result) { //动态创建出js引用 var s = document.createElement('script'); s.id = 'gt_lib'; //设置id s.src = 'http://static.geetest.com/static/js/geetest.0.0.0.js'; //设置路径,这里引用的是官网下的js s.charset = 'UTF-8'; //utf8 s.type = 'text/javascript'; //设置type document.getElementsByTagName('head')[0].appendChild(s); //把引用的js放到header中 //初始化loaded变量名称,用来标记是不是已经下载了 var loaded = false; //当页面加载状态改变,或者,页面或图像加载完成,执行下面的匿名函数 s.onload = s.onreadystatechange = function() { console.log(this); //判断当前的状态 if (!loaded && (!this.readyState|| this.readyState === 'loaded' || this.readyState === 'complete')) { //如果没有载入完成,就执行下面的方式进行载入 loadGeetest(result); loaded = true;//执行之后,把loaded的状态变成已经读取 } }; } //get geetest server status, use the failback solution //执行载入操作 var loadGeetest = function(config) { //1. use geetest capthca window.gt_captcha_obj = new window.Geetest({ //载入对应的配置】 gt : config.gt, //3386e03c620a4067f18fa92c370f1594 challenge : config.challenge, //f1ccacfa56ca8085a59fd493cd4305aa product : 'embed', offline : !config.success //表示是不是离线模式 }); //创建对象,验证码放到div中 gt_captcha_obj.appendTo("#div_id_embed"); } //创建一个引入js的对象 s = document.createElement('script'); s.src = 'http://api.geetest.com/get.php?callback=gtcallback'; $("#div_geetest_lib").append(s); //放到验证码div的内容 //变量赋值给匿名函数 var gtcallback =( function() { var status = 0, result, apiFail; //返回一个匿名函数 return function(r) { status += 1; //状态+1 ,外层定义变量,供给内部反复赋值使用 if (r) { // r Object {success: 1, gt: "3386e03c620a4067f18fa92c370f1594", challenge: "f1ccacfa56ca8085a59fd493cd4305aa"} //如果返回的结果失败.下面进行一秒后的再次重试 result = r; setTimeout(function() { if (!window.Geetest) { apiFail = true; gtFailbackFrontInitial(result) } }, 1000) } else if(apiFail) { return } //如果成功 , 也就是执行两次 // 当前返回函数 if (status == 2) { //载入页面 loadGeetest(result); } } })() //ajax访问本地连接库,返回供页面展示的参数 $.ajax({ url : "../web/StartCaptchaServlet.php?rand="+Math.round(Math.random()*100), type : "get", dataType : 'JSON', success : function(result) { // console.log(result); gtcallback(result) } }) </script> </div>
js思路比较简单
- 引入js
- ajax获取展示验证码的对象参数
- 如果没有载入完成就再次载入
- 通过ajax返回的参数,再用js创建验证码对象
- 展示在页面上
ajax本地库
url : “…/web/StartCaptchaServlet.php?rand=”+Math.round(Math.random()*100)
这里对应的地址,是ajax本地的地址,后面接了一个随机的地址
文件位置 /web/StartCaptchaServlet.php
/** * 使用Get的方式返回:challenge和capthca_id 此方式以实现前后端完全分离的开发模式 专门实现failback * @author Tanxu */ error_reporting(0); //吐槽一下,还没有使用命名空间,并且放在项目的vendor中的配置文件还需要修改而不是单拿出来,真像一个没有完成的sdk require_once dirname(dirname(__FILE__)) . '/lib/class.geetestlib.php'; $GtSdk = new GeetestLib(); session_start(); $return = $GtSdk->register(); //返回的结果是0或者1,session里面也没有保存其他结果 if ($return) { $_SESSION['gtserver'] = 1; $result = array( 'success' => 1, 'gt' => CAPTCHA_ID, 'challenge' => $GtSdk->challenge //所以返回展示的界面都在这个参数内部 ); echo json_encode($result); }else{ $_SESSION['gtserver'] = 0; $rnd1 = md5(rand(0,100)); $rnd2 = md5(rand(0,100)); $challenge = $rnd1 . substr($rnd2,0,2); $result = array( 'success' => 0, 'gt' => CAPTCHA_ID, 'challenge' => $challenge ); $_SESSION['challenge'] = $result['challenge']; echo json_encode($result); }
和服务器通讯的类
js进行ajax,到最后和服务器进行通讯的类
文件位置 lib/class.geetestlib.php
从文件名和引用配置文件来看,做SDK并没有考虑到命名空间 = =
/** * 极验行为式验证安全平台,php 网站主后台包含的库文件 *@author Tanxu */ //引入配置文件 require_once dirname(dirname(__FILE__)) . '/config/config.php'; class GeetestLib{ const GT_SDK_VERSION = 'php_2.15.7.6.1'; //初始化返回值 public function __construct() { $this->challenge = ""; } /** *判断极验服务器是否down机 * * @return */ public function register() { $url = "http://api.geetest.com/register.php?gt=" . CAPTCHA_ID; $this->challenge = $this->send_request($url); //判断返回值是不是32位,来界定是不是服务器能用 if (strlen($this->challenge) != 32) { return 0; } return 1; } //进行验证 public function validate($challenge, $validate, $seccode) { if ( ! $this->check_validate($challenge, $validate)) { return FALSE; } $data = array( "seccode"=>$seccode, "sdk"=>self::GT_SDK_VERSION, ); $url = "http://api.geetest.com/validate.php"; $codevalidate = $this->post_request($url, $data); if (strlen($codevalidate) > 0 && $codevalidate == md5($seccode)) { return TRUE; } else if ($codevalidate == "false"){ return FALSE; } else { return $codevalidate; } } private function check_validate($challenge, $validate) { if (strlen($validate) != 32) { return FALSE; } if (md5(PRIVATE_KEY.'geetest'.$challenge) != $validate) { return FALSE; } return TRUE; } //通过curl和远程服务器进行通信 private function send_request($url){ if(function_exists('curl_exec')){ $ch = curl_init(); curl_setopt ($ch, CURLOPT_URL, $url); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($ch); curl_close($ch); }else{ $opts = array( 'http'=>array( 'method'=>"GET", 'timeout'=>2, ) ); $context = stream_context_create($opts); $data = file_get_contents($url, false, $context); } return $data; } /** *解码随机参数 * * @param $challenge * @param $string * @return */ private function decode_response($challenge,$string) { if (strlen($string) > 100) { return 0; } $key = array(); $chongfu = array(); //重复 = = $shuzi = array("0"=>1,"1"=>2,"2"=>5,"3"=>10,"4"=>50); //数字 = = $count = 0; $res = 0; $array_challenge = str_split($challenge); $array_value = str_split($string); for ($i=0; $i < strlen($challenge); $i++) { $item = $array_challenge[$i]; if (in_array($item, $chongfu)) { continue; }else{ $value = $shuzi[$count % 5]; array_push($chongfu, $item); $count++; $key[$item] = $value; } } for ($j=0; $j < strlen($string); $j++) { $res += $key[$array_value[$j]]; } $res = $res - $this->decodeRandBase($challenge); return $res; } /** * * @param $x_str * @return */ private function get_x_pos_from_str($x_str) { if (strlen($x_str) != 5) { return 0; } $sum_val = 0; $x_pos_sup = 200; $sum_val = base_convert($x_str,16,10); $result = $sum_val % $x_pos_sup; $result = ($result < 40) ? 40 : $result; return $result; } /** * * @param full_bg_index * @param img_grp_index * @return */ private function get_failback_pic_ans($full_bg_index,$img_grp_index) { $full_bg_name = substr(md5($full_bg_index),0,9); $bg_name = substr(md5($img_grp_index),10,9); $answer_decode = ""; // 通过两个字符串奇数和偶数位拼接产生答案位 for ($i=0; $i < 9; $i++) { if ($i % 2 == 0) { $answer_decode = $answer_decode . $full_bg_name[$i]; }elseif ($i % 2 == 1) { $answer_decode = $answer_decode . $bg_name[$i]; } } $x_decode = substr($answer_decode, 4 , 5); $x_pos = $this->get_x_pos_from_str($x_decode); return $x_pos; } /** * 输入的两位的随机数字,解码出偏移量 * * @param challenge * @return */ private function decodeRandBase($challenge) { $base = substr($challenge, 32, 2); $tempArray = array(); for ($i=0; $i < strlen($base); $i++) { $tempAscii = ord($base[$i]); $result = ($tempAscii > 57) ? ($tempAscii - 87) : ($tempAscii -48); array_push($tempArray,$result); } $decodeRes = $tempArray['0'] * 36 + $tempArray['1']; return $decodeRes; } /** * 得到答案 * * @param validate * @return */ public function get_answer($validate) { if ($validate) { $value = explode("_",$validate); $challenge = $_SESSION['challenge']; $ans = $this->decode_response($challenge,$value['0']); $bg_idx = $this->decode_response($challenge,$value['1']); $grp_idx = $this->decode_response($challenge,$value['2']); $x_pos = $this->get_failback_pic_ans($bg_idx ,$grp_idx); $answer = abs($ans - $x_pos); if ($answer < 4) { return 1; }else{ return 0; } }else{ return 0; } } public function post_request($url, $postdata = null){ $data = http_build_query($postdata); if(function_exists('curl_exec')){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); if(!$postdata){ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); }else{ curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); } $data = curl_exec($ch); curl_close($ch); }else{ if($postdata){ $url = $url.'?'.$data; $opts = array( 'http' => array( 'method' => 'POST', 'header'=> "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($data) . "\r\n", 'content' => $data ) ); $context = stream_context_create($opts); $data = file_get_contents($url, false, $context); } } return $data; } }
直接访问地址 "http://api.geetest.com/register.php?gt=" . CAPTCHA_ID
,可以得到每次不一样的32位字符串,所以这个加密字符串就是每次验证码显示的内容,经过js解析之后,进行展示
验证
每次展示验证码,都会从服务器获取验证码对于的参数,经过动态加载的js文件,展示出对应的验证码。
对应的验证操作,它会根据表单的提交方式,提交用户滑动后的结果,从服务器端进行校验。
表单提交的结果
[geetest_challenge] => aec462f7abc1edf69048b1057c5d2ac7l7 [geetest_validate] => 2abebf70f08b839e3037f6417459a65f [geetest_seccode] => 2abebf70f08b839e3037f6417459a65f|jordan
再通过后台进行校验
服务器校验
本地服务器进行对用户拖拽验证码进行校验。
文件对应位置 /web/VerifyLoginServlet.php
/** * 本文件示例只是简单的输出 Yes or No */ // error_reporting(0); require_once dirname(dirname(__FILE__)) . '/lib/class.geetestlib.php'; //通过session进行判断,是不是需要采用本地算法校验 session_start(); $GtSdk = new GeetestLib(); if ($_SESSION['gtserver'] == 1) { //在线判断,传递参数过去,返回拖拽是否成功的结果 $result = $GtSdk->validate($_POST['geetest_challenge'], $_POST['geetest_validate'], $_POST['geetest_seccode']); if ($result == TRUE) { echo 'Yes!'; } else if ($result == FALSE) { echo 'No'; } else { echo 'FORBIDDEN'; } }else{ //本地进行检验,使用类库内部的算法进行匹配,返回结果 if ($GtSdk->get_answer($_POST['geetest_validate'])) { echo "yes"; }else{ echo "no"; } }
就这样,完成了下图的流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plLks82O-1671803129579)(http://www.geetest.com/install/_images/formal-flow-20150703161215.png)]
项目核心
- 前端展示机制,通过js动态生成拖拽页面元素进行本地拖拽展示
- 后端检验,要求和前端的算法进行解密,匹配是否验证准确
不足
- 本地验证只是能进行本地校验结果,如果没有对官方服务器的通信,还是不能展示出验证码的,同时也将网站的所有验证信息暴露给了极验官方