几乎所有的应用程序在涉及到用户信息安全的操作时,都会弹出验证码让用户进行识别,以确保该操作为人类行为,而不是大规模运行的机器。
早期的验证码是由随机字符加入杂点、干扰线后组成的一张图片,用户只需将图片中的字符输入到文本框中即可。该验证码利用了人类的视觉识别能力,将没有识别能力的机器拒之门外,但是这类验证码很快就被OCR技术绕过了。腾讯防水墙滑块验证码是基于视觉识别+用户操作的验证码,长这样:

用户需要先识别出缺口位置,再将方块正确移动到对应的位置上。相比传统的字符验证码,不仅改成了对机器来说更难识别的图片,还加入了用户操作的行为,难度更上一层楼。
网上类似的教程大多数都是Java / Python + Selenium + OpenCV,识别率约90%;
本文提供Javascript + Selenium的简单粗暴解决思路,且识别率99%+。
打开验证码
这里演示打开腾讯防水墙官网的滑块验证码。
const { Builder, By } = require('selenium-webdriver')
;(async () => {
/**
* 打开验证码
*/
// 打开浏览器
const driver = new Builder().forBrowser('chrome').build()
// 加载腾讯防水墙官网
await driver.get('https://007.qq.com/online.html');
// 点击体验验证码按钮
(await driver.findElement(By.id('code'))).click()
// 等验证码加载完
await driver.sleep(2000)
})
识别缺口
缺口由1px的白色边框 + 深色背景组成,上下左右四个边框可能会凸出来或凹下去,如图所示:

下图标出绿色的部分不会受到凹凸影响,将该部分的某一行二值化后就是:白 + 黑 * 87 + 白,我们按照这个规则就能找到缺口的位置了。

是的,就是这么简单。
// ...
;(async () => {
// ...
/**
* 进入验证码的iframe里
*/
const frame = await driver.findElement(By.id('tcaptcha_iframe'))
await driver.switchTo().frame(frame)
/**
* 前端计算出对齐缺口需要的偏移值
*/
let offset = await driver.executeScript(() => {
// 获取背景图及其宽高
const bg = document.getElementById('slideBg')
const w = bg.naturalWidth
const h = bg.naturalHeight
// 把背景绘制到canvas中,用于获取每个像素的数据
const cvs = document.createElement('canvas')
cvs.width = w
cvs.height = h
const ctx = cvs.getContext('2d')
ctx.drawImage(bg, 0, 0)
// 获取不会收到凹凸影响的某一行:滑块top * 2 + 方块顶部偏移值(23) + 会受到凹凸影响的高度(16) + 1
// 在该行中寻找符合规则的索引:白 + 黑*87 + 白
const y = parseInt($('#slideBlock').css('top')) * 2 + 40
let lastWhite = -1
for (let x = w / 2; x < w; x ++) {
const [r, g, b] = ctx.getImageData(x, y, 1, 1).data
const grey = (r * 299 + g * 587 + b * 114) / 1000
// 以150为阈值,大于该值的认定为白色
if (grey > 150) {
if (lastWhite === -1 || x - lastWhite !== 88) {
lastWhite = x
} else {
lastWhite /= 2 // 图片缩小了2倍
lastWhite -= 37 // 滑块left(26) + 方块自身偏移值(23 / 2)
lastWhite >>= 0 // 移动的像素必须为整数
return lastWhite
}
}
}
})
})
此时再看OpenCV的做法:

模拟拖动
除了识别缺口位置外,拖动也是验证中的一部分,有下面行为之一的可能会无法通过
- 拖动过快,0.1s内完成验证
- 总是1像素偏差都没有
- 从头到尾都是均匀移动
按照此规则,制定一个拖动的方案:每次以20ms随机拖动2-10像素,当与目标位置差值在5像素内时停止:
// ...
;(async () => {
// ...
/**
* 执行滑块拖动操作
*/
// 找到iframe中的滑块元素
const slide = await driver.findElement(By.id('tcaptcha_drag_thumb'))
// 将鼠标移动到滑块上并按下左键
const actions = driver.actions().move({origin: slide}).press()
// 每次以20ms随机拖动2-10个像素,当与目标位置差值在5像素内时停止
let current = 0
while (Math.abs(offset - current) > 5) {
const distance = Math.round(Math.random() * 8) + 2
current += distance
actions.move({origin: slide, x: distance, duration: 20})
console.log(`moving: ${current} / ${offset}`)
}
// 松开鼠标左键
await actions.release().perform()
})
拖动的效果看起来比较弱智,但是真的管用。
博主红包交流下哦:) QQ306582825
您好,跨域好像有点问题,顺丰站点提示 UnhandledPromiseRejectionWarning: WebDriverError: : Blocked a frame with origin “https://www.sf-express.com” from accessing a cross-origin frame.能麻烦指点下嘛
是不是没有禁止同源策略?参考代码:
const options = new Chrome.Options().addArguments(‘–disable-web-security’)
const driver = new Builder().forBrowser(‘chrome’).setChromeOptions(options).build()
感谢大神回复,是从您的源代码改动的,加了的。
您源码中的网页正常运行,然后改了下地址就不行了
加一下我QQ吧:2437404258
然后腾讯验证码很狗的加了 滑块儿 白色内发光。。