驗(yàn)證碼WEB端產(chǎn)品調(diào)研(一):Google reCAPTCHA

本文主要旨在橫向?qū)Ρ葒?guó)內(nèi)外同行產(chǎn)品,為公司自研驗(yàn)證碼產(chǎn)品設(shè)計(jì)提供參考和思路(目前僅關(guān)注web端,app端前期復(fù)用h5)。部分內(nèi)容來(lái)源于網(wǎng)絡(luò)。如有錯(cuò)誤,歡迎指正

筆者所在的團(tuán)隊(duì)今年的重點(diǎn)工作之一,是完善現(xiàn)有驗(yàn)證碼的產(chǎn)品形態(tài),改善只有字符輸入型驗(yàn)證碼的窘境:

原因是此類驗(yàn)證碼易被各圖像識(shí)別軟件、打碼平臺(tái)輕易破解,導(dǎo)致黃牛注冊(cè)、領(lǐng)券、撞庫(kù)等成本偏低,公司利益和用戶安全難以得到保障。

好的設(shè)計(jì)從學(xué)習(xí)開始。調(diào)研范圍如下,基本涵蓋了比較主流的驗(yàn)證碼平臺(tái):

如果要問筆者心中最好的產(chǎn)品,那無(wú)疑是Google reCAPTCHA。

產(chǎn)品背景

??reCAPTCHA起初是由CMU(卡耐基梅隆大學(xué))設(shè)計(jì),將OCR(光學(xué)自動(dòng)識(shí)別)軟件無(wú)法識(shí)別的文字掃描圖傳給世界各大網(wǎng)站,用以替換原來(lái)的驗(yàn)證碼圖片。那些網(wǎng)站的用戶在正確識(shí)別出這些文字之后,其答案便會(huì)被傳回CMU。這樣一來(lái)既起到了驗(yàn)證碼的效果,又可以幫助進(jìn)行古籍的數(shù)字化工作(可以稱為人工OCR)。

??reCAPTCHA在2009年被Google 收購(gòu),??Google讓reCAPTCHA里顯示Google街景的圖片。這樣經(jīng)常會(huì)從街景里提取如街道名稱和交通標(biāo)志等數(shù)據(jù),向Google地圖里添加商鋪地址和位置等有用信息。

思維超前,做事一舉兩得,似乎已融入reCAPTCHA的基因

再往后,新版reCAPTCHA又進(jìn)化為noCAPTCHA——這也是不得已而為之。人工智能飛速發(fā)展,據(jù)說(shuō)已經(jīng)能夠解決99.8%的圖片字符型驗(yàn)證碼,因此扭曲的文本驗(yàn)證方式也不再是一個(gè)可靠的方法。noCAPTCHA只提供了一個(gè)復(fù)選框,里面寫著“我不是機(jī)器人”。當(dāng)你打鉤之后,Google就能利用“風(fēng)險(xiǎn)分析引擎”進(jìn)行一系列無(wú)縫檢查,以此來(lái)判斷你是否是真人。

??如果noCAPTCHA認(rèn)為你是真人,那就不用再做什么了;否則會(huì)要求你填一個(gè)傳統(tǒng)的CAPTCHA字符串或更先進(jìn)的字符串,比如門牌號(hào),或從一組圖片中挑選出正確的圖片。??




??

復(fù)選框看似更簡(jiǎn)單,但背后確是Google強(qiáng)大的機(jī)器學(xué)習(xí)技術(shù)。基本的原理就是先檢測(cè)客戶端環(huán)境,判斷使用者是否處于人類的操作環(huán)境中。如果檢測(cè)結(jié)果在容錯(cuò)范圍內(nèi)則直接通過測(cè)試,否則彈出驗(yàn)證碼進(jìn)行二次認(rèn)證。
目前國(guó)內(nèi)的驗(yàn)證碼產(chǎn)品,只有阿里采用了類似做法。區(qū)別只在于一個(gè)是復(fù)選框,一個(gè)是滑動(dòng)條(后續(xù)阿里的部分會(huì)講到)

據(jù)統(tǒng)計(jì)reCAPTCHA的技術(shù)可以大大提高識(shí)別準(zhǔn)確率,總共可以有效節(jié)約用戶每天50000小時(shí)的上網(wǎng)時(shí)間。

