美文网首页程序员@IT·互联网
验证码WEB端产品调研(二):极限验证

验证码WEB端产品调研(二):极限验证

作者: 弗拉明哥 | 来源:发表于2017-07-07 02:36 被阅读0次

    本文是验证码系列的第二篇。笔者的写作顺序基于心中排名,所以这次为大家带来的是极限验证——GeeTest。

    写到这里可能会有人质疑:难道阿里、腾讯、网易(以下简称ATN)的名气和实力不是比极验强吗?不可否认,但我们讨论的只是验证码:

    • 逻辑上,知名度高,综合实力强,不代表垂直领域实力也一定强——当然,并不否认ATN具备这样的人才和实力。这应该很好理解。玩吉他的人都知道YAMAHA,但YAMAHA的吉他却不见得是最好的——大师可能更喜欢Martin or Taylor,或者纯私人订制。换句话说:如果你有一把YAMAHA FG730,发朋友圈会有很多人点赞竖大拇指,但是也有人会给你一个手动微笑。

    • 市场上,极验在验证码领域已深耕多年,合作伙伴从直播、金融、电商,到资讯、游戏、航旅,甚至...政府,且号称有16w家企业正在使用。实力和人脉,可见一斑;而ATN的验证码服务只是其云平台下的一个分支,官网均未专门介绍具体合作方和数量。笔者虽没有详实的数据证明ATN接入方比极验少,但从个人上网经历来看,极验显然更“面熟”。起码简书都在用了。具体极验官网可查询,一张图感受下:

    优势往往就是这样建立并扩大的:当你和一个行业的Top 3合作愉快的时候

    遗憾的是,极验并没有给笔者任何软文费。鉴于此便不再继续吹了。下面还是从产品和技术层面聊一聊。

    产品背景

    极验目前的产品已经升级为3.0:


    其实这三代的产品,目前是共存的势态:

    存在即合理

    1.0

    1.0代表的是键盘输入型字符验证码,开发成本低,常用开源框架即可搞定(如Kaptcha 、JCaptcha)。其设计思路就一点:
    如何让生成的问题人可以解答,而机器不可以


    因此传统验证码势必要在让机器感到压力山大的事情上做文章。但纯图片上做的文章,总是可以一物降一物:
    左侧是验证码常用技术,右侧是对应的破解方法
    当网站本身被频繁突破意义不大时,自然不用考虑这些对抗上的“短兵相接”。但就此类验证码而言,最明显的问题是:
    • 每次都要敲键盘输入
    • 为了防范图像识别破解,便增大识别难度,导致有时候人都难以识别

    更有甚者直接利用“人类(大学生或许更准确)目前在认知学上对机器的优势“,在图片上直接祭出了高数,将反人类做到极致:


    当然,上图可能只是一个段子。总之极验官网已经号称对此没有兴趣:字符验证码不在服务范围。而这也是大势所趋:验证码本身没有任何商业价值,为了阻挡机器会自带反人类属性。因此设计时一定要同时兼顾安全和用户体验。对于一些电商类的网站,用户体验甚至是摆在首位的。

    2.0

    2.0产品,极验称其为”行为式验证“。代表作是滑动拼图验证码。目前也是各大验证码平台的标配:


    为什么官网2.0的配图不是点选验证码?点选同样是标配,极验也有这款产品:

    笔者认为,主要原因是:
    1)体验不如滑动——点击虽然比输入更便捷,但识别文字依然要花大量时间,尤其是当备选汉字远多于待验证汉字。
    2)安全性不输于滑动(如果设计足够好),但其原理并非潮流所向——想要破解,首先要能抠出图片中的汉字,保证所有的汉字都被识别,且能够顺序和待验证汉字对上。这里穷举法(随便点三个)是没有用的,或者说成功概率很小(1/P(n,m))。因为只要错一个字就会刷新。可以看出,其校验原理更侧重前端展示,并不过分依赖行为分析。既让用户可见,又要在可见基础上安全,就势必带来体验的下降,这还是和字符验证码有殊途同归的意思。
    所以2.0强调行为,也是基于思路的转换:弱化前端展示,强化后台分析,在不可见之处强化安全。
    对滑动拼图而言,滑到正确位置只是一个必要条件,用户行为特征的提取才是核心。通过采集并分析用户使用鼠标拖动滑块的行为特征(速度、频率、耗时等)来判定是人还是机器。对用户来说,体验上比输入或点选字符要更轻松愉快,还兼具一定趣味性。但是便捷性往往和安全性相冲突:理论上只要机器模拟出人的行为,就可以绕过验证(有人可能会第一时间联想到按键精灵)。其实是否轻松,就看对行为判定是否精准了。这类判定,可以基于一些固定的规则(如滑动时间小于某个阈值),也可以引入机器学习分析特征量(如Kmeans做聚类分析),看各家的思路和实力了。未来这些偏后端的技术才是卖点,也不会轻易开源。因此此类验证码更多还是各类验证码平台提供,接入是有偿的。
    知乎上有一位叫@darbra的兄弟,对破解极验2.0产品饶有兴趣。他总结出了selenium大法 和 requests基本法,号称破率分别是98%和80%。其Github上的代码7.4还有更新。有兴趣的读者可以尝试。

    3.0

    3.0时代则伴随着人工智能的浪潮全面进化,强调的则是进一步弱化前端+强化机器学习。前端越简单越好,一个按钮,一个滑块,一个复选框即可,甚至...nothing。个人认为,目前只有三家公司在这方面引领市场:Google,极验和阿里。

    1)Google第一篇已经画了大量篇幅介绍,已经进化到“化无形为有形”的invisible reCAPTCHA,验证码实体完全透明,是目前来说产品理念上最领先的(技术上就不谈了)。
    2)阿里和极验是第二档,区别在于一个是滑块,一个是按钮。相当于Google的noCAPTCHA产品。



    极验的3.0产品思想,其实是官网提到的“验证码形如按钮,更像按钮一样百搭”——也就是说这个按钮会一直存在,只不过普通合法用户,需要多点一下它来进行人机识别罢了。只有非法用户时才会出现滑动拼图或图文点选验证。
    如果极验有4.0,笔者大胆推测方向是invisible。
    技术层面则强调使用了人工智能——其实各公司对应的2.0产品,应该都用到了。按照极验官方的说法:
    恶意程序模仿人类行为轨迹对验证码进行破解针对模拟,极验拥有超过4000万人机行为样本的海量数据利用机器学习和神经网络构建线上线下的多重静态、动态防御模型识别模拟轨迹,界定人机边界
    极验很可能是加强了人工智能的投入:例如新增更多特征量,优化识别算法等。毕竟机器学习还是一个数据为王的技术,没有足够多的训练样本是无法做到足够精准的。所以以极验目前的市场份额,是有这个实力优化的。但是加强了多少,效果如何,目前并没有太好的印证方式,毕竟今年4月才上线。看后期市场的反馈如何吧。

    接入方式

    同Google一样,极验的接入方式也如其UI一样:清爽简洁
    按官网的说法:

    1. 引入初始化函数
    <script src="gt.js"></script>
    
    1. 调用初始化函数进行初始化
    initGeetest({
        // 以下配置参数来自服务端 SDK
        gt: data.gt,
        challenge: data.challenge,
        offline: !data.success,
        new_captcha: data.new_captcha
    }, function (captchaObj) {
        // 这里可以调用验证实例 captchaObj 的实例方法
    })
    

    结合配置参数:


    看似非常多,其实只有前面四个是必须,后面都是定制,按默认来均可不填。这样的参数配置,作为一家专业验证码公司,还是十分合理的——既要考虑到宕机问题,又得支持顾客的个性化需求。
    参数含义,文档已经介绍的十分详细,Markdown排版阅读起来也很舒适,笔者便不再赘言。值得一提的是,第二个参数challenge,在前面介绍的Google验证码参数也出现过,且含义基本一样。可能一半借鉴,一半致敬吧。

    源码分析

    细心的朋友会发现,步骤1中加载的是一个本地路径的gt.js,而非从极验服务器加载。按一般验证码公司的做法,给出一个http://static.geetest.com/static/tools/gt.js 就够了。极验则考虑到了failover层面——如果自己服务器故障怎么办?在目前机房容灾能力越来越强的今天,这么考虑仍然是一种非常专业和负责的做法。online offline皆可进行验证,这恰恰也是极验有别于其他验证码的特点之一。

    下面还是回到gt.js——最近喜欢看源码。虽然笔者前端经验并不丰富,但是觉得有趣的东西,还是忍不住分享:

    gt.js的主要功能是定义初始化验证码的函数initGeetest

    最开始的地方如下:

    "v0.4.6 Geetest Inc.";
    
    (function (window) {
        "use strict";
        if (typeof window === 'undefined') {
            throw new Error('Geetest requires browser environment');
        }
    
    ……
    })(window);
    
    

    "v0.4.6 Geetest Inc."行首直接字符串秀出版本号和公司,一目了然。
    这种重点要说的是"use strict"

    严以律己,宽以待人

    "use strict",严格模式,是Javascript非常好的一个特性,可能会大大节约你查错的时间。
    先举个例子:

    var zombie = {
        eyeLeft : 0,
        eyeRight: 1,
        // ... a lot of keys ...
        eyeLeft : 1
    }
    mingo= 5;
    
    1. eyeLeft出现了两次,你打算用哪个?
    2. mingo会是一个全局变量,如果被用于各种嵌套循环中会怎样?

    这段代码严格模式下会抛出两个错误,而非严格模式不会报。
    它以严格换来了如下好处,主要是以下两点:

    • 消除语法的一些不严谨之处,减少怪异行为
    • 消除代码运行的一些不安全之处
      最初网景在开发JavaScript可能存在一些考虑不周的情况,一些模棱两可的特性被人诟病,新手可能分不清是语法糖还是语法坑。
      user stick便是语言成熟化的一个方向。毕竟JavaScript将来会是一种全栈语言,尤其是在服务端Node.js逐渐强势的今天。

    好像扯远了。其实笔者想说的是极验代码写的非常严谨。有一个细节:

    var loadScript = function (url, cb) {
        var script = document.createElement("script");
        script.charset = "UTF-8";
        script.async = true;
    
        script.onerror = function () {
            cb(true);
        };
        var loaded = false;
        script.onload = script.onreadystatechange = function () {
            if (!loaded &&
                (!script.readyState ||
                "loaded" === script.readyState ||
                "complete" === script.readyState)) {
    
                loaded = true;
                setTimeout(function () {
                    cb(false);
                }, 0);
            }
        };
        script.src = url;
        head.appendChild(script);
    };
    

    不知该写法应该是参考了JQuery如下源码:

    callback = errorCallback = xhr.onload =
                                    xhr.onerror = xhr.onabort = xhr.onreadystatechange = null
    

    这么写主要还是因为:
    IE的 script 元素支持onreadystatechange事件,不支持onload事件。FF则正好相反
    如果要在一个<script src="x.js"> 加载完成执行一个操作,FF使用onload事件就行了,IE下则要结合onreadystatechange事件和this.readyState。所以:

    script.onload = script.onreadystatechange = function(){
    
         if(  ! this.readyState     //这是FF的判断语句,因为ff下没有readyState,IE的readyState肯定有值
              || this.readyState=='loaded' || this.readyState=='complete'   //这是IE的判断语句
        ){
              alert('loaded');
        }
    
    };
    

    此外还有一个细节:
    gt.js有一个尽量利用多CDN,使静态文件尽可能加载成功的机制:

    fallback_config: {
            slide: {
                static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"],
                type: 'slide',
                slide: '/static/js/geetest.0.0.0.js'
            },
            fullpage: {
                static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"],
                type: 'fullpage',
                fullpage: '/static/js/fullpage.0.0.0.js'
            }
        }
    

    其目的是加载滑动和点选的widget。在fullpage.js(已混淆)开头可见以下写法:

    !function (a, b) {
      'use strict';
      'object' == typeof module && 'object' == typeof module.exports ? module.exports = a.document ? b(a, !0)  : function (a) {
        if (!a.document) throw new Error('Geetest requires a window with a document');
        return b(a)
      }
       : b(a)
    }('undefined' != typeof window ? window : this, function (a, b) {
    ……
    }
    

    今天天气不错,挺风和日丽的

    这一段让笔者想起中学时写作的范文——JQuery就是这么玩的。JQuery向来以严谨和兼容性强大著称,究其根本是写法的包容性。大家可以对照看一下其的源码开头,以最新版本为例:

    /*!
     * jQuery JavaScript Library v3.2.1
     * https://jquery.com/
     *
     * Includes Sizzle.js
     * https://sizzlejs.com/
     *
     * Copyright JS Foundation and other contributors
     * Released under the MIT license
     * https://jquery.org/license
     *
     * Date: 2017-03-20T18:59Z
     */
    ( function( global, factory ) {
    
        "use strict";
    
        if ( typeof module === "object" && typeof module.exports === "object" ) {
    
            // For CommonJS and CommonJS-like environments where a proper `window`
            // is present, execute the factory and get jQuery.
            // For environments that do not have a `window` with a `document`
            // (such as Node.js), expose a factory as module.exports.
            // This accentuates the need for the creation of a real `window`.
            // e.g. var jQuery = require("jquery")(window);
            // See ticket #14549 for more info.
            module.exports = global.document ?
                factory( global, true ) :
                function( w ) {
                    if ( !w.document ) {
                        throw new Error( "jQuery requires a window with a document" );
                    }
                    return factory( w );
                };
        } else {
            factory( global );
        }
    
    // Pass this if window is not defined yet
    } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
    
    // Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
    // throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
    // arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
    // enough that all such attempts are guarded in a try block.
    "use strict";
    
    

    这里其实是一个闭包(Closures)。现今流行的一些 JS 库中经常见到以下形式的代码。在闭包中,可以定义私有变量和函数,外部无法访问它们,从而做到了私有成员的隐藏和隔离。而通过返回对象或函数,或是将某对象作为参数传入,在函数体内对该对象进行操作,就可以公开希望对外暴露的方法与数据。

    JQuery这一段的目的,注释已经写得非常清楚。其实就是为了保证不污染全局变量,将所有的对象及方法创建都放到了factory函数中执行。通过形参global来传递window变量,在利用factory创建jQuery对象以前,首先进行window变量的检测。
    module 和 module.exports主要是为了让jQuery能够以模块的形式(如CMD)注入到没有window.document变量的诸如Node.js的运行环境中,当遇到这种情况,就不会在window中设置jQuery$变量。要使用jQuery时,则是使用所返回的jQuery对象,如在Node.js中:
    var jQuery = require("jquery")(window);
    所以极限以"范文"开头,也是考虑了同样的事情。

    最后还是聊一聊UBC的部分(写累了,突然想装13)——笔者曾在阿里验证码里看到了一个类似的文件,所以对这个缩写印象深刻——其实就是User Behavior Collection,给到后端做人机识别用。极验的UBC在http://apiguard.geetest.com/dist/geeguard.js 这里。其实做的事情就是用户行为收集。
    老套路,先搜一波userAgent——为什么?因为做这件事情必须要浏览器指纹,否则无法锁定设备,也就缺少了一个关键风控维度。
    不出意料可以看到这样一段代码:

     '_getKeys': function () {
          return ['textLength',
          'HTMLLength',
          'documentMode'].concat(this._tagKeys).concat(['screenLeft',
          'screenTop',
          'screenAvailLeft',
          'screenAvailTop',
          'innerWidth',
          'innerHeight',
          'outerWidth',
          'outerHeight',
          'browserLanguage',
          'browserLanguages',
          'systemLanguage',
          'devicePixelRatio',
          'colorDepth',
          'userAgent',
          'cookieEnabled',
          'netEnabled',
          'screenWidth',
          'screenHeight',
          'screenAvailWidth',
          'screenAvailHeight',
          'localStorageEnabled',
          'sessionStorageEnabled',
          'indexedDBEnabled',
          'CPUClass',
          'platform',
          'doNotTrack',
          'timezone',
          'canvas2DFP',
          'canvas3DFP',
          'plugins',
          'maxTouchPoints',
          'flashEnabled',
          'javaEnabled',
          'hardwareConcurrency',
          'jsFonts',
          'timestamp',
          'performanceTiming'])
        },
    

    其实这段代码就是基于多维度给浏览器采集一个指纹。Github上有一款开源Js叫
    fingerprintjs,目前已经出了第二版,定位就是Modern & flexible browser fingerprinting library,功能是一样的,如下:

        get: function(done){
          var keys = [];
          keys = this.userAgentKey(keys);
          keys = this.languageKey(keys);
          keys = this.colorDepthKey(keys);
          keys = this.pixelRatioKey(keys);
          keys = this.hardwareConcurrencyKey(keys);
          keys = this.screenResolutionKey(keys);
          keys = this.availableScreenResolutionKey(keys);
          keys = this.timezoneOffsetKey(keys);
          keys = this.sessionStorageKey(keys);
          keys = this.localStorageKey(keys);
          keys = this.indexedDbKey(keys);
          keys = this.addBehaviorKey(keys);
          keys = this.openDatabaseKey(keys);
          keys = this.cpuClassKey(keys);
          keys = this.platformKey(keys);
          keys = this.doNotTrackKey(keys);
          keys = this.pluginsKey(keys);
          keys = this.canvasKey(keys);
          keys = this.webglKey(keys);
          keys = this.adBlockKey(keys);
          keys = this.hasLiedLanguagesKey(keys);
          keys = this.hasLiedResolutionKey(keys);
          keys = this.hasLiedOsKey(keys);
          keys = this.hasLiedBrowserKey(keys);
          keys = this.touchSupportKey(keys);
          keys = this.customEntropyFunction(keys);
          var that = this;
          this.fontsKey(keys, function(newKeys){
            var values = [];
            that.each(newKeys, function(pair) {
              var value = pair.value;
              if (typeof pair.value.join !== "undefined") {
                value = pair.value.join(";");
              }
              values.push(value);
            });
            var murmur = that.x64hash128(values.join("~~~"), 31);
            return done(murmur, newKeys);
          });
    }
    

    有兴趣的读者可以试着了解。

    总结

    个人观点:极验是国内目前最好的验证码产品——不论是产品设计、文档风格、还是代码专业性。这么说是因为,其他平台,鲜有这几方面都不错的。例如阿里最大的问题就是接入配置较为复杂(接入页面插入多段代码);网易则是点选验证码图文结合过于丑陋,无视审美;点触文档排版不佳...诸如此类。
    另外一个好感的原因,可能是笔者坐标武汉,明白在武汉做互联网之不易。不管是政府扶持力度,还是人才凝聚力,都和一线城市有差距。

    希望极验越做越好。

    后续不打算再介绍其他平台的验证码。一来都大同小异,其他平台亮点不多,反复论述意义不大;二来自身疲乏,想转移一下兴趣点。下一篇可能会介绍机器学习在人机识别中的简单应用。鄙人新手,如有翻车,老司机多多包涵扶正。

    相关文章

      网友评论

        本文标题:验证码WEB端产品调研(二):极限验证

        本文链接:https://www.haomeiwen.com/subject/cgcwcxtx.html