几乎所有的应用程序在涉及到用户信息安全的操作时,都会弹出验证码让用户进行识别,以确保该操作为人类行为,而不是大规模运行的机器。
早期的验证码是由随机字符加入杂点、干扰线后组成的一张图片,用户只需将图片中的字符输入到文本框中即可。该验证码利用了人类的视觉识别能力,将没有识别能力的机器拒之门外,但是这类验证码很快就被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
然后腾讯验证码很狗的加了 滑块儿 白色内发光。。