问题引入

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

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

进程

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();
    }
})();

完毕语

本文就到此完毕了,期望我们有所收获,欢迎点赞谈论关注。

如果文中有不对的当地,或是我们有不同的见地,欢迎指出。