likes
comments
collection
share

突破滑块验证码,实现自动登陆!思路+代码

作者站长头像
站长
· 阅读数 33

问题引入

滑块验证码越来越常见,大多数网站不再使用简单的文本验证码,而使用趣味性更强、用户体验更好的拼图式的滑块验证码。

但身为自动化脚本爱好者,怎么能因为一道滑块验证码而退缩?本文以某网站中的滑块验证码为例,提供思路与代码。

步骤

1. 观察

观察,寻找网站中有关该验证码的一切信息,是产生思路的基础。

突破滑块验证码,实现自动登陆!思路+代码

1.1 简单观察

首先不看源码,多次刷新观察图片,能发现该滑块验证码有以下几个特点:

  1. 滑块验证码属于拼图类型,并且拼图边缘特征不是特别明显(不是简单的方框)
  2. 背景缺口处像素透明度发生变化,即RGBA中A通道值与周围不同
  3. 拼图的形状是保持不变的(没有出现凹凸方向改变的问题)

基于简单观察的结果,我们可以理出一个基本的方向:

  • 首先,背景缺口处像素的透明度不同于非缺口处的透明度,我们可以从通过遍历图像像素点,根据A通道值变化实现边缘识别(定位缺口位置)

  • 其次,拼图的形状保持不变,所以我们可以简化边缘识别定位缺口位置的操作为确定一个特殊的固定的透明点的位置来实现找到整个缺口的特征位置(如中心或左上角等等)

1.2 缺口观察

为了找出上文所说的“特殊的固定的透明点的位置”,我们要对缺口进行进一步观察分析。

首先将背景图片保存到本地,使用图像工具打开。

(我使用的是电脑自带的画图,比较方便查看每个像素点)

突破滑块验证码,实现自动登陆!思路+代码

因为图像像素遍历时,方向是:从上往下遍历像素行,每一行从左往右遍历像素点

所以,可以构建一个坐标系,以左上角为原点,向右为x轴,向下为y轴。

根据图像中的透明像素分布的特点,我们自然就会想到两个相对简单的确定位置的方案:

  1. 找到上方突出的角顶端的中点,将其x坐标减去拼图宽度的一半,y坐标不变,即可得到缺口左上角坐标(即图中的鼠标所在的黑色十字)
  2. 找到右侧突出的角顶端的中点,将其x坐标减去拼图宽度,y坐标减去拼图高度的一半,即可得到缺口左上角坐标

显然根据遍历顺序来看,方案1实现起来比较简单:

因为第一个透明像素点行的中点,就是这个突出的角的中点。

再想想,因为拼图形状不变,所以我们完全可以找到第一个透明像素点,然后x坐标向右偏移固定的值,就是我们要找的中点。

综上,我们可以找到第一个透明像素点,然后x坐标向左偏移一个固定值,即可得到缺口的左上角坐标

1.3 源码观察

观察源码是期望能从源码中找出滑块验证码的刷新验证逻辑,方便使用代码来模拟这个流程。

右键滑块,审查元素,从而定位到滑块验证码相关的HTML代码

突破滑块验证码,实现自动登陆!思路+代码

可以看到滑块验证码相关元素的信息,发现滑块和背景图都带有id

既然带有id那么js代码中基本上会使用其id获取元素,进而对这些元素进行操作!

所以我们在源代码一栏中搜索滑块的id,发现能轻易定位到相关代码

(只能说这个网页开发者比较心大,相关代码直接暴露在源网页中,甚至直接就是全局变量)

突破滑块验证码,实现自动登陆!思路+代码

根据函数名和函数内容可以得知该函数是用来刷新验证码的,既然他为我们“提供”了刷新函数,那么我们之后就不用自己模拟Ajax请求来刷新验证码了,直接调用该函数即可。

接下来我们寻找验证逻辑

在原网页中没看到验证相关的代码,但是可以看到左侧工作栏中有drag.js文件,显然这个名字十有八九和滑块验证码有关

一览代码,就差把验证两字写注释里了。

突破滑块验证码,实现自动登陆!思路+代码

具体的代码可以点击此处

研究一番,发现这个验证属于一种非常简陋的验证,大体思路就是:

  1. 为滑块添加mousedown, mousemove, mouseup监听(同时对touchstart等手指触碰也做了相应的监听)
  2. mousedown时记录起始位置,mouseup记录终止位置,从而得到水平移动距离mousemove并没有做任何记录)
  3. mouseup时发送验证请求到后端(包含起始和终止位置信息),后端返回验证结果

既然这样的验证逻辑完全只考虑水平移动距离,我们用于突破验证码的自动化脚本就省事多了。

2. 总结思路

根据上述观察的内容,并在观察的过程中不断思考,我们可以总结出思路:

  1. 通过刷新验证码获取滑块验证码背景图片链接(因为网页并没有直接加载验证码)
  2. 通过对图片进行像素遍历,找到其左上角的x坐标,即与水平移动距离相关的值
  3. 模拟验证过程

