前言
入正题看看滑块是怎么个事
目标网站:aHR0cHM6Ly9idXNpbmVzcy5vY2VhbmVuZ2luZS5jb20vbG9naW4=
开启F12,选择邮箱登录,邮箱填的简单一点就可以触发滑块验证码,比如"abcabc123456@qq.com"
点击登录后我们就可以看见滑块,观察请求,可以看见是一个"/account_login/v2/..."的请求返回了验证码的一些信息,这个接口实际上就是登录接口,异常就会返回验证码信息。
分析请求
我们继续看请求,下面有一个"rmc.bytedance.com/verifycenter/captcha/v2/....."的get请求,并且是一个html页面,我们直接双击进去,可以发现这就是滑块验证码本体。这个验证码窗口实际上是内嵌进登录页面的"iframe"中的,那么我们后面的滑动测试和分析都可以单独打开这个页面进行了。
我们手动滑动测试一下看看它会发送什么请求。
可以看见有2个请求,请求1(验证是否滑动成功)"/captcha/verify/...";请求2(重新登录)"/account_login/v2/..."。
到这就差不多确定整个流程了:
了解滑块验证码构成
滑块验证码一般是由滑块角块(也是图片)和背景图片两部分构成,其中滑块背景图片位置是固定的,我们滑动的是滑块角块图片到背景图片缺口位置。
滑块的初始位置一般会是随机的,而巨量(抖音系)的滑块则是固定在最左边,这无形中降低了我们的处理难度,因为不需要去找滑块角块的初始位置。
ddddocr 解决方案
经过测试,发现巨量滑块是可以使用无头浏览器进行处理的,所以我们这里使用ddddocr框架进行图像识别,然后获取滑动距离;使用无头浏览器进行滑块滑动。
所以我们想要实现自动化登录就需要处理几个关键点:
- 获取滑块验证码的参数信息。滑块图片、背景图片(如果有滑块验证码)、以及获取滑块的参数(用于验证)。
- 使用无头浏览器滑动滑块。
1、获取滑块验证码的参数信息
我们清空缓存,刷新页面重新登录,找到/account_login/v2/...接口,可以看到我们在清空缓存后的第一次请求fp是空的,并且有一个a_bogus参数,account、password是经过一定算法处理的账号和密码。
然后我们在关闭验证码框,账号密码不变,重新点一次登录,再次观察请求,我们会发现fp参数就不为空了,并且a_bogus也改变了,只有账号和密码还有其他参数没有改变,因此我们可以得出结论:在账号密码不变的情况下,登录接口只有a_bogus和fp参数是动态改变的。
a_bogus算法处理生成的(这个参数登录接口暂时没有做强校验,如果设置为空后台登录只是会显示未知登录设备,所以暂不处理,后续单独出一篇内容讲解。),而fp参数是由登录接口返回的。
我们在看请求接口"rmc.bytedance.com/verifycenter/captcha/v2/...",可以发现它的请求参数fp、verify_data就是在登录接口中获得到的2个参数,并且其他的参数只有一个时间戳会改变。
我们在手动滑动滑块到正确的位置,可以看见先发送了一个请求/captcha/verify/...这是一个验证是否滑动正确的请求,然后又发送了第二次/account_login/v2/...登录请求,并且这一次的响应不是验证码信息,而是登录成功/密码错误的信息。
那么其实到这我们需要的所有内容就已经得到了。
完整的流程是:先登录获取验证码参数 -> 构造验证码html -> 访问html(滑块图片、背景图片都包含在内) -> 通过无头浏览器去处理滑块。
2、使用无头浏览器滑动
使用无头浏览器滑动我们需要先获得滑块和目标位置的距离,而巨量这个滑块起点固定在初始的位置,并且没有y轴的需求,所以我们使用ddddocr可以很轻松的得到距离。
import time
import base64
import random
import ddddocr
import requests
from playwright.sync_api import sync_playwright, Page
def get_img_bytes_by_htmltag_selector(page: Page, selector: str) -> bytes:
"""
根据浏览器标签id获取图片bytes 数据
:param page: page对象
:param selector: css选择器 img#captcha-verify_img_slide 表示找img标签id=captcha-verify_img_slide的
:return:
"""
img_bytes = b''
if "img" in selector:
page.wait_for_selector(selector)
img_element = page.query_selector(selector)
img_url = img_element.get_attribute('src')
img_bytes = requests.get(img_url).content
elif "canvas" in selector:
page.wait_for_selector(selector)
canvas = page.query_selector(selector)
base64_data: str = page.evaluate('(canvas) => canvas.toDataURL("image/jpg")', canvas).replace(
, '')
img_bytes = base64.b64decode(base64_data)
return img_bytes
def get_distance_by_ddddocr(slide_img_bytes, target_img_bytes) -> int:
"""
通过ddddocr识别到目标图案的坐标
:param slide_img_bytes: 小滑块图片字节
:param target_img_bytes: 目标背景图片字节
:return: 目标图案坐标x轴
"""
det = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False)
res = det.slide_match(slide_img_bytes, target_img_bytes, simple_target=True)
target_x = res["target"][0]
return target_x
if __name__ == '__main__':
# 滑块iframe测试链接
url_base64='aHR0cHM6Ly9ybWMuYnl0ZWRhbmNlLmNvbS92ZXJpZnljZW50ZXIvY2FwdGNoYS92Mj9mcm9tPWlmcmFtZSZmcD12ZXJpZnlfbHdlejN5ZmdfYmM3YWIxMTRfNGMwOF9lNmU2XzNmZWFfNDc1NzIxM2EwNzE3JmVudj0lN0IlMjJzY3JlZW4lMjIlM0ElN0IlMjJ3JTIyJTNBMTkyMCUyQyUyMmglMjIlM0ExMDgwJTdEJTJDJTIyYnJvd3NlciUyMiUzQSU3QiUyMnclMjIlM0ExOTIwJTJDJTIyaCUyMiUzQTEwNTUlN0QlMkMlMjJwYWdlJTIyJTNBJTdCJTIydyUyMiUzQTkwMSUyQyUyMmglMjIlM0E5MzQlN0QlMkMlMjJkb2N1bWVudCUyMiUzQSU3QiUyMndpZHRoJTIyJTNBOTAxJTdEJTJDJTIycHJvZHVjdF9ob3N0JTIyJTNBJTIyYnVzaW5lc3Mub2NlYW5lbmdpbmUuY29tJTIyJTJDJTIydmNfdmVyc2lvbiUyMiUzQSUyMjEuMC4wLjYwJTIyJTJDJTIybWFza1RpbWUlMjIlM0ExNzE2MjA5OTc4NDYyJTJDJTIyaDVfY2hlY2tfdmVyc2lvbiUyMiUzQSUyMjQuMC41JTIyJTdEJmFpZD0xNDAyJnNjZW5lX2xldmVsPXAyJmFwcF9uYW1lPWFjd2ViJmhvc3Q9JTJGJTJGdmVyaWZ5LnppamllYXBpLmNvbSZsYW5nPXpoJnRoZW1lPSU3QiUyMmhhbGZfY25fT2tCdG5CZ0NvbG9yJTIyJTNBJTIyJTIzMkE1NUU1JTIyJTdEJnZlcmlmeV9kYXRhPSU3QiUyMmNvZGUlMjIlM0ElMjIxMDAwMCUyMiUyQyUyMmZyb20lMjIlM0ElMjJzaGFya19hZG1pbiUyMiUyQyUyMnR5cGUlMjIlM0ElMjJ2ZXJpZnklMjIlMkMlMjJ2ZXJzaW9uJTIyJTNBJTIyMSUyMiUyQyUyMnJlZ2lvbiUyMiUzQSUyMmNuJTIyJTJDJTIyc3VidHlwZSUyMiUzQSUyMnNsaWRlJTIyJTJDJTIydWlfdHlwZSUyMiUzQSUyMiUyMiUyQyUyMmRldGFpbCUyMiUzQSUyMnhSRnZOOVJhNXE0ZmhHZHc0ek5YdTRsbk5tNmQ0b1dEbW1UU1FPOGxpM1dBOUNEa2JoRUdpaEZqUUFvcEtGY2czVEZHVioqU01ZeFFObWdIN1lHMXZJUnpuM1lORCpOWW14QWFuVmotenprNmNMUFZUWVllS3dicVF0MEpnZlBRRDZ6KnBick9PeGFhS2FPd3ktQnZYczZzcHkqY0lFUU5wQ0c1dGQyd3NXT3FVM21sZ1lJMDlucVhvUDBZRWViRTY4ZW1mVEk1NzkqNW1nTk5hMjdTeHFoeEJBM1AzRHFNb2tDWHRoLVM0SjJMcSpPbHJ5aDg1QTZycFA0TU1SMkg5RmFodTJkUy1oUzNHV254UEdxbGdqZWZlZjljZCpRVE5IckRLd1UxTll5QVUqNS1HVEh0ZEI3aVV6Zk40RU8tVlJzTm1rbWt6YkdpUEFZRXUyMnZ6cGRSVnd2dWhZWWlmeDRqRVhPdDFyY25zVG5mWXNRWmxPYWFCd09XWDh1bEotSDl4TDhjWnpnRTh0enI1dy1XKlc3ZyUyMiUyQyUyMnZlcmlmeV9ldmVudCUyMiUzQSUyMnR0X3Nzb19hY2NvdW50X2xvZ2luJTIyJTJDJTIyZnAlMjIlM0ElMjJ2ZXJpZnlfbHdlejN5ZmdfYmM3YWIxMTRfNGMwOF9lNmU2XzNmZWFfNDc1NzIxM2EwNzE3JTIyJTJDJTIyc2VydmVyX3Nka19lbnYlMjIlM0ElMjIlN0IlNUMlMjJpZGMlNUMlMjIlM0ElNUMlMjJsZiU1QyUyMiUyQyU1QyUyMnJlZ2lvbiU1QyUyMiUzQSU1QyUyMkNOJTVDJTIyJTJDJTVDJTIyc2VydmVyX3R5cGUlNUMlMjIlM0ElNUMlMjJwYXNzcG9ydCU1QyUyMiU3RCUyMiUyQyUyMmxvZ19pZCUyMiUzQSUyMjIwMjQwNTIwMjA1OTM4RTlGN0U5REJGQ0I3Rjc4OTE5NzclMjIlMkMlMjJpc19hc3Npc3RfbW9iaWxlJTIyJTNBZmFsc2UlMkMlMjJpc19jb21wbGV4X3NtcyUyMiUzQWZhbHNlJTJDJTIyaWRlbnRpdHlfYWN0aW9uJTIyJTNBJTIyJTIyJTJDJTIyaWRlbnRpdHlfc2NlbmUlMjIlM0ElMjIlMjIlMkMlMjJ2ZXJpZnlfc2NlbmUlMjIlM0ElMjJwYXNzcG9ydCUyMiUyQyUyMmxvZ2luX3N0YXR1cyUyMiUzQTAlMkMlMjJhaWQlMjIlM0EwJTJDJTIybWZhX2RlY2lzaW9uJTIyJTNBJTIyJTIyJTdE'
url=...
with sync_playwright() as p:
browser = p.chromium.launch(headless=False, args=['--disable-blink-features=AutomationControlled'])
page = browser.new_page()
page.goto(url)
page.wait_for_timeout(2000) # 滑块加载太慢了等待5秒防止异常
# ddddocr识别
slide_img_bytes = get_img_bytes_by_htmltag_selector(page, "img#captcha-verify_img_slide")
target_img_bytes = get_img_bytes_by_htmltag_selector(page, "img#captcha_verify_image")
x = get_distance_by_ddddocr(slide_img_bytes, target_img_bytes)
# 计算实际x轴位置
selector = "div.captcha-slider-btn"
slide_element = page.wait_for_selector(selector)
slide_element_pos = slide_element.bounding_box()
width = slide_element_pos['width']
height = slide_element_pos['height']
x = x / (width / height) # 计算实际位置,x/宽高比
# 开始滑动
mouse = page.mouse
mouse.move(slide_element_pos['x'], slide_element_pos['y'])
page.wait_for_timeout(200)
mouse.down()
mouse.move(slide_element_pos['x'] + x, slide_element_pos['y'], steps=random.randint(10, 15))
page.wait_for_timeout(200)
mouse.up()
time.sleep(60)
browser.close()