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

简介: 极验验证官方SDK源码分析和实现思路

前言

是这次要说的极验验证,在这个万众创新的时代,工具类产品能做到这样,也是很不错的~

源码来源

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

不足

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

引用资料

相关文章
|
6月前
|
开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(三)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(三)
88 0
|
3月前
|
存储 Java API
【Azure 存储服务】Java Storage SDK 调用 uploadWithResponse 代码示例(询问ChatGTP得代码原型后人力验证)
【Azure 存储服务】Java Storage SDK 调用 uploadWithResponse 代码示例(询问ChatGTP得代码原型后人力验证)
|
4月前
|
PHP 开发工具
tp5+微信公众号服务器配置时使用官方sdk还是token验证失败
tp5+微信公众号服务器配置时使用官方sdk还是token验证失败
36 0
|
6月前
|
编解码 IDE 开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(一)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(一)
50 1
|
6月前
|
开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(二)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(二)
66 0
|
物联网 开发工具 开发者
NRF52832官方SDK介绍
SDK(Software Development Kit)是指软件开发包,一般是由官方提供,主要用来介绍开发流程,提供常用的例程代码等等。可以降低开发者使用的门槛,提高开发效率,所以在开发者在进行具体的开发前必须先熟悉官方的SDK,一般可以在对应的官网获取(有些网站是开放的,可以直接下载,有些网站需要申请)。
276 0
|
传感器 监控 JavaScript
NodeJS & Dapr Javascript SDK 官方使用指南
用于在 JavaScript 和 TypeScript 中构建 Dapr 应用程序的客户端库。该客户端抽象了公共 Dapr API,例如服务到服务调用、状态管理、发布/订阅、机密等,并为构建应用程序提供了一个简单、直观的 API。
384 0
NodeJS & Dapr Javascript SDK 官方使用指南
|
3月前
|
JavaScript 前端开发 Java
[Android][Framework]系统jar包,sdk的制作及引用
[Android][Framework]系统jar包,sdk的制作及引用
65 0
|
6天前
|
程序员 开发工具 Android开发
Android|使用阿里云推流 SDK 实现双路推流不同画面
本文记录了一种使用没有原生支持多路推流的阿里云推流 Android SDK,实现同时推送两路不同画面的流的方法。
24 7
|
3月前
|
开发工具 Android开发
解决Android运行出现NDK at /Library/Android/sdk/ndk-bundle did not have a source.properties file
解决Android运行出现NDK at /Library/Android/sdk/ndk-bundle did not have a source.properties file
156 4
解决Android运行出现NDK at /Library/Android/sdk/ndk-bundle did not have a source.properties file