利用puppeteer破解极验的滑动验证

目录
  1. 1. 基本的流程
  2. 2. 代码实现
  3. 3. 运行
  4. 4. 演示
  5. 5. 说明

基本的流程

  1. 打开前端网,点击登录。
  2. 填写账号,密码。
  3. 点解验证按钮,通过滑动验证,最后成功登陆。

代码实现

github上可以checkout。

run.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6 Plus'];
let timeout = function (delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(1)
} catch (e) {
reject(0)
}
}, delay);
})
}

let page = null
let btn_position = null
let times = 0 // 执行重新滑动的次数
const distanceError = [-10,2,3,5] // 距离误差

async function run() {
const browser = await puppeteer.launch({
headless:false //这里我设置成false主要是为了让大家看到效果,设置为true就不会打开浏览器
});
page = await browser.newPage();

// 1.打开前端网
await page.emulate(iPhone);
await page.goto('https://www.qdfuns.com/');
await timeout(1000);

// 2.打开登录页面
page.click('a[data-type=login]')
await timeout(1000);

// 3.输入账号密码
page.type('input[data-type=email]','你的账号')
await timeout(500);
page.type('input[placeholder=密码]','你的密码')
await timeout(1000);

// 4.点击验证
page.click('.geetest_radar_tip')
await timeout(1000);

btn_position = await getBtnPosition();

// 5.滑动
drag(null)
}

/**
* 计算按钮需要滑动的距离
* */
async function calculateDistance() {
const distance = await page.evaluate(() => {

// 比较像素,找到缺口的大概位置
function compare(document) {
const ctx1 = document.querySelector('.geetest_canvas_fullbg'); // 完成图片
const ctx2 = document.querySelector('.geetest_canvas_bg'); // 带缺口图片
const pixelDifference = 30; // 像素差
let res = []; // 保存像素差较大的x坐标

// 对比像素
for(let i=57;i<260;i++){
for(let j=1;j<160;j++) {
const imgData1 = ctx1.getContext("2d").getImageData(1*i,1*j,1,1)
const imgData2 = ctx2.getContext("2d").getImageData(1*i,1*j,1,1)
const data1 = imgData1.data;
const data2 = imgData2.data;
const res1=Math.abs(data1[0]-data2[0]);
const res2=Math.abs(data1[1]-data2[1]);
const res3=Math.abs(data1[2]-data2[2]);
if(!(res1 < pixelDifference && res2 < pixelDifference && res3 < pixelDifference)) {
if(!res.includes(i)) {
res.push(i);
}
}
}
}
// 返回像素差最大值跟最小值,经过调试最小值往左小7像素,最大值往左54像素
return {min:res[0]-7,max:res[res.length-1]-54}
}
return compare(document)
})
return distance;
}

/**
* 计算滑块位置
*/
async function getBtnPosition() {
const btn_position = await page.evaluate(() => {
const {clientWidth,clientHeight} = document.querySelector('.geetest_popup_ghost')
return {btn_left:clientWidth/2-104,btn_top:clientHeight/2+59}
})
return btn_position;
}

/**
* 尝试滑动按钮
* @param distance 滑动距离
* */
async function tryValidation(distance) {
//将距离拆分成两段,模拟正常人的行为
const distance1 = distance - 10
const distance2 = 10

page.mouse.click(btn_position.btn_left,btn_position.btn_top,{delay:2000})
page.mouse.down(btn_position.btn_left,btn_position.btn_top)
page.mouse.move(btn_position.btn_left+distance1,btn_position.btn_top,{steps:30})
await timeout(800);
page.mouse.move(btn_position.btn_left+distance1+distance2,btn_position.btn_top,{steps:20})
await timeout(800);
page.mouse.up()
await timeout(4000);

// 判断是否验证成功
const isSuccess = await page.evaluate(() => {
return document.querySelector('.geetest_success_radar_tip_content') && document.querySelector('.geetest_success_radar_tip_content').innerHTML
})
await timeout(1000);
// 判断是否需要重新计算距离
const reDistance = await page.evaluate(() => {
return document.querySelector('.geetest_result_content') && document.querySelector('.geetest_result_content').innerHTML
})
await timeout(1000);
return {isSuccess:isSuccess==='验证成功',reDistance:reDistance.includes('怪物吃了拼图')}
}

/**
* 拖动滑块
* @param distance 滑动距离
* */
async function drag(distance) {
distance = distance || await calculateDistance();
const result = await tryValidation(distance.min)
if(result.isSuccess) {
await timeout(1000);
//登录
console.log('验证成功')
page.click('#modal-member-login button')
}else if(result.reDistance) {
console.log('重新计算滑距离录,重新滑动')
times = 0
await drag(null)
} else {
if(distanceError[times]){
times ++
console.log('重新滑动')
await drag({min:distance.max,max:distance.max+distanceError[times]})
} else {
console.log('滑动失败')
times = 0
run()
}
}
}

run()

package.json

1
2
3
4
5
6
7
8
{
"name": "demo",
"version": "1.0.0",
"dependencies": {
"puppeteer": "^1.0.0"
}
}

运行

  1. 将这个两个文件保存到文件夹下面,终端切换到当前路径下
  2. npm i
  3. 补上前端网的账号,密码
  4. node run

演示

下图演示可以分为四步:

  1. 打开登陆页面,输入事先写好的 账号密码
  2. 第一次拖动滑块提示“ 被怪兽吃了”,所以重新计算了新的图片的缺口距离。
  3. 第二,三次拖动提示 “没正确合拼”,所以重新拖动。
  4. 验证成功,登录

说明

  1. 滑动验证有三个canvas,其中只需要 classname为 geetest_canvas_fullbg 以及 geetest_canvas_bg 的进行像素差对比,从而判断缺口的位置。ps: 前者是完整背景图,后者是带缺口的背景图。

  1. 每个带缺口的图片都有一块误导的阴影,所以对比像素差的时候,计算出的距离分别是误导阴影以及缺口的。因此,滑动距离的取值,我取{min:res[0]-7,max:res[res.length-1]-54}。当缺口比误导阴影靠左时,min(距离最小值)值就是滑动距离,否则就是max(距离最大值)减去滑块宽度

  1. 滑动结果分三种情况:验证成功被吃了失败“被吃了”会重新请求图片,所以重新计算了距离再滑动;“失败”则重新滑动,如果执行4次依然失败,则重新run整个流程。