reCAPTCHA今年3.13日又升級(jí)到了Invisible reCAPTCHA,顧名思義,“隱形驗(yàn)證碼”。這樣一來(lái),連復(fù)選框都不會(huì)出現(xiàn),對(duì)普通用戶完全透明,用戶體驗(yàn)又更進(jìn)一步。

頁(yè)面底部將展示一個(gè)logo注明當(dāng)前頁(yè)面使用到此技術(shù)(當(dāng)然不想要也可以將其隱藏)。

這種黑科技可謂十分霸氣,要知道國(guó)內(nèi)目前大多數(shù)公司還停留在滑動(dòng)拼圖和圖文點(diǎn)選,驗(yàn)證碼基本是強(qiáng)制展示——這意味著對(duì)所有用戶一視同仁,必須輸入。不過在贊助商眼中,“汝之毒藥,吾之蜜糖”

可以說(shuō),reCAPTCHA是一個(gè)劃時(shí)代的產(chǎn)品,如其官網(wǎng)所言:Tough on bots,Easy on humans,是另一種“一舉兩得”:既優(yōu)化了用戶體驗(yàn),又推動(dòng)了機(jī)器學(xué)習(xí)在工程領(lǐng)域的發(fā)展。筆者認(rèn)為,這也是未來(lái)驗(yàn)證碼這種自帶反人類屬性產(chǎn)品的發(fā)展方向——讓普通用戶和非法用戶,看到兩個(gè)不同的世界。

reCaptcha雖是免費(fèi),但受限于一些眾所周知的原因,此產(chǎn)品并沒有在國(guó)內(nèi)得到廣泛應(yīng)用。如果在國(guó)內(nèi)廣泛應(yīng)用,那可能google本身就不用墻了,這樣一來(lái),動(dòng)的可不止是國(guó)內(nèi)專業(yè)驗(yàn)證碼公司的奶酪...所以還是看國(guó)內(nèi)的驗(yàn)證碼產(chǎn)品,如何緊跟黑科技老大的步伐吧。

接入方式

官網(wǎng)目前產(chǎn)品主要有兩款:reCAPTCHA V2Invisible reCAPTCHA

reCAPTCHA V2便是noCAPTCHA的最新版(V1版據(jù)說(shuō)官方已不再維護(hù)),嵌入式復(fù)選框。

Invisible reCAPTCHA為之前提到的隱形驗(yàn)證碼。在此只關(guān)注它的接入方式(其實(shí)和reCAPTCHA V2類似),也算是觀摩學(xué)習(xí)業(yè)內(nèi)標(biāo)桿。

Invisible reCAPTCHA的調(diào)用方式有三種:

  1. Automatically bind the challenge to a button

  2. Programmatically bind the challenge to a button

  3. Programmatically invoke the challenge

其中第1種比較簡(jiǎn)單,官網(wǎng)的說(shuō)法是“The easiest method”——其實(shí)就是在已有的按鈕元素上添加一些屬性以綁定reCAPTCHA,由點(diǎn)擊自動(dòng)觸發(fā)。示例如下:

<html>
  <head>
    <title>reCAPTCHA demo: Simple page</title>
     <script src="https://www.google.com/recaptcha/api.js" async defer></script>
     <script>
       function onSubmit(token) {
         document.getElementById("demo-form").submit();
       }
     </script>
  </head>
  <body>
    <form id='demo-form' action="?" method="POST">
      <button class="g-recaptcha" data-sitekey="your_site_key" data-callback='onSubmit'>Submit</button>
      <br/>
    </form>
  </body>
</html>

