前言
2016年就这么来了,新的一年,继续努力~
最近,除了12306的验证码火起来以后,还有一个在界面上拖拽的验证码,也火了起来,就是这次要说的极验验证
,在这个万众创新的时代,工具类产品能做到这样,也是很不错的~
源码来源
来自于官网提供的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";
}
}
就这样,完成了下图的流程
项目核心
- 前端展示机制,通过js动态生成拖拽页面元素进行本地拖拽展示
- 后端检验,要求和前端的算法进行解密,匹配是否验证准确
不足
- 本地验证只是能进行本地校验结果,如果没有对官方服务器的通信,还是不能展示出验证码的,同时也将网站的所有验证信息暴露给了极验官方