开发者社区> 小雨雨hi> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

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

简介: 前言 2016年就这么来了,新的一年,继续努力~ 最近,除了12306的验证码火起来以后,还有一个在界面上拖拽的验证码,也火了起来,就是这次要说的极验验证,在这个万众创新的时代,工具类产品能做到这样,也是很不错的~ 源码来源 来自于官网提供的PHP SDK https://github.
+关注继续查看

前言

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动态生成拖拽页面元素进行本地拖拽展示
  • 后端检验,要求和前端的算法进行解密,匹配是否验证准确

不足

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

引用资料

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
用户指南—账号和安全—三权分立—权限验证
开启三权分立模式后,高权限账号自动转换为系统管理员账号,同时还需创建安全管理员账号和审计管理员账号,三个账号拥有不同的权限,当您对数据库执行不同操作时,需要对应的管理员进行权限验证。本文将介绍权限验证的相关操作步骤。
39 0
Solrflux源码分析-Sql Support within Solr-类Sql的solr搜索实现(2)
假期重新把之前在新浪博客里面的文字梳理了下,搬到这里。
34 0
Python 技术篇 - python镜像推荐,实现python最新官方安装包飞速下载,解决www.python.org官网无法访问、下载安装包慢问题
Python 技术篇 - python镜像推荐,实现python最新官方安装包飞速下载,解决www.python.org官网无法访问、下载安装包慢问题
217 0
从源码中编译安装gmt官方开发版(优先体验最新功能)
一步一步解释在ubuntu系统下如何从github源码仓库中编译安装gmt (generic mapping tools),文中有详细的编译安装命令,同时也配有亲自操作的演示视频!从源码安装,提前体验gmt最新功能并修改和添加自己的功能!从这里可以体验gmt组新版gmt 6.
1504 0
JNI源码分析 (并实现JNI动态注册)
本博客转载自网址:http://blog.csdn.net/urrjdg/article/details/78091094 1. C/C++ 的 编译 和 链接 c/c++ ========= 二进制文件 对于C/C++ 一般分为两个阶段 编译 xxx.
1014 0
C控制台实现模拟平抛运动算法
平抛运动这个相信读了高中物理都知道这个概念了,详细的我就不说了,不明白的看看百度: 平抛运动 接下来看看用控制台实现的平抛运动算法: #include #include #include #include #include #include int main(void) { float v0 = 0.
875 0
验证码程序Demo
       小伙伴都有这样的经历,册各种网站,总是输不对验证码,双十一那天狂买的小伙伴是不是对输入验证码有着不一样的感触呢,以前觉得验证码真是个麻烦鬼,一个不小心,一个眼拙,哎呦,没有输入正确,又是一阵子大眼瞪小眼,这个时候,突然想起做软件的不都是...
1099 0
iOS新手引导页的实现,源码。
1 /*1.在Main.storyboard中找到,ScrollView和PageControl并添加到ViewController中。 2 2.在ScrollView中添加ImageView,新手引导页有几个图片就添加几个,然后设置ImageView的image,就是准备好的图片。
996 0
实现一个简单的视频聊天室(源码)
在 《实现一个简单的语音聊天室》一文发布后,很多朋友建议我也实现一个视频聊天室给他们参考一下,其实,视频聊天室与语音聊天室的原理是差不多的,由于加入了摄像头、视频的处理,逻辑会繁杂一些,本文就实现一个简单的多人视频聊天系统,让多个人可以进入同一个房间进行语音视频沟通。
1209 0
+关注
小雨雨hi
CSDN学院讲师、博客专家,专注服务端开发,服务端架构演变,区块链技术研究,项目管理,热衷学习前沿技术,以及日常的技术分享,曾经历过创业技术合伙人角色。
277
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载