這里比較有趣的是在第一個(gè)script使用的時(shí)候用到了asyncdefer屬性。查閱資料發(fā)現(xiàn),區(qū)別如下:
<pre><ol><li><p><code><script src="script.js"></script></code></p>沒有 <code>defer</code> 或 <code>async</code>,瀏覽器會(huì)立即加載并執(zhí)行指定的腳本,“立即”指的是在渲染該 <code>script</code> 標(biāo)簽之下的文檔元素之前,也就是說(shuō)不等待后續(xù)載入的文檔元素,讀到就加載并執(zhí)行。</li><li><p><code><script async src="script.js"></script></code></p><p>有 <code>async</code>,加載和渲染后續(xù)文檔元素的過程將和 <code>script.js</code> 的加載與執(zhí)行并行進(jìn)行(異步)。</p></li><li><p><code><script defer src="myscript.js"></script></code></p><p>有 <code>defer</code>,加載后續(xù)文檔元素的過程將和 <code>script.js</code> 的加載并行進(jìn)行(異步),但是 <code>script.js</code> 的執(zhí)行要在所有元素解析完成之后,<code>DOMContentLoaded</code> 事件觸發(fā)之前完成。</p></li></ol></pre>
一張圖說(shuō)明上面一段講的是啥:


所以可以看出,最大的差別在于"腳本下載完之后何時(shí)執(zhí)行"——顯然defer是最接近我們對(duì)于應(yīng)用腳本加載和執(zhí)行的要求的。async對(duì)于應(yīng)用腳本的用處不大,因?yàn)樗耆豢紤]依賴(哪怕是最低級(jí)的順序執(zhí)行),不過它對(duì)于那些可以不依賴任何腳本或不被任何腳本依賴的腳本來(lái)說(shuō)卻是非常合適的,最典型的例子:Google Analytics

那么問題來(lái)了。如果asyncdefer同時(shí)使用,會(huì)產(chǎn)生什么效果?
對(duì)此有專門的規(guī)范:

The defer attribute may be specified even if the async attribute is specified, to cause legacy Web browsers that only support defer (and not async) to fall back to the defer behavior instead of the synchronous blocking behavior that is the default.

意思是有些老瀏覽器就只支持defer不支持async,那么自然就達(dá)不到async的效果。這時(shí)就要用defer來(lái)fallback async,從而避免同步阻塞。

說(shuō)完第1種,再來(lái)看看第2種:自己通過JS代碼的方式控制一些屬性的傳入。示例如下:

<html>
  <head>
    <title>reCAPTCHA demo: Explicit render after an onload callback</title>
    <script type="text/javascript">
      
        var onSubmit = function(token) {
          console.log('success!');
        };

        var onloadCallback = function() {
          grecaptcha.render('submit', {
            'sitekey' : 'your_site_key',
            'callback' : onSubmit
          });
        };
    </script>
  </head>
  <body>
    <form action="?" method="POST">
      <input id='submit' type="submit" value="Submit">
    </form>
    <script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit"
        async defer>
    </script>
  </body>
</html>

其中需要注意的是:由于順序執(zhí)行的原因,onloadCallback要寫在前面,這樣最后才能保證onload可以接收到
onloadCallback(否則 async and defer 也是白用了)

When your callback is executed, you can call thegrecaptcha.render
method from the Javascript API.Note: your onload callback function must be defined before the reCAPTCHA API loads. To ensure there are no race conditions:

  • order your scripts with the callback first, and then reCAPTCHA
  • use the async and defer parameters in thescript tags

第2種方式,代碼量相對(duì)第1種多了許多,但是卻做到了“不污染已有的html元素”
第3種方式,是分配一塊頁(yè)面空間給到reCAPTCHA,從而固定其展示的位置。因?yàn)镮nvisible reCAPTCHA的Invisible并不是絕對(duì)的——你可以通過調(diào)參使其visible。那么這樣就可以秀出“我們是有用驗(yàn)證碼的”。

隱形==不存在?不存在的

與第2種相比,同樣沒有污染已有元素,但是更加customised。示例如下:

<html>
<head>
<script>
  function onSubmit(token) {
    alert('thanks ' + document.getElementById('field').value);
  }

  function validate(event) {
    event.preventDefault();
    if (!document.getElementById('field').value) {
      alert("You must add text to the required field");
    } else {
      grecaptcha.execute();
    }
  }

  function onload() {
    var element = document.getElementById('submit');
    element.onclick = validate;
  }
</script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
   <form>
     Name: (required) <input id="field" name="field">
     <div id='recaptcha' class="g-recaptcha"
          data-sitekey="your_site_key"
          data-callback="onSubmit"
          data-size="invisible"></div>
     <button id='submit'>submit</button>
   </form>
<script>onload();</script>
</body>
</html>

注意這里的invoke不是使用render,而是execute。具體的api調(diào)用如下:

