【源码分析】极验验证官方SDK源码分析和实现思路

前言

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";
    }
}

就这样,完成了下图的流程

【源码分析】极验验证官方SDK源码分析和实现思路

项目核心

  • 前端展示机制,通过js动态生成拖拽页面元素进行本地拖拽展示
  • 后端检验,要求和前端的算法进行解密,匹配是否验证准确

不足

  • 本地验证只是能进行本地校验结果,如果没有对官方服务器的通信,还是不能展示出验证码的,同时也将网站的所有验证信息暴露给了极验官方

引用资料

上一篇:《HBase企业应用开发实战》—— 3.4 数据模型的特殊属性


下一篇:《ELK Stack权威指南(第2版)》一1.3 配置语法