概述
垃圾邮件对于站长来说是个大问题,但是,为了拦截垃圾邮件而设置的验证给用户带来的糟糕体验同样令人发疯。
验证很糟,但或许我们可以直面它,而事实上现在也有很多种验证方式来确保当前操作是人为的有效操作,但每一种都很糟。
近年来,自动化脚本变得越来越聪明,也越来越难从它们手中保护网站。凭借大量的业余时间和足够的资源,人们几乎可以绕过任何验证:人们编写插件来阻止Puppeteer检测,或者绕过reCAPTCHA1(甚至有公司专门提供绕过验证的服务)而我们只是通过 Puppeteer2控制无法被检测识别到的Headless版Chrome3。
很多网站的验证方式都包含了滑动验证,但是为什么会有人选择一种如此易被绕过的验证方式呢?主要有以下3个原因:
- 大部分脚本机器人都没有执行JS,因此可以阻止机器人;
- 滑动验证对用户友好;
- 滑动验证的动作对于移动用户较为自然。
因此滑动验证是一种优秀并且易操作的验证方式,但是只是用滑动验证作为唯一的验证手段无异于手持木棍和狮子搏斗一样不自量力。下面我们来演示如何通过简单的几步绕过滑动验证。
滑动提交
有一个叫做“Slide To Submit”的jQuery插件,通过滑动来执行提交操作,以阻止垃圾邮件。
首先我们需要填充输入域(input fields)。为了能够移动滑动验证中的滑块,需要执行以下动作:
- 鼠标悬停再滑块正中心上;
- 按下鼠标;
- 移动鼠标;
- 松开鼠标;
好了,就这么简单。
const puppeteer = require('puppeteer');
async function run() {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: { width:1366, height: 768 }
})
const page = await browser.newPage()
await page.goto('http://kthornbloom.com/slidetosubmit/')
await page.type('input[name="name"]', 'Puppeteer Bot')
await page.type('input[name="email"]', 'js@automation.com')
let sliderElement = await page.$('.slide-submit')
let slider = await sliderElement.boundingBox() // Get the whole slide-bar element infomation to know the range of mouse move
let sliderHandle = await page.$('.slide-submit-thumb')
let handle = await sliderHandle.boundingBox() // Get the slide-block element infomation for mouse clicking
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2)
await page.mouse.down()
await page.mouse.move(handle.x + slider.width, handle.y + handle.height / 2, {steps: 10 })
await page.mouse.up()
// success!
await browser.close()
}
run()
Dipbit注册验证
Dipbit是一个用于数字货币交易的网站,无论是登录还是注册都需要进行滑动验证。Dipbit比较智能,所以我们需要在代码中添加几行内容来隐藏掉Puppeteer的操作。
const puppeteer = require('puppeteer')
async function run() {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: { width:1366, height: 768 }
})
const page = await browser.newPage()
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false
})
})
await page.goto('http://www.dipbit.com/auth/login')
await page.type('#email', 'js@automation.com')
await page.type('#password', 'password123')
let sliderElement = await page.$('.slidetounlock')
let slider = await sliderElement.boundingBox()
let sliderHandle = await page.$('.nc_iconfont.btn_slide')
let handle = await sliderHandle.boundingBox()
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2)
await page.mouse.down()
await page.mouse.move(handle.x + slider.width, handle.y + handle.height / 2, {steps: 50 })
await page.mouse.up()
// success!
await browser.close()
}
run()
演示效果如下:
淘宝注册验证
淘宝是一个来自中国的线上购物平台,它使用了和Dipbit类似的滑动验证注册,唯一的区别在于它将整个表单包裹在<iframe>
元素中。但这对Puppeteer来说,并不是什么问题。
const puppeteer = require('puppeteer')
async function run() {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: { width:1366, height: 768 }
})
const page = await browser.newPage()
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false
})
})
await page.goto('https://world.taobao.com/markets/all/sea/register')
let frame = page.frames()[1]
await frame.waitForSelector('.nc_iconfont.btn_slide')
const sliderElement = await frame.$('.slidetounlock')
const slider = await sliderElement.boundingBox()
const sliderHandle = await frame.$('.nc_iconfont.btn_slide')
const handle = await sliderHandle.boundingBox()
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2)
await page.mouse.down()
await page.mouse.move(handle.x + slider.width, handle.y + handle.height / 2, {steps: 50 })
await page.mouse.up()
// success!
await browser.close()
}
run()
滑动拼图验证
我碰到过对用户来说简单但是对脚本来说很困难的“滑动验证”Vue组件。这种验证方法使用了图片,创建了两个canvas
和一个滑动栏,把原始图片用拼图的形式渲染出来。用户通过移动滑块控制拼图并使两个拼图对齐,接着松开滑块完成验证。这种验证会让拼图出现在水平随机位置来干扰脚本,避免脚本的暴力破解。
我不打算在这里使用任何花哨的办法,像是机器学习或是OCR,所以我使用的简单粗暴的办法——通过一步一步的滑动滑块并将拼图和原图片比较,直到找到最合适的位置。
我使用Rembrandt.js库来比较图像。
值得一提的是,我甚至在垂直方向(Y轴)上叠加了一个随机移动量来模拟用户的鼠标移动。
const puppeteer = require('puppeteer')
const Rembrandt = require('rembrandt')
async function run() {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: { width: 1366, height: 768 }
})
const page = await browser.newPage()
let originalImage = ''
await page.setRequestInterception(true)
page.on('request', request => request.continue())
page.on('response', async response => {
if (response.request().resourceType() === 'image'){
originalImage = await response.buffer().catch(() => {})
console.log(originalImage)
}
})
await page.goto('https://monoplasty.github.io/vue-monoplasty-slide-verify')
const sliderElement = await page.$('.slide-verify-slider')
const slider = await sliderElement.boundingBox()
const sliderHandle = await page.$('.slide-verify-slider-mask-item')
const handle = await sliderHandle.boundingBox()
let currentPosition = 0
let bestSlider = {
position: 0,
difference: 100
}
await page.mouse.move(handle.x + handle.width /2, handle.y + handle.height / 2)
await page.mouse.down()
while (currentPosition < slider.width - handle.width / 2) {
await page.mouse.move(
handle.x + currentPosition,
handle.y + handle.height / 2 + Math.random() * 10 - 5
)
let sliderContainer = await page.$('.slide-verify')
let sliderImage = await sliderContainer.screenshot()
const rembrandt = new Rembrandt({
imageA: originalImage,
imageB: sliderImage,
thresholdType: Rembrandt.THRESHOLD_PERCENT
})
let result = await rembrandt.compare()
let difference = result.percentageDifference * 100
if (difference < bestSlider.difference) {
bestSlider.difference = difference
bestSlider.position = currentPosition
}
currentPosition += 5
}
await page.mouse.move(handle.x + bestSlider.position, handle.y + handle.height / 2, { steps: 10 })
await page.mouse.up()
}
run()
结语
网站应当使用对用户友好却更容易被绕过的验证,还是使用更加严苛的验证来免于遭受同样具有侵略性的脚本攻击而罔顾用户的体验,这是一个永恒的难题。网站与脚本之间的战争永不停息。无论网站推出怎样的验证方法,都会有人能个找到并写出绕过它们的程序,只是时间长短的问题。 然而这篇文章仅用于教学,当你出于个人用途用到Puppeteer时,一定要更加负责任的使用。