服務(wù)端的地方就不討論,因?yàn)楣P者這次主要是關(guān)注前端。但是服務(wù)端的東西,都大同小異:無(wú)非是需要特定的密鑰,結(jié)合前端callback的內(nèi)容做二次驗(yàn)證。原因是前端可以被繞過。

驗(yàn)證流程如下(這里借用網(wǎng)易易盾流程圖。其實(shí)都差不多):

源碼分析

其實(shí)不能叫源碼——因?yàn)槟芸吹降幕径冀?jīng)過壓縮和混淆,可讀性是非常差的。只能看出一個(gè)大概。
reCAPTCHA主要是用到了兩個(gè)js:api.jsrecaptcha__en.js
都是用原生Js實(shí)現(xiàn),這樣不依賴任何框架或插件,對(duì)調(diào)用方的兼容性最好。
api.js是對(duì)外暴露的js,其內(nèi)部會(huì)調(diào)用recaptcha__en.js。完整代碼如下:

(function () {
    if (!(window["___grecaptcha_cfg"])) {
        window["___grecaptcha_cfg"] = {};
    }

    ;
    if (!(window["___grecaptcha_cfg"]["render"])) {
        window["___grecaptcha_cfg"]["render"] = "onload";
    }

    ;
    window["__google_recaptcha_client"] = true;
    var po = document.createElement("script");
    po.type = "text/javascript";
    po.async = true;
    po.src = "https://www.gstatic.com/recaptcha/api2/r20170613131236/recaptcha__en.js";
    var elem = document.querySelector("script[nonce]");
    var nonce = elem && ((elem["nonce"]) || (elem.getAttribute("nonce")));
    if (nonce) {
        po.setAttribute("nonce", nonce);
    }

    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(po, s);
})();

其中可以看出,recaptcha__en.js是通過代碼parentNode.insertBefore引入的。這樣的寫法有點(diǎn)類似一個(gè)Proxy,接入方可以不必關(guān)注具體是用的什么recaptcha__en.js來(lái)處理(從r20170613131236/recaptcha__en.js這種寫法上看,這個(gè)js是有多版本的。很可能會(huì)動(dòng)態(tài)變化)。
至于recaptcha__en.js,Idea美化后接近1w行... 加上可讀性這座大山,很難短時(shí)間分析出太多有價(jià)值的東西,只能掃讀,找到一些感興趣的點(diǎn)。
例如筆者想到,reCAPTCHA在校驗(yàn)不通過的時(shí)候,一定會(huì)彈出常規(guī)驗(yàn)證碼。那么Widget的內(nèi)容一定在recaptcha__en.js中(除非再引用專門的Widget.js)。果然包含如下代碼:

