实战复盘:我是如何用浏览器调试搞定PDD滑块验证码的(附完整JS调用流程)
浏览器调试实战:动态破解PDD滑块验证码的技术解析
第一次遇到PDD的滑块验证码时,我正尝试批量查询一些商品数据。页面突然弹出那个熟悉的拼图块,要求我拖动滑块完成验证。作为开发者,我的第一反应不是老老实实滑动,而是打开了Chrome开发者工具——这背后究竟是怎样一套验证机制?经过三天的调试与分析,我终于摸清了从触发验证到最终通过的完整流程。本文将分享如何仅用浏览器开发者工具,无需复杂逆向工程,动态追踪PDD滑块验证码的核心逻辑。
1. 环境准备与验证触发
要分析滑块验证码,首先需要稳定触发它。PDD的风控系统会在检测到异常行为时弹出验证,比如频繁刷新页面、使用自动化工具等。我通过以下方式确保验证码稳定出现:
- 清除浏览器缓存后首次登录
- 连续快速刷新商品页5-7次
- 使用隐身模式避免cookie干扰
关键点:验证码出现后,立即在Network面板开启"Preserve log"选项,防止请求记录被清除。同时勾选"Disable cache",确保每次都是全新请求。
验证码触发后,会先后发起三个关键请求:
vc_pre_ck_b- 获取初始验证参数obtain_captcha- 获取加密的滑块图片user_verify- 提交验证结果
注意:所有PDD验证接口都包含
anti_content参数,这是其反爬机制的核心,需要特别关注其生成位置。
2. 关键参数定位与分析
2.1 verifyAuthToken的获取
第一个关键参数verifyAuthToken出现在初始请求的响应中。通过以下步骤可以捕获它:
- 在Network面板过滤
vc_pre_ck_b接口 - 查看响应JSON中的
verify_auth_token字段 - 复制该值用于后续请求
// 示例响应结构 { "success": true, "result": { "verify_auth_token": "a1b2c3d4e5...", "salt": "5f8d3a..." } }2.2 salt与加密参数的关联
salt参数是后续加密的基础,它会用于生成AES密钥和初始向量。通过调试发现:
salt在vc_pre_ck_b接口返回- 通过固定算法转换为
aes_key和aes_iv - 这两个参数用于加密最终的验证数据
在Console中执行以下代码可以观察转换过程:
function generateKeys(salt) { const key = CryptoJS.MD5(salt + 'key_suffix').toString(); const iv = CryptoJS.MD5(salt + 'iv_suffix').toString().substr(0, 16); return { key, iv }; }3. 动态调试captcha_collect生成
captcha_collect是整个验证过程中最关键的参数,它包含了滑块轨迹、时间戳等验证信息。通过以下步骤定位其生成逻辑:
3.1 打断点追踪
- 在Sources面板搜索
captcha_collect - 找到包含该参数的请求初始化代码
- 在相关行设置断点并重新触发验证
当断点触发时,调用栈会显示完整的生成路径。我发现核心逻辑在getAntiToken方法中:
getAntiToken: function() { var rawData = collectBrowserEnv(); var encrypted = aesEncrypt(rawData, aesKey, aesIv); return encrypted; }3.2 数据组成分析
通过调试器可以查看被加密的原始数据rawData的结构:
{ "screenWidth": 1920, "screenHeight": 1080, "plugins": "Chrome PDF Viewer...", "timezone": 8, "timestamp": 1630000000000, "moveTrack": [[x1,y1,t1],[x2,y2,t2],...] }提示:PDD会验证部分浏览器环境参数,但实际测试发现大多数字段可以使用固定值。
4. 图片解密与滑块模拟
4.1 获取加密图片
obtain_captcha接口返回两张Base64编码的图片:
- 完整背景图
- 滑块拼图块
这些图片并非标准Base64,而是经过自定义混淆。通过调试发现解密方法隐藏在decodeImage函数中:
function decodeImage(encoded) { const key = 'pdd_special_key'; let decoded = ''; for(let i=0; i<encoded.length; i++) { decoded += String.fromCharCode(encoded.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return decoded; }4.2 计算滑块位置
获取清晰图片后,需要计算滑块应该移动的距离。通过像素比对可以找到缺口位置:
- 使用Canvas API加载两张图片
- 逐像素比对差异
- 找到差异最大的x坐标
const findGap = (bg, slider) => { const ctx = bg.getContext('2d'); const sliderCtx = slider.getContext('2d'); let maxDiff = 0; let gapX = 0; for(let x=0; x<bg.width; x++) { let diff = 0; for(let y=0; y<bg.height; y++) { const bgPixel = ctx.getImageData(x,y,1,1).data; const sliderPixel = sliderCtx.getImageData(x,y,1,1).data; diff += Math.abs(bgPixel[0]-sliderPixel[0]); } if(diff > maxDiff) { maxDiff = diff; gapX = x; } } return gapX; };5. 完整验证流程组装
将所有参数组合起来,形成完整的验证请求:
- 获取
verify_auth_token和salt - 生成
aes_key和aes_iv - 收集浏览器环境数据
- 模拟滑块移动轨迹
- 加密生成
captcha_collect - 提交验证请求
async function completeVerification() { const { verify_auth_token, salt } = await getInitialToken(); const { key, iv } = generateKeys(salt); const envData = collectEnvData(); const track = simulateMoveTrack(gapPosition); const raw = JSON.stringify({ ...envData, moveTrack: track }); const captcha_collect = aesEncrypt(raw, key, iv); const result = await submitVerification({ verify_auth_token, captcha_collect, verify_code: gapPosition }); return result.code === 0; }在实际项目中,我封装了一个自动处理验证码的模块,核心思路是重用相同的环境参数和加密密钥,只在每次验证时生成新的轨迹数据。这样既避免了频繁的密钥获取请求,又保证了每次验证数据的唯一性。