2.1 验证思路

对于3.模拟验证过程,也有两种不同的思路

  1. 通过模拟Ajax请求实现模拟验证过程

该方法在本例中有显而易见的优势,因为该网页的验证码仅仅是将滑块的开始结束位置记录得到移动距离,将其发送到后端实现验证。数据非常简单,模拟请求不仅快速而且操作简单,前后端实现都适合

  1. 通过模拟拖拽滑块实现模拟验证过程

这种方式对于纯前端实现并不友好(比如油猴脚本),因为模拟拖拽要发送自定义的MouseEvent事件到滑块中,代码复杂,并且速度较慢。但对于比较完善的滑块验证码(验证逻辑与mousemove有关的)就不得不使用这种方式了。

如果使用puppeteer, selenium等控制浏览器进行操作的自动化脚本,那么难度不大。对于比较完善的滑块验证码也可以通过设置拖动的速度、方向偏移等实现比较“拟人”的模拟拖拽。

当然对于本例,只要拖动的距离对了,验证就能通过。

3. 代码实现

根据本例滑块验证码的验证逻辑特性,本代码实现以油猴脚本的方式来编写,突出一个纯前端实现自动登陆。当然其代码也可以作为其他思路的参考代码

3.1 刷新验证码部分

简单的方式,可以通过模拟请求验证码,然后根据请求相应得到背景图片的链接,再进入下一步。

但是,通过监听验证码刷新时对背景图片链接的修改,来得知验证码刷新完成,进而进入下一步处理,更加花里胡哨,更加符合用户操作逻辑

所以该部分使用MutationObserver观察者来实现。

首先,我们需要的是一个防抖下的处理程序handler,因为网页的验证码可能会被频繁刷新,要避免处理程序随之多次触发

    // 防抖下的滑块处理程序
    const handler = debounce((imgURL) => {
       //在此处编写对imgURL的处理逻辑
    }, 500)

    // 防抖函数工厂
    function debounce(fn, delay) {
        // 闭包实现防抖
        let timer = null
        return function () {
            let context = this, args = arguments
            // 如果事件被触发,清除timer并重新开始计时
            clearTimeout(timer)
            timer = setTimeout(function () {
                fn.apply(context, args)
            }, delay);
        }
    }

接下来就是对图片变化启动监听,就完成了刷新验证码部分的代码

    // 监听滑块验证码图片的变化
    const observe = new MutationObserver(mutations => {
        if (mutations[0].attributeName === 'src') {
            handler(mutations[0].target.src) //传递新链接给处理函数
        }
    });
    observe.observe(document.querySelector('#drag-captcha-bg>img'), {
        childList: false,
        attributes: true, // 观察属性变动
        subtree: false
    });

3.2 图片处理部分

我们需要得到的是该滑块的目标移动距离,下列代码得到了背景缺口左边的中点的坐标,那么坐标的x值即滑块的目标移动距离

值得注意的是,纯前端实现图片像素遍历有几个注意点:

  1. 要使用Image加载图片到canvas中,从而获取像素点数组
  2. 像素点数组是一维扁平的,每4个元素构成一个像素点的R, G, B, A,然后像素点在数组中的顺序就是上述的像素点遍历顺序(注意坐标的计算

具体代码如下:

    // 获取缺口左边中点坐标
    function getBlockPos(imgURL) {
        return new Promise((resolve) => {
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            const img = new Image()
            img.onload = () => {
                canvas.height = img.height
                canvas.width = img.width
                ctx.drawImage(img, 0, 0)

                //获取像素的一维数组
                let imgData = ctx.getImageData(0, 0, img.width, img.height)

                let x=0,y = 0
                for (let i = 0; i < imgData.data.length; i += 4) {
                    // RGBA分别为对应i,i+1,i+2,i+3
                    // 找出第一个半透明点,得到滑块区域左边中点
                    if (imgData.data[i + 3] < 255) {
                        let index = i / 4
                        x = index % img.width
                        y = (index - x) / img.width
                        x -= 17 // 左边
                        y += 20 // 中点
                        break
                    }
                }
                resolve([x, y])
            }
            img.src = imgURL
        })
    }

3.3 请求验证部分

这一部分基本照搬了网页的源码,没什么难度就偷个懒

    // 发送请求验证滑块验证码(用到的各种对象来自网站的其他js文件)
    function verifySliderCaptcha(distance) {
        var verifyRequest = $.post(
            "/wengine-auth/login/verify", {
                w: distance,
                t: 0,
                locations: [{ 'x': 156, 'y': 572 }, { 'x': 156 + distance, 'y': 479 }]
                // 其他坐标不会被验证,所以都用固定值,关键是 distance 要正确
            })

        verifyRequest.done(function (data) {
            if (data.success) {
                verifySuccess()
            } else {
                verifyFailed()
            }
        })

        verifyRequest.fail(function (error) {
            console.log(error)
            verifyFailed()
        })
    }

    function verifyFailed() {
        $(".drag-slide-message").addClass("error-message")
        $(".drag-slide-message").text("验证失败,请重试")
        $(".drag-slide-message").css("display", "block")
        setTimeout(() => initCaptcha(), 200)
    }

    function verifySuccess() {
        $(".drag-slide-message").addClass("succes-message")
        $(".drag-slide-message").text("验证成功")
        $(".drag-slide-message").css("display", "block")
        layer.close(layer.index)
        $("button#login").click();
    }
})();