switch (u(c) ? c.toString() : c) {
            case "tileselect":
            case "multicaptcha":
                var c = "", d = a.label;
                switch (u(d) ? d.toString() : d) {
                    case "Turkeys":
                        c += "Select all squares with <strong>Turkeys</strong>.";
                        break;
                    case "GiftBoxes":
                        c += "Select all squares with <strong>gift boxes</strong>.";
                        break;
                    case "Fireworks":
                        c += "Select all squares with <strong>fireworks</strong>.";
                        break;
                    case "TileSelectionStreetSign":
                        c += "Select all squares with <strong>street signs</strong>.";
                        break;
                    case "TileSelectionBizView":
                        c += "Select all squares with <strong>business names</strong>.";
                        break;
                    case "stop_sign":
                        c += "Select all squares with <strong>stop signs</strong>.";
                        break;
                    case "vehicle":
                    case "/m/07yv9":
                    case "/m/0k4j":
                        c += "Select all squares with <strong>vehicles</strong>.";
                        break;
                    case "road":
                    case "/m/06gfj":
                        c += "Select all squares with <strong>roads</strong>.";
                        break;
                    case "house":
                    case "/m/03jm5":
                        c +=
                            "Select all squares with <strong>houses</strong>.";
                        break;
                    case "apparel_and_fashion":
                        c += "Select all squares with <strong>clothing</strong>.";
                        break;
                    case "bag":
                        c += "Select all squares with <strong>bags</strong>.";
                        break;
                    case "dress":
                        c += "Select all squares with <strong>dresses</strong>.";
                        break;
                    case "eye_glasses":
                        c += "Select all squares with <strong>eye glasses</strong>.";
                        break;
                    case "hat":
                        c += "Select all squares with <strong>hats</strong>.";
                        break;
                    case "pants":
                        c += "Select all squares with <strong>pants</strong>.";
                        break;
                    case "shirt":
                        c += "Select all squares with <strong>shirts</strong>.";
                        break;
                    case "shoe":
                        c += "Select all squares with <strong>shoes</strong>.";
                        break;
                    case "USER_DEFINED_STRONGLABEL":
                        c += "Select all squares that match the label: <strong>" + W(a.Ld) + "</strong>.";
                        break;
                    default:
                        c += "Select all images below that match the one on the right."
                }
                "multicaptcha" == a.ie && (c += '<br/><span class="rc-imageselect-carousel-instructions">If there are none, click skip.</span>');
                a = V(c);
                b += a;
                break;
            default:
                c = "";
                d = a.label;
                switch (u(d) ?
                    d.toString() : d) {
                    case "romantic":
                        c += "Select all images that feel <strong>romantic for dining</strong>.";
                        break;
                    case "outdoor_seating_area":
                        c += "Select all images with <strong>outdoor seating areas</strong>.";
                        break;
                    case "indoor_seating_area":
                        c += "Select all images with <strong>indoor seating areas</strong>.";
                        break;
                   ……//省略類似代碼
                    default:
                        d = "Select all images that match the label: <strong>" + (W(a.Ld) + "</strong>."), c += d
                }
                "dynamic" == a.ie && (c += "<br/><span>Click verify once there are none left.</span>");
                a = V(c);
                b += a
        }

問題都是固定的,配以的應(yīng)該是隨機(jī)的同名卻不同內(nèi)容的圖片。
那么就可以推測(cè),這個(gè)js應(yīng)該包含各類驗(yàn)證碼的集合。于是發(fā)現(xiàn):

var Fo = function (a) {
        switch (a) {
            case "default":
                return new On;
            case "nocaptcha":
                return new yo;
            case "imageselect":
                return new Bn;
            case "tileselect":
                return new Bn("tileselect");
            case "dynamic":
                return new Wn;
            case "audio":
                return new mn;
            case "text":
                return new Do;
            case "multicaptcha":
                return new Sn;
            case "canvas":
                return new Jn;
            case "coref":
                return new no;
            case "prepositional":
                return new vo
        }
    };

所以Google的"軍備庫(kù)"還是很豐富的——甚至遠(yuǎn)多于國(guó)內(nèi)的驗(yàn)證碼公司。每一項(xiàng)是什么不得而知,但能看出有音頻和點(diǎn)選。這近1w行代碼,可能是一個(gè)整包。驗(yàn)證碼本身還是有難易之分的,推測(cè)Google是靠行為分析來(lái)展示出不同難度的驗(yàn)證碼給robots。
進(jìn)而再想到,行為分析一定要收集瀏覽器信息,或者常用設(shè)備,那么必須要用到userAgent。搜索發(fā)現(xiàn),確實(shí)有一處:

 a:{
        var tb = k.navigator;
        if (tb) {
            var ub = tb.userAgent;
            if (ub) {
                sb = ub;
                break a
            }
        }
        sb = ""
    }

但具體如何使用,就需要花時(shí)間研究了。有興趣的讀者可以分析一下,歡迎交流。

總結(jié)

同國(guó)內(nèi)驗(yàn)證碼產(chǎn)品相比,Google reCAPTCHA在接入上,并不強(qiáng)調(diào)產(chǎn)品的形態(tài)(點(diǎn)選 、滑動(dòng)等),而是側(cè)重接入的靈活程度。具體的形態(tài)由“風(fēng)險(xiǎn)分析引擎”得出,動(dòng)態(tài)展示給調(diào)用方。在這想起李小龍的武術(shù)哲學(xué):

以無(wú)法為有法,以無(wú)限為有限

截拳道包羅萬(wàn)象,卻不被萬(wàn)象所包羅。Google reCAPTCHA也有點(diǎn)這個(gè)意思——大概這就是所謂的強(qiáng)者姿態(tài)。

?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容