Selenium模拟登录滑块验证:轨迹模拟与风控绕过实战
1. 滑块验证到底在防谁先搞懂对手再动手做Web自动化的人迟早会撞上滑块验证这道墙。尤其是模拟登录电商平台这类场景账号密码填完、点击登录页面突然弹出一个缺口滑块要求你把拼图拖到正确位置。这时候如果还用传统的send_keys加click那套打法基本就是原地卡死——元素定位不到或者拖过去直接被判定为机器操作验证失败。我最早接触这个问题的时候思路很朴素不就是拖拽吗Selenium有ActionChainsdrag_and_drop一把梭。结果实测下来十次有十次失败。后来才明白滑块验证背后是一整套行为风控体系它检测的不只是你有没有把滑块拖到缺口位置而是你拖动的方式像不像人。这就引出了本文要解决的核心问题用Selenium模拟登录时如何让滑块拖拽行为通过平台的风控检测。关键词里提到的解决登陆滑块的问题本质上不是技术上的能不能拖而是拖得够不够像人。这篇文章适合几类人看一是刚接触Selenium自动化、想跑通登录流程的新手二是有一定经验、但滑块环节总是失败的中级开发者三是对行为风控机制感兴趣、想理解背后原理的技术爱好者。我会从滑块的检测原理讲起然后给出完整的实操方案包括轨迹生成、速度控制、环境伪装这些关键环节最后分享几个我踩过的坑和对应的解法。需要提前说明的是本文讨论的技术方案仅用于学习研究和自动化测试场景请勿用于任何违反平台服务条款的用途。自动化操作应当遵守目标网站的使用协议这是基本的技术伦理。2. 滑块验证的检测逻辑它到底在看什么2.1 三层检测机制拆解很多人以为滑块验证就是比对最终位置其实远不止。根据我多次实测和查阅相关资料的经验主流的滑块验证至少包含三层检测第一层是位置检测。这个最直观你拖动滑块后系统会比对滑块最终停留位置和缺口位置的偏差。偏差在允许范围内通常是几个像素就算位置正确。这一层最容易过只要图像识别到位算出缺口坐标就行。第二层是轨迹检测。这是核心。系统会记录你从按下鼠标到松开鼠标的整个过程中鼠标移动的轨迹数据——包括每个时间点的坐标、移动速度、加速度、是否有停顿、是否有回退。人类的拖动轨迹有几个特征起步慢、中间快、接近目标时减速、轨迹有轻微抖动、y轴会有小幅波动。而程序化的drag_and_drop是一条直线、匀速、没有任何抖动一眼就能被识别出来。第三层是环境检测。这一层最隐蔽。系统会检测浏览器环境的各种参数navigator.webdriver是否为true、浏览器指纹是否异常、是否有Selenium相关的特征残留、鼠标事件的isTrusted属性等。如果环境检测没过哪怕轨迹再像人也会被判定为机器。三层检测是叠加的任何一层出问题都会导致验证失败。所以解决滑块问题不能只盯着怎么拖要从环境、轨迹、位置三个维度同时下手。2.2 为什么传统drag_and_drop必死我拿ActionChains的drag_and_drop做过对比测试。用这种方式拖动滑块失败率接近100%。原因很简单# 这种写法基本必死 from selenium.webdriver.common.action_chains import ActionChains action ActionChains(driver) action.drag_and_drop(slider, target).perform()drag_and_drop底层是瞬间完成拖拽或者以固定步长匀速移动。它产生的鼠标事件序列极其规整mousedown → 若干次mousemove等间距→ mouseup。这种规整性恰恰是机器行为的典型特征。人类的鼠标移动不可能等间距也不可能匀速。更关键的是drag_and_drop往往只触发少数几个mousemove事件而真实的人类拖动会产生几十甚至上百个mousemove事件。事件数量的差异本身就是强特征。2.3 风控系统的判定阈值虽然各平台的具体阈值不公开但通过反复测试可以摸出一些规律。以下是我实测总结的经验值仅供参考检测维度机器行为特征人类行为特征建议模拟方向移动轨迹直线、等间距曲线、间距不均加入y轴偏移移动速度匀速或瞬间完成先加速后减速变速曲线事件数量少于10个50-200个增加中间点停顿情况无停顿起步和结尾有微停顿加入随机延迟回退行为无偶尔小幅回退加入1-2次回退总耗时极短200ms800ms-3s控制在1-2秒这张表是我多次调试后总结的实际应用中需要根据目标平台的具体表现微调。核心思路是让程序产生的行为数据落在人类行为特征那一列。3. 环境伪装在拖滑块之前先把马甲穿好3.1 navigator.webdriver的隐藏Selenium启动的浏览器navigator.webdriver默认是true这是最明显的机器特征。隐藏它有几种方式我推荐用CDPChrome DevTools Protocol注入脚本比改配置文件更稳定from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) # 通过CDP注入脚本覆盖webdriver属性 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); })这里解释一下每个参数的作用。--disable-blink-featuresAutomationControlled是告诉浏览器不要暴露自动化控制的特征excludeSwitches里的enable-automation是去掉浏览器顶部那条Chrome正受到自动测试软件控制的提示条useAutomationExtension是禁用自动化扩展。最后通过CDP在页面加载前注入脚本把navigator.webdriver改成undefined。注意CDP注入的脚本必须在页面加载之前执行所以要用Page.addScriptToEvaluateOnNewDocument而不是普通的execute_script。后者在页面加载后执行可能已经晚了。3.2 其他容易被忽略的指纹除了webdriver还有几个属性容易被检测navigator.plugins真实浏览器有插件列表Selenium环境往往是空的navigator.languages语言列表真实环境通常是[zh-CN, zh]window.chromeChrome浏览器应该有这个对象navigator.permissions权限查询的返回结果我一般会把这些一起处理driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}); Object.defineProperty(navigator, languages, {get: () [zh-CN, zh]}); Object.defineProperty(navigator, plugins, {get: () [1, 2, 3, 4, 5]}); window.chrome {runtime: {}}; })这些伪装不是万能的但能过滤掉大部分基础检测。环境伪装是滑块验证的第一道门槛过不了这关后面的轨迹模拟做得再好也没用。3.3 窗口大小和User-Agent的一致性有个细节很多人不注意窗口大小和User-Agent要匹配。如果你用移动端的User-Agent但窗口是1920x1080的桌面尺寸这本身就是矛盾。反过来也一样。我一般统一用桌面端的配置options.add_argument(--window-size1920,1080) options.add_argument(user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36)User-Agent里的Chrome版本最好和你实际安装的浏览器版本接近差太多也会引起怀疑。这个可以通过driver.capabilities[browserVersion]获取实际版本后再拼接。4. 缺口识别把滑块位置算准4.1 图像获取的两种方式要拖动滑块首先得知道缺口在哪。获取验证码图片有两种方式方式一直接截图元素。用Selenium对验证码图片元素截图保存为本地文件。这种方式简单但受限于页面渲染有时候截出来的图不完整或者被遮挡。方式二从页面源码提取base64。很多平台的验证码图片是以base64编码嵌在HTML里的直接从src属性提取解码即可。这种方式拿到的图片最干净。import base64 from selenium.webdriver.common.by import By # 方式二示例 img_element driver.find_element(By.CSS_SELECTOR, .验证码图片的选择器) src img_element.get_attribute(src) # 去掉base64前缀 base64_data src.split(,)[1] img_bytes base64.b64decode(base64_data) with open(captcha.png, wb) as f: f.write(img_bytes)我一般优先用方式二拿到的图片质量更稳定。如果图片是动态加载的需要配合显式等待确保图片加载完成再提取。4.2 用OpenCV计算缺口偏移拿到图片后用OpenCV做缺口识别。核心思路是滑块验证通常有两张图一张是带缺口的背景图一张是滑块小图。通过模板匹配或者边缘检测找到缺口位置。import cv2 import numpy as np def get_gap_offset(bg_path, slider_path): bg cv2.imread(bg_path, 0) slider cv2.imread(slider_path, 0) # 边缘检测 bg_edge cv2.Canny(bg, 100, 200) slider_edge cv2.Canny(slider, 100, 200) # 模板匹配 result cv2.matchTemplate(bg_edge, slider_edge, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) # max_loc[0]就是缺口的x坐标 return max_loc[0]这段代码的逻辑是对背景图和滑块图分别做Canny边缘检测然后用matchTemplate做模板匹配找到匹配度最高的位置那个位置的x坐标就是缺口左边缘的坐标。实际使用中matchTemplate有时候会匹配到错误位置尤其是背景图干扰较多的时候。我的经验是加一个筛选如果匹配度max_val低于0.5就认为识别失败重新获取图片。另外有些平台的滑块图本身就是缺口形状这时候直接对背景图做边缘检测、找轮廓也行。4.3 偏移量的换算算出来的max_loc[0]是图片上的像素坐标但实际拖动距离需要换算。因为网页上显示的图片尺寸和实际图片尺寸可能不一致存在缩放比例。# 获取网页上图片的显示宽度 display_width img_element.size[width] # 获取实际图片的宽度 actual_width bg.shape[1] # 缩放比例 scale display_width / actual_width # 实际需要拖动的距离 drag_distance max_loc[0] * scale这个换算很容易被忽略。如果不做换算拖动距离就会偏差导致验证失败。我踩过这个坑当时怎么调轨迹都过不了后来发现是缩放比例没算对。提示有些平台的滑块本身有初始偏移计算拖动距离时要减去滑块的初始位置。这个初始位置可以通过slider.location[x]获取。5. 轨迹模拟让程序拖得像个真人5.1 为什么匀速直线必死前面说过人类的拖动轨迹不是匀速直线。具体来说人类拖动滑块时的速度曲线大致是这样的起步阶段速度从0逐渐加快中间阶段达到最大速度接近目标时速度逐渐降低到0。这个曲线类似一个钟形。如果用匀速拖动速度曲线是一条水平线和人类的钟形曲线差异巨大。风控系统只要对比速度曲线的形状就能判定是机器。所以轨迹模拟的核心是生成一条符合人类速度特征的轨迹。5.2 用缓动函数生成轨迹我常用的方案是用缓动函数easing function来生成轨迹。缓动函数本来是动画领域的概念用来让动画看起来更自然。这里借用它的思想生成先加速后减速的位移曲线。import numpy as np def ease_out_quad(t): 二次缓出函数先快后慢 return 1 - (1 - t) ** 2 def generate_track(distance, duration1.5, steps100): 生成拖动轨迹 track [] for i in range(steps): t i / steps # 用缓动函数计算当前进度 progress ease_out_quad(t) # 当前位移 current distance * progress # 计算增量 if i 0: move current else: move current - track[-1][0] # 加入随机抖动 move np.random.uniform(-0.5, 0.5) # 加入y轴偏移 y_offset np.random.uniform(-1, 1) track.append((round(move, 2), round(y_offset, 2))) return track这段代码生成的轨迹位移增量是先大后小的符合人类拖动的速度特征。同时加入了x轴和y轴的随机抖动让轨迹不是完美的直线。ease_out_quad是二次缓出特点是开始快、结尾慢。如果你想要更接近人类的效果可以用ease_in_out_quad先加速后减速或者用贝塞尔曲线。我实测下来ease_out_quad配合随机抖动已经能过大部分平台。5.3 用ActionChains逐段执行生成轨迹后用ActionChains逐段执行。注意不能用drag_and_drop要手动控制click_and_hold、move_by_offset、releasefrom selenium.webdriver.common.action_chains import ActionChains import time def drag_slider(driver, slider, track): action ActionChains(driver) # 按住滑块 action.click_and_hold(slider).perform() time.sleep(np.random.uniform(0.1, 0.3)) # 起步停顿 for move_x, move_y in track: action ActionChains(driver) action.move_by_offset(move_x, move_y).perform() # 每步之间加入随机延迟 time.sleep(np.random.uniform(0.005, 0.02)) # 结尾停顿 time.sleep(np.random.uniform(0.1, 0.2)) # 松开 action ActionChains(driver) action.release().perform()这里有几个关键点第一每步都要新建ActionChains对象。如果复用同一个对象move_by_offset的偏移量会累加导致轨迹错乱。这是很多人踩的坑。第二每步之间加随机延迟。延迟时间在5ms到20ms之间随机模拟人类鼠标移动的时间间隔。如果延迟固定又变成机器特征了。第三起步和结尾加停顿。人类按下鼠标后不会立刻移动会有一个短暂的反应时间松开前也会有一个微小的停顿。这两个停顿加上后轨迹的真实度会明显提升。5.4 加入回退和过冲人类拖动滑块时经常会出现拖过头再拉回来的情况。这种过冲和回退行为是机器很少有的加入后能显著提升通过率。def add_overshoot(track, overshoot_pixels5): 在轨迹末尾加入过冲和回退 # 过冲多拖几个像素 track.append((overshoot_pixels, 0)) # 回退拉回来 track.append((-overshoot_pixels, 0)) return track过冲的像素数一般控制在3到8个像素之间太小看不出来太大可能直接拖出边界。回退的像素数和过冲相同方向相反。我实测下来加入过冲回退后通过率大概能提升20%左右。这个行为的心理依据是人类在瞄准目标时往往会先超过目标再修正这是肌肉控制的自然特征。6. 完整流程串联与参数调优6.1 从打开页面到登录成功的完整链路把前面的环节串起来完整的流程是这样的import time import numpy as np from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def login_with_slider(username, password): # 1. 环境伪装 options Options() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}); Object.defineProperty(navigator, languages, {get: () [zh-CN, zh]}); Object.defineProperty(navigator, plugins, {get: () [1, 2, 3, 4, 5]}); window.chrome {runtime: {}}; }) # 2. 打开登录页 driver.get(目标登录页地址) time.sleep(np.random.uniform(2, 4)) # 模拟人类打开页面后的停顿 # 3. 输入账号密码 # 注意这里的选择器需要根据实际页面调整 username_input WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, username)) ) # 模拟人类输入逐字符输入并加延迟 for char in username: username_input.send_keys(char) time.sleep(np.random.uniform(0.05, 0.15)) password_input driver.find_element(By.ID, password) for char in password: password_input.send_keys(char) time.sleep(np.random.uniform(0.05, 0.15)) # 4. 点击登录 login_btn driver.find_element(By.CSS_SELECTOR, .登录按钮选择器) login_btn.click() time.sleep(np.random.uniform(1, 2)) # 5. 等待滑块出现 slider WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, .滑块选择器)) ) # 6. 获取验证码图片并计算缺口 # 这里省略图片获取和缺口识别的代码参考第4节 distance calculate_gap_distance(driver) # 7. 生成轨迹并拖动 track generate_track(distance) track add_overshoot(track) drag_slider(driver, slider, track) # 8. 等待验证结果 time.sleep(np.random.uniform(2, 3)) return driver这个流程里每一步都加入了随机延迟模拟人类的操作节奏。账号密码的输入也是逐字符输入而不是一次性send_keys因为一次性输入的速度太快也是机器特征。6.2 参数调优的经验值轨迹模拟涉及多个参数这些参数没有标准答案需要根据目标平台的表现调整。以下是我总结的经验值范围参数建议范围说明总拖动时长1.0-2.5秒太短像机器太长可能超时轨迹步数80-150步步数太少轨迹粗糙太多执行慢每步延迟5-20ms随机分布不要固定x轴抖动±0.5像素幅度不宜过大y轴偏移±1像素模拟手部自然抖动起步停顿100-300ms按下后的反应时间结尾停顿100-200ms松开前的确认时间过冲像素3-8像素拖过头再拉回这些参数不是孤立的要配合调整。比如总时长增加了每步延迟也要相应调整保证步数和时长匹配。6.3 失败重试机制滑块验证不可能100%一次通过需要设计重试机制。但重试不是简单重复每次重试要调整参数def drag_with_retry(driver, slider, distance, max_retry3): for attempt in range(max_retry): # 每次重试调整参数 duration np.random.uniform(1.0, 2.5) steps np.random.randint(80, 150) track generate_track(distance, duration, steps) if attempt 0: # 重试时加入过冲回退 track add_overshoot(track, np.random.randint(3, 8)) drag_slider(driver, slider, track) time.sleep(np.random.uniform(2, 3)) # 检查是否通过 if check_slider_passed(driver): return True # 失败后刷新验证码 refresh_btn driver.find_element(By.CSS_SELECTOR, .刷新按钮选择器) refresh_btn.click() time.sleep(np.random.uniform(1, 2)) # 重新计算距离 distance calculate_gap_distance(driver) return False重试的关键是每次调整参数而不是用同样的参数重复。如果第一次失败是因为轨迹太规整第二次就要加入更多随机性如果是因为速度太快第二次就要放慢。另外失败后要刷新验证码因为缺口位置可能已经变了。7. 那些年我踩过的坑7.1 元素定位到了但拖不动这个问题困扰了我很久。滑块元素明明定位到了click_and_hold也执行了但滑块就是不动。后来发现原因是滑块元素上覆盖了另一个透明元素鼠标事件被上层元素拦截了。解决办法有两种一是用execute_script直接触发鼠标事件绕过元素层级二是找到真正接收鼠标事件的元素。我一般用第一种# 用JS直接触发mousedown driver.execute_script( var slider arguments[0]; var event new MouseEvent(mousedown, { bubbles: true, cancelable: true, view: window }); slider.dispatchEvent(event); , slider)但这种方式有个问题它触发的是合成事件isTrusted属性是false可能被风控识别。所以更稳妥的方式还是用ActionChains但要确保定位到正确的元素。7.2 轨迹执行到一半报错用move_by_offset逐段移动时有时候会报move target out of bounds错误。原因是偏移量累加后超出了浏览器窗口范围。这个问题的根源是ActionChains的偏移量是相对当前位置的如果前一步已经移动了很多下一步再移动就可能超出边界。解决办法是控制单步偏移量不要太大同时确保总偏移量不超过窗口宽度。另外前面提到过每步要新建ActionChains对象否则偏移量会累加。这个坑我踩过当时轨迹完全乱套排查了半天才发现是ActionChains复用的问题。7.3 验证码图片识别不准matchTemplate有时候会匹配到错误位置尤其是背景图有干扰的时候。我遇到过一种情况背景图里有一个和滑块形状相似的图案matchTemplate匹配到了那个图案导致拖动距离完全错误。解决办法是加验证如果匹配度低于阈值或者匹配位置明显不合理比如在图片最右侧就重新识别。另外可以结合多种识别方法比如同时用模板匹配和轮廓检测两者结果接近才采用。还有一种情况是图片有噪点或者干扰线。这时候可以先做图像预处理比如高斯模糊、二值化再去识别。预处理能显著提升识别准确率。7.4 通过了滑块但登录还是失败有时候滑块验证显示通过了但登录还是失败提示操作异常。这种情况通常是环境检测没过或者账号本身有风险。环境检测方面除了前面说的webdriver还要检查Cookie、LocalStorage是否有异常。有些平台会记录你之前的操作行为如果发现行为模式异常即使当次滑块过了也会拦截登录。账号风险方面如果账号是新注册的、或者异地登录、或者之前有过异常操作平台可能会加严验证。这种情况不是技术能解决的只能换账号或者养号。7.5 速度太快被识别我一开始调参数的时候总想着快把总时长压到0.5秒以内。结果通过率极低。后来把时长放到1.5秒左右通过率明显提升。人类的操作速度是有生理极限的。拖动一个滑块从按下到松开正常人的反应时间加上动作时间一般在1秒以上。如果程序在0.3秒内完成那肯定不是人。所以参数调优的第一原则是不要贪快。宁可慢一点也要像人。我现在的默认配置是总时长1.5秒起步停顿200ms结尾停顿150ms这个配置在大部分平台都能过。8. 进阶思路从对抗到理解8.1 行为风控的演进趋势滑块验证只是行为风控的一种形式。现在很多平台已经升级到了更复杂的验证方式比如点选验证、无感验证、行为序列分析等。点选验证要求你按顺序点击图片中的特定文字或图案无感验证则完全不弹窗后台根据你的行为数据判断是不是机器。这些新形式的共同特点是检测维度更多、更隐蔽。单纯靠模拟鼠标轨迹已经不够了还需要模拟键盘输入节奏、页面滚动行为、鼠标悬停行为等。8.2 用真实用户数据训练轨迹模型我有个想法但还没完整实现用真实用户的鼠标轨迹数据来训练一个轨迹生成模型。思路是收集大量真实用户的拖动轨迹提取特征速度曲线、抖动幅度、回退概率等然后用这些特征来生成轨迹。这个思路的理论依据是最像人的轨迹就是用人的数据生成的。如果能拿到足够多的真实轨迹样本生成的轨迹会比用缓动函数模拟的更自然。不过这个方案有两个难点一是真实轨迹数据不好获取二是不同平台的用户行为特征可能不同需要分平台训练。目前我还是用缓动函数加随机抖动的方案够用。8.3 什么时候该放弃自动化最后说一个现实问题不是所有场景都适合自动化。如果目标平台的风控极其严格或者自动化操作违反了平台的服务条款那就应该放弃。技术能力是一回事用在哪里是另一回事。自动化测试、学习研究是正当用途批量注册、爬取数据、恶意操作就超出了合理范围。作为技术人员心里要有一条线。我在实际项目中只在获得授权的测试环境或者自己的账号上做自动化验证。对于生产环境的自动化一定会先确认是否符合平台规则。这不是技术问题是职业操守问题。滑块验证的对抗本质上是攻防双方的持续博弈。今天能过的方案明天可能就失效了。所以重要的不是记住某个具体的参数或代码而是理解背后的原理风控在检测什么、为什么这样检测、如何让行为数据落在正常范围内。理解了这些面对新的验证形式时才能快速找到应对思路。