<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端:從零破解一款輕量級(jí)滑動(dòng)驗(yàn)證碼

          共 6238字,需瀏覽 13分鐘

           ·

          2021-09-25 16:32

          轉(zhuǎn)自: https://juejin.cn/post/7009333291140513799

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          每天定時(shí)分享技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          昨天看到小夕的文章《從零開發(fā)一款輕量級(jí)滑動(dòng)驗(yàn)證碼插件》,介紹了一些相關(guān)驗(yàn)證碼的知識(shí)點(diǎn)。巧的是就在前兩周,公司舉辦了一個(gè)爬蟲攻防賽,需要用到多種機(jī)器驗(yàn)證的破解,其中一種就是滑塊驗(yàn)證。

          今天在這篇文章里給大家介紹一下怎樣使用代碼破解滑塊驗(yàn)證碼。文章末尾有源碼鏈接,需要的朋友可以自取,不過(guò)拿走之前記得三連哇!

          思路講解

          打開頁(yè)面

          滑塊驗(yàn)證碼是滑動(dòng)驗(yàn)證碼的一種,生成步驟至少有三步:

          1. 根據(jù)用戶標(biāo)識(shí),從后臺(tái)獲得驗(yàn)證碼圖片
          2. 監(jiān)聽鼠標(biāo)事件并回傳后臺(tái)
          3. 后臺(tái)判斷事件的真?zhèn)?,回傳?yàn)證結(jié)果

          無(wú)論生成機(jī)制的細(xì)節(jié)如何,滑塊都是要展示在頁(yè)面上的。直接用 HTTP Request 把頁(yè)面請(qǐng)求下來(lái)肯定不行,這里我們需要使用 Puppeteer 打開頁(yè)面。測(cè)試頁(yè)面就以 react-slider-vertify[1] 的官網(wǎng)為例。

          Puppeteer[2] 是一個(gè)通過(guò) DevTools 協(xié)議來(lái)控制瀏覽器行為的庫(kù),只需編寫不多的代碼,就可以操作真實(shí)的瀏覽器搞定諸如爬蟲、自動(dòng)化測(cè)試、網(wǎng)頁(yè)性能分析、瀏覽器擴(kuò)展測(cè)試等功能。使用起來(lái)比較方便,它文檔挺全的,根據(jù)文檔,打開頁(yè)面前需要啟動(dòng)一個(gè)瀏覽器實(shí)例,然后調(diào)用 newPage 方法創(chuàng)建一個(gè)新頁(yè)面。核心代碼如下。

          const puppeteer = require('puppeteer')

          puppeteer.launch().then(async browser => {
          const page = await browser.newPage()
          await page.goto('http://h5.dooring.cn/slider-vertify/vertify')
          })

          打開頁(yè)面測(cè)試

          正常瀏覽網(wǎng)頁(yè)時(shí),很少會(huì)碰到滑塊驗(yàn)證,因?yàn)樗穆窂椒浅iL(zhǎng),會(huì)需要浪費(fèi)用戶好一會(huì)兒時(shí)間。雖然文案上給你顯示“恭喜 0.9s 打敗了 99% 的用戶”,但從前置腳本請(qǐng)求,到加載圖片,用戶滑動(dòng)滑塊,到回傳驗(yàn)證...前前后后的步驟加起來(lái)可能花了你 9s 不止。本質(zhì)上它是防御性的手段,一般當(dāng)服務(wù)器限流,或者服務(wù)器已經(jīng)懷疑你是爬蟲的時(shí)候,才會(huì)讓它跳出來(lái)要求做進(jìn)一步驗(yàn)證。所以一般來(lái)說(shuō),爬蟲代碼可以默認(rèn)忽略滑塊驗(yàn)證的,不過(guò)本文代碼我們就假定默認(rèn)驗(yàn)證碼一定存在吧。

          接下來(lái),分析一下用戶行為。解決滑塊驗(yàn)證,無(wú)非就是先判斷一下缺口的位置,然后移動(dòng)鼠標(biāo)。這里進(jìn)一步可以細(xì)分為鼠標(biāo)的點(diǎn)擊事件和鼠標(biāo)移動(dòng)事件兩種。代碼邏輯大致如下。

          1. 等待驗(yàn)證碼圖片加載完畢
          2. 移動(dòng)鼠標(biāo)到滑塊位置
          3. 按下鼠標(biāo)
          4. 移動(dòng)鼠標(biāo)到缺口位置
          5. 松開鼠標(biāo)
          6. 等待結(jié)果返回

          啊,流程我都知道,可問(wèn)題就在,怎么判斷要把鼠標(biāo)移動(dòng)到哪里?服務(wù)器端返回的是一張帶缺口的圖片,缺口位置指定是不通過(guò)接口傳遞的。

          可能還有些同學(xué)會(huì)問(wèn),怎么移動(dòng)鼠標(biāo)呢?如果我一次性就把滑塊給移到指定位置了,那服務(wù)器不是會(huì)立馬把我標(biāo)記為爬蟲嘛... 這兩個(gè)問(wèn)題我逐一解答。

          判斷位置

          要分析缺口的位置,我們必須先知道這個(gè)缺口是怎么畫上去的。打開控制臺(tái)初步檢查可以發(fā)現(xiàn),頁(yè)面從服務(wù)器先拿到了一張完整的圖片,然后缺口位置是通過(guò) JS 隨機(jī)生成的。啊這...這...這是因?yàn)槲覀冇玫臏y(cè)試頁(yè)面是文檔頁(yè),大家不必在意這些安全方面的小細(xì)節(jié)。

          找到請(qǐng)求

          接著剛才的思路繼續(xù),既然缺口是從原圖中挖的一個(gè)洞,那么我們只需要識(shí)別一下圖片中洞的位置就好了。比較簡(jiǎn)單的方案是使用第三方的圖像識(shí)別技術(shù)(或相關(guān)技術(shù)),把圖片上傳至第三方,就直接拿到缺口位置的相對(duì)坐標(biāo)。下圖給大家展示一下阿里云的圖像分割的效果alimagic[3]。

          阿里云圖像分割

          如果想做一個(gè)效果穩(wěn)定一點(diǎn)的驗(yàn)證碼破解工具,我建議大家還是用自己的模型,或者自己寫算法。一來(lái)是第三方要的錢錢不少哇~ 再就是這種圖像識(shí)別并不是專門為驗(yàn)證碼訓(xùn)練的,所以放到爬蟲中還不太成熟,一旦背景圖片復(fù)雜,識(shí)別率下降得老快了

          百度云圖像主體識(shí)別

          以下介紹一種新思路。反正我們已經(jīng)有一張完整的圖片了,那就只要不斷地滑動(dòng)滑塊,把結(jié)果和原圖比對(duì)就行。理論上,只要滑倒差不多“那個(gè)點(diǎn)”,肉眼看起來(lái)不會(huì)有很大的違和感時(shí),就搞定了。比如下面這張圖。

          和原圖進(jìn)行比對(duì)

          截圖嘛可以直接用 Pupeteer 的截圖功能,它提供了對(duì)應(yīng)的 API,可以精準(zhǔn)的截出特定元素。

          至于圖片比對(duì),其實(shí)就是給圖片初步處理后,兩張圖一個(gè)像素一個(gè)像素的去比較。兩個(gè)像素如果顏色差異超過(guò)閾值,就認(rèn)為這是兩個(gè)不相同的像素。簡(jiǎn)便起見,我們直接用開源庫(kù) rembrandt[4],它會(huì)給我們返回兩張圖片間的差異。

          最后是滑動(dòng)滑塊,既然要模擬人肉操作,那么操作 CSS,用絕對(duì)定位,把滑塊一個(gè)像素一個(gè)像素的向右移;每移動(dòng)一次,把圖像比對(duì)的結(jié)果記錄下來(lái)。

          以上三個(gè)流程的核心代碼非常簡(jiǎn)單,只需要以下幾行:

          while (left <= maxOffset) {

          /* 使用 CSS left 屬性控制懸浮的滑塊的偏移量 */

          await page.evaluate(async ($sliderFloat, left) => {
          $sliderFloat.setAttribute('style', `left: ${left}px`)
          }, $sliderFloat, left)

          /* 截圖并和原圖進(jìn)行比對(duì),把結(jié)果存到 results 數(shù)組里 */

          const $panel = await page.$('#Vertify-demo-4 .canvasArea')
          const panelImgBase64 = await $panel.screenshot({
          type: 'jpeg'
          })
          const compareRes = await rembrandt({
          imageA: panelImgBase64,
          imageB: rawImage
          })
          results.push({
          left,
          diff: compareRes.differences
          })

          left += 1
          }

          通過(guò)CSS控制位移

          最后,把 results 扔到里面展示一下(這里給個(gè) ECharts 折線圖示例網(wǎng)址[5]),不出意外能得到這樣一張圖表。看到那個(gè)尖尖的“V”型山谷了嘛,呼哈哈哈,答案很明顯,當(dāng)我們把滑塊從左往右移動(dòng)時(shí),滑塊約接近缺口,那截出來(lái)的圖片就越像原圖,它兩之間像素差異越??;一直往右移動(dòng),滑塊會(huì)逐漸遠(yuǎn)離缺口,截出來(lái)的圖片和原圖相比像素差異又逐漸開始增大。我們只需要把差異最小的那個(gè)點(diǎn)找到,然后滑動(dòng)滑塊到對(duì)應(yīng)的 left 偏移量就闊以了。

          圖像比對(duì)結(jié)果

          題外話,為什么最大的差異在 3000 左右呢?我們簡(jiǎn)單估算一下。

          滑塊的大小為 45*45,再加上外面的圓形,約莫占了 2100 像素;也就是說(shuō)缺口加滑塊,理論上最大會(huì)有 4200 個(gè)像素和原圖不同。不過(guò)滑塊可能和遮住的地方像素有重合,假設(shè)重合了 350 像素,再加上我們的最低點(diǎn)的圖片差異都有 351,減去這些誤差,得 3499。嗚呼,3499 約等于 3000,估算成功(手動(dòng)狗頭)。

          不過(guò)還沒完,你要是把代碼跑起來(lái)就會(huì)發(fā)現(xiàn),臥草,太慢了這玩意兒!正常人劃一下驗(yàn)證碼頂多兩秒鐘的事兒,我們一幀一幀截圖得花個(gè) 40s 的時(shí)間才能截完圖算出山谷谷底的值來(lái)。

          這里提供幾種思路優(yōu)化效率:

          1. 把元素縮小,復(fù)制多份,平鋪開來(lái)展示;這樣只要截一次,然后再裁剪、比對(duì)就好。
          2. 放大步長(zhǎng),比方說(shuō)先每次平移 15px,找到局部最優(yōu)解,然后在局部最優(yōu)解附近再回到平移 1px 的方案找最優(yōu)解。
          3. 因?yàn)閳D片比對(duì)的結(jié)果類似“V”字,“V”字右半邊其實(shí)是可以不用再計(jì)算的。

          使用 1+2+3 我覺得可以在 3s 內(nèi)搞定最優(yōu)解,不過(guò)代碼復(fù)雜度會(huì)變得很高,文中簡(jiǎn)單起見暫只實(shí)現(xiàn)一下方案 2。

          首先是每次移動(dòng) 15px 找局部最優(yōu)解。

          // 圖片缺口是不會(huì)給挖在初始附近的,
          // 所以 left 從 45 像素開始計(jì)算可以節(jié)約不少計(jì)算量,
          let left = 45;
          const max15Offset = 265;
          const res15px = [];
          while (left <= max15Offset) {
          await setLeft(left);
          const compareRes = await compare();
          res15px.push({
          left,
          diff: compareRes.differences,
          });
          left += 15;
          }

          然后再嘗試每次移動(dòng) 2px 找最優(yōu)解,搜尋的范圍是 15px 步長(zhǎng)最優(yōu)解的 left 偏移量的左右共 20 個(gè)像素。

          const min15pxDiff = Math.min(...res15px.map((x) => x.diff));
          const min15pxLeft = res15px.find((x) => x.diff === min15pxDiff).left;

          left = min15pxLeft - 12;
          const max2Offset = min15pxLeft + 8;
          const res2px = [];
          while (left <= max2Offset) {
          await setLeft(left);
          const compareRes = await compare();
          res2px.push({
          left,
          diff: compareRes.differences,
          });
          left += 2;
          }

          此時(shí)得到的解可以約等于最優(yōu)解了。當(dāng)然,如果你覺得不穩(wěn)的話,還可以使用 1px 步長(zhǎng)去找。

          估算一下,原先需要截 245 次圖片,現(xiàn)在直接降到 1/10,23 次。不過(guò),也別太高興,因?yàn)闇y(cè)試發(fā)現(xiàn)只做優(yōu)化 2,解驗(yàn)證碼的時(shí)候還是要 7s...

          移動(dòng)鼠標(biāo)

          缺口位置都搞定了,那移動(dòng)鼠標(biāo)還不簡(jiǎn)單嘛~

          Puppeteer 已經(jīng)提供了鼠標(biāo)相關(guān)的接口[6],一共四個(gè):mouse.click、mouse.down、mouse.move、mouse.up,分別是點(diǎn)擊、按下、移動(dòng)和松開。使用 mouse.move 可以直接把鼠標(biāo)位置移動(dòng)到一個(gè)特定的坐標(biāo)上。假設(shè)我們現(xiàn)在從坐標(biāo)(100,100)花大約 1s 把鼠標(biāo)移動(dòng)到 (200,200),可以使用循環(huán)實(shí)現(xiàn)。

          const now = {
          x: 100,
          y: 100
          }
          const target = {
          time: 1000,
          x: 200,
          y: 200,
          }
          const steps = 10
          const step = {
          x: Math.floor((target.x - now.x) / steps),
          y: Math.floor((target.y - now.y) / steps),
          time: target.time / steps
          }
          while (now.x < target.x) {
          await sleep(step.time)
          now.x += step.x
          now.y += step.y
          await page.mouse.move(now)
          }

          害,要是打游戲的時(shí)候也像這段代碼一樣,我想我的手點(diǎn)到哪兒它就點(diǎn)到哪兒就好了~

          機(jī)器是不會(huì)手抖的,這段代碼和真實(shí)世界的滑動(dòng)效果相差太遠(yuǎn)了!我們看一張我用手滑的效果,尤其是要仔細(xì)觀察滑動(dòng)過(guò)程中鼠標(biāo)的位置。

          鼠標(biāo)軌跡不穩(wěn)定
          • 鼠標(biāo) Y 軸位置總是在變
          • 鼠標(biāo) X 軸位置會(huì)滑過(guò)頭

          這里做一波小優(yōu)化,把這兩個(gè)細(xì)節(jié)整合進(jìn)去。

          // 獲得一個(gè)隨機(jī)的偏移量
          const getRandOffset = (enableNegative = true, max = 3) => {
          const negative = enableNegative
          ? (Math.random() < 0.5) ? -1 : 1
          : 1
          return Math.floor(Math.random() * max) * negative
          }

          // 先滑過(guò)頭十幾像素,然后再花 100 毫秒的時(shí)間往回滑到正確位置
          const targets = [
          {
          time: 1000,
          x: 200 + getRandOffset(false, 15),
          y: 200 + getRandOffset(false, 15),
          steps: 10
          },
          {
          time: 100,
          x: 200,
          y: 200,
          steps: 3
          }
          ]

          // 注意這里用 for await 循環(huán)把 targets 串起來(lái)執(zhí)行
          for await (const target of targets) {
          const step = {
          x: Math.floor((target.x - now.x) / target.steps),
          y: Math.floor((target.y - now.y) / target.steps),
          time: target.time / target.steps,
          }
          let gap
          while (gap = Math.abs((target.x - now.x)), gap > 0) {
          await sleep(step.time)
          // 最后一步就直接滑動(dòng)到位,不需要隨機(jī)數(shù)了
          const inOneStep = Math.abs(target.x - now.x) <= Math.abs(step.x);
          if (inOneStep) {
          now.x = target.x;
          now.y = target.y;
          } else {
          now.x += step.x + getRandOffset();
          now.y += step.y + getRandOffset();
          }
          moveMouseTo(now)
          }
          }

          如何移動(dòng)鼠標(biāo)到這里就解決了,如果要考慮加速度、用戶習(xí)慣等因素,代碼會(huì)更加復(fù)雜,暫時(shí)就不深入討論啦,有興趣的同學(xué)可以自己研究。

          最終效果如下(這張 GIF 忘記設(shè)置循環(huán)播放了,可能要在新頁(yè)面打開它才能看到效果)。

          最終效果(加速)


          源碼地址:Crack-the-Slider[7]

          更多推薦



          點(diǎn)個(gè)在看你最好看

          瀏覽 159
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人拍拍视频 | 91视频免费在线观看 | 大香蕉手机视频 | 五月99久久婷婷国产综合亚洲 | 怡红院毛片 |