3.4 油猴脚本代码总览

// ==UserScript==
// @name         厦门大学WebVPN自动登陆
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  厦门大学WebVPN登陆!自动滑块验证+登陆
// @author       ruchuby
// @match        https://applg.xmu.edu.cn/wengine-auth/*
// ==/UserScript==

(function() {
    'use strict';

    //===============================配置====================================
    const config = {
        autoLogin: true, //是否自动输入账号密码并登陆,false时后两项可以不填,仍然会自动滑块验证
        id: '你的账号',
        pw: '你的密码'
    }
    //=======================================================================

    // 防抖下的滑块处理程序
    const handler = debounce((imgURL) => {
        getBlockPos(imgURL).then(([destX, destY]) => verifySliderCaptcha(destX))
            .then(() => console.log('验证成功')).catch(err => console.error(err))
    }, 500)

    // 监听滑块验证码图片的变化
    const observe = new MutationObserver(mutations => {
        if (mutations[0].attributeName === 'src') {
            handler(mutations[0].target.src)
        }
    });
    observe.observe(document.querySelector('#drag-captcha-bg>img'), {
        childList: false,
        attributes: true, // 观察属性变动
        subtree: false
    });

    if (config.autoLogin){
        // 尝试登陆(可选)
        document.querySelector('#user_name').value = config.id
        document.querySelector('.password-input>input').value = config.pw
        initCaptcha() //网页自带的函数
    }

    // 防抖函数工厂
    function debounce(fn, delay) {
        // 闭包实现防抖
        let timer = null
        return function () {
            let context = this, args = arguments
            // 如果事件被触发,清除timer并重新开始计时
            clearTimeout(timer)
            timer = setTimeout(function () {
                fn.apply(context, args)
            }, delay);
        }
    }

    //==========================================================================
    // 获取滑块区域左边中点坐标
    function getBlockPos(imgURL) {
        return new Promise((resolve) => {
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            const img = new Image()
            img.onload = () => {
                canvas.height = img.height
                canvas.width = img.width
                ctx.drawImage(img, 0, 0)

                //获取像素的一维数组
                let imgData = ctx.getImageData(0, 0, img.width, img.height)

                let x=0,y = 0
                for (let i = 0; i < imgData.data.length; i += 4) {
                    // RGBA分别为对应i,i+1,i+2,i+3
                    // 找出第一个半透明点,得到滑块区域左边中点
                    if (imgData.data[i + 3] < 255) {
                        let index = i / 4
                        x = index % img.width
                        y = (index - x) / img.width
                        x -= 17 // 左边
                        y += 20 // 中点
                        break
                    }
                }
                resolve([x, y])
            }
            img.src = imgURL
        })
    }

    //============================================================================================
    // 发送请求验证滑块验证码(用到的各种对象来自网站的其他js文件)
    function verifySliderCaptcha(distance) {
        var verifyRequest = $.post(
            "/wengine-auth/login/verify", {
                w: distance,
                t: 0,
                locations: [{ 'x': 156, 'y': 572 }, { 'x': 156 + distance, 'y': 479 }] // 数字不重要,关键是 distance
            })

        verifyRequest.done(function (data) {
            if (data.success) {
                verifySuccess()
            } else {
                verifyFailed()
            }
        })

        verifyRequest.fail(function (error) {
            console.log(error)
            verifyFailed()
        })
    }

    function verifyFailed() {
        $(".drag-slide-message").addClass("error-message")
        $(".drag-slide-message").text("验证失败,请重试")
        $(".drag-slide-message").css("display", "block")
        setTimeout(() => initCaptcha(), 200)
    }

    function verifySuccess() {
        $(".drag-slide-message").addClass("succes-message")
        $(".drag-slide-message").text("验证成功")
        $(".drag-slide-message").css("display", "block")
        layer.close(layer.index)
        $("button#login").click();
    }
})();

结束语

本文就到此结束了,希望大家有所收获,欢迎点赞评论关注。

如果文中有不对的地方,或是大家有不同的见解,欢迎指出。

转载自:https://juejin.cn/post/7156818996178731044
评论
请登录