极验4滑动拼图验证码逆向笔记

!QUOTE\] 参考文章 [爬虫逆向之极验四滑块(补环境秒杀)_极验四逆向-CSDN博客](https://blog.csdn.net/m0_64408930/article/details/155533409)

前言

本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

逆向地址

网址aHR0cHM6Ly9ndDQuZ2VldGVzdC5jb20v
接口

  1. captcha_id接口:https://gt4.geetest.com/assets/index.xxxxxxxx.js
  2. 请求验证码接口(load接口):https://gcaptcha4.geetest.com/load
  3. 验证验证码接口(verify接口):https://gcaptcha4.geetest.com/verify

逆向目标

通过补环境的方式实现加密参数w的还原

逆向分析

1.三个接口

首先分析一下这三个接口,看看请求和响应的参数是否动态

captcha_id接口https://gt4.geetest.com/assets/index.604047e9.js

这个接口返回的是一个js文件,文件里有我们需要的caprcha_id,这个参数需要传递给下面两个接口(请求验证码接口和验证验证码接口)当作请求参数,如下图所示:

请求验证码接口(load接口)https://gcaptcha4.geetest.com/load

请求参数

  1. callback:geetest_13位的时间戳
  2. captcha_id:由captcha_id接口返回
  3. challenge:uuid格式的数据
  4. client_type:web
  5. risk_type:slide,滑块类型
  6. lang:zh

响应数据(JSONP格式),有以下几个参数需要注意:

  1. bg:滑块背景图
  2. slice:滑块图片
  3. pow_detail:里面的4个参数均作为参数用于生成加密参数w
    • version
    • bits
    • hashfunc
    • datetime
  4. lot_number:传递给下一个接口当请求参数,并且传参用于加密参数w
  5. payload:传递给下一个接口当请求参数
  6. process_token:传递给下一个接口当请求参数

验证验证码接口(verify接口)https://gcaptcha4.geetest.com/verify

请求参数

  1. callback:geetest_13位的时间戳
  2. captcha_id:由captcha_id接口返回
  3. client_type:web
  4. risk_type:slide,滑块类型
  5. lot_number:由请求验证码接口返回
  6. payload:由请求验证码接口返回
  7. process_token:由请求验证码接口返回
  8. payload_protocol:1
  9. pt:1,作为参数用于生成加密参数w
  10. w:加密参数(1536位),接下去要分析的重点

2.加密参数w

接下去分析加密参数w,接着就是跟栈找加密参数的入口,在第一处地方打上断点,然后再次拖动滑块(不要成功),断点断住了,如下图所示:

接着往下跟栈,直到找到加密参数生成的位置,最终找到了加密的位置,如下图所示:

接着就是分析加密参数是由那个函数生成的,可以发现加密函数就在上面,如下图所示:

接着跟进函数,查看具体的加密逻辑,进去后在两个return处打上断点,点击执行,跳到了第二个断点,如下图所示:

然后查看返回的数据,看能不能对上,发现是1504位,能对应上,如下图所示:

既然能对应上,就可以把这个i函数导出到全局,接着复制所有代码到本地,然后补环境,在本地实现加密参数的生成,如下图所示:

然后发现报错了,如下图所示:

第2个参数

根据报错信息,发现是一个pt值没有,所以继续去浏览器中调试,找到响应的位置,如下图所示:

第1个参数

从上面的两个图中,可以看出少了入参里面的options参数的pt参数,所以在代码中补充完整,接着w参数就出来了,如下图所示:

经过对比,虽然本地生成的加密参数长度也是1504位,但是每次出的值却是不一样的,应该是加密的过程中有随机数,之后再看吧!!!

接着就是分析入参的第一个参数了,为了方便,把参数进行格式化,之后调用加密函数的时候再转成Json字符串,如下图所示:

分析一下几个比较关键的参数:

01.setLeft

这个参数需要获取滑块的两张图片,然后使用opencv 获取缺口的位置

02.userresponse
  1. 从后往前找生成的地方,找到了这里:

  2. 说明生成的地方还在前面,继续往前,也可以直接搜索userresponse,发现有12个地方,在每处都打上断点,然后刷新页面,滑动滑块,看最后断在哪里,如下图所示:

  3. 仔细观察发现userresponse的计算公式为:

    x坐标(setLeft) / 1.0059466666666665 + 2

03.pow_msg

也是直接搜索pow_msg,发现有三处,刷新页面后断住了,如下图所示:

接着就是找ch,发现c的参数都是上一个堆栈init传过来的,于是往上跟栈,如下图所示:

上面图片的6个参数就是组成c的参数,接下去要去看生成的逻辑

  1. 第1个参数_ᖁᖁᕷᖗ,是lot_numberlot_number又是由load接口返回的,如下图所示:

  2. 第2个参数_ᖁᖘᕷᖘ,它是captch_id,由captcha_id接口返回,如下图所示:

  3. 剩下的4个参数,也都是由load接口返回的数据

接着继续回去看参数h,发现是由guid这个函数生成的,如下图所示:

跟进去看看逻辑,发现参数是由e函数生成的,如下图所示:

e函数里面的逻辑:生成一个 4 位随机的小写十六进制字符串,如下图所示:

  • 等价逻辑代码:Math.random().toString(16).substr(2, 4)

因为这个值是随机的,全程都需要保持一致,所以也需要把整个函数导出到全局,方便调用,如下图所示:

04.pow_sign

接下去看pow_sign这个参数,是由l赋值而来的,并且是由pow_msg加密得来的,加密的方式也有三种:md5sha1sha256,所以其实也可以把pow_msg里的加密方式写成活的,不要固定住。

这里也有需要要注意的,就是pow_sign最后的结果需要以'00'开头 ,否则使用python程序验证接口的时候,data里的result会是forbidden。如下图所示:

如何让pow_sign最后的结果以是'00'开头,只需要不停的生成h,如下图所示:

05.XwNA

这个参数其实固定住也可以,生成逻辑就是下面这张图:

06.接下去分析这4个值,如下图所示:

上面图片中的三个值都是lot_number中截取出来的值,

  1. str1:6ec3db ====> 索引19开始截取6位
  2. str2:dbf78654 ====> 索引23开始截取8位
  3. str3:07836054 ====> 索引5开始截取8位
  4. str4:1090b6 ====> 索引14开始截取6位

代码中的逻辑如下,规律如下图所示:

参数分析的差不多了,看看结果:

结果演示

最终代码

JS代码

javascript 复制代码
const CryptoJs = require("crypto-js");
// delete __dirname;
// delete __filename;

// ========================= 工具函数 =========================

dtavm = {};
dtavm.log = console.log;

function proxy(obj, objname, type) {
    function getMethodHandler(WatchName, target_obj) {
        let methodhandler = {
            apply(target, thisArg, argArray) {
                if (this.target_obj) {
                    thisArg = this.target_obj;
                }
                let result = Reflect.apply(target, thisArg, argArray);
                if (target.name !== "toString") {
                    if (target.name === "addEventListener") {
                        dtavm.log(
                            `调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray[0]}], 结果 => [${result}].`,
                        );
                    } else if (WatchName === "window.console") {
                    } else {
                        dtavm.log(
                            `调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`,
                        );
                    }
                } else {
                    dtavm.log(
                        `调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`,
                    );
                }
                return result;
            },
            construct(target, argArray, newTarget) {
                var result = Reflect.construct(target, argArray, newTarget);
                dtavm.log(
                    `调用者 => [${WatchName}] 构造函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`,
                );
                return result;
            },
        };
        methodhandler.target_obj = target_obj;
        return methodhandler;
    }

    function getObjhandler(WatchName) {
        let handler = {
            get(target, propKey, receiver) {
                let result = target[propKey];
                if (result instanceof Object) {
                    if (typeof result === "function") {
                        dtavm.log(
                            `调用者 => [${WatchName}] 获取属性名 => [${propKey}] , 是个函数`,
                        );
                        return new Proxy(result, getMethodHandler(WatchName, target));
                    } else {
                        dtavm.log(
                            `调用者 => [${WatchName}] 获取属性名 => [${propKey}], 结果 => [${result}]`,
                        );
                    }
                    return new Proxy(result, getObjhandler(`${WatchName}.${propKey}`));
                }
                if (typeof propKey !== "symbol") {
                    dtavm.log(
                        `调用者 => [${WatchName}] 获取属性名 => [${propKey?.description ?? propKey}], 结果 => [${result}]`,
                    );
                }
                return result;
            },
            set(target, propKey, value, receiver) {
                if (value instanceof Object) {
                    dtavm.log(
                        `调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${value}]`,
                    );
                } else {
                    dtavm.log(
                        `调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${value}]`,
                    );
                }
                return Reflect.set(target, propKey, value, receiver);
            },
            has(target, propKey) {
                var result = Reflect.has(target, propKey);
                dtavm.log(
                    `针对in操作符的代理has=> [${WatchName}] 有无属性名 => [${propKey}], 结果 => [${result}]`,
                );
                return result;
            },
            deleteProperty(target, propKey) {
                var result = Reflect.deleteProperty(target, propKey);
                dtavm.log(
                    `拦截属性delete => [${WatchName}] 删除属性名 => [${propKey}], 结果 => [${result}]`,
                );
                return result;
            },
            defineProperty(target, propKey, attributes) {
                var result = Reflect.defineProperty(target, propKey, attributes);
                dtavm.log(
                    `拦截对象define操作 => [${WatchName}] 待检索属性名 => [${propKey.toString()}] 属性描述 => [${attributes}], 结果 => [${result}]`,
                );
                // debugger
                return result;
            },
            getPrototypeOf(target) {
                var result = Reflect.getPrototypeOf(target);
                dtavm.log(`被代理的目标对象 => [${WatchName}] 代理结果 => [${result}]`);
                return result;
            },
            setPrototypeOf(target, proto) {
                dtavm.log(
                    `被拦截的目标对象 => [${WatchName}] 对象新原型==> [${proto}]`,
                );
                return Reflect.setPrototypeOf(target, proto);
            },
            preventExtensions(target) {
                dtavm.log(`方法用于设置preventExtensions => [${WatchName}] 防止扩展`);
                return Reflect.preventExtensions(target);
            },
            isExtensible(target) {
                var result = Reflect.isExtensible(target);
                dtavm.log(
                    `拦截对对象的isExtensible() => [${WatchName}] isExtensible, 返回值==> [${result}]`,
                );
                return result;
            },
        };
        return handler;
    }

    if (type === "method") {
        return new Proxy(obj, getMethodHandler(objname, obj));
    }
    return new Proxy(obj, getObjhandler(objname));
}

// 【全局防 toString 检测 · 完整版】
(() => {
    "use strict";
    // 1. 保存原生的 Function.toString 方法
    const $toString = Function.toString;
    // 2. 创建唯一隐形标记(Symbol)
    const myFunction_toString_symbol = Symbol(
        "_" + Math.random().toString(36).slice(2),
    );

    // 3. 自定义 toString 逻辑(检票员)
    const myToString = function () {
        return (
            (typeof this === "function" && this[myFunction_toString_symbol]) ||
            $toString.call(this)
        );
    };

    // 4. 隐形贴标工具(给函数挂隐藏属性)
    function set_native(func, key, value) {
        Object.defineProperty(func, key, {
            enumerable: false, // 设置为【不可枚举】= 隐形
            configurable: true,
            writable: true,
            value: value, // 要伪装的内容:原生函数字符串{[native code]}
        });
    }

    // 5. 替换全局的 toString(接管所有函数)
    delete Function.prototype.toString; // 删除系统原生的toString
    set_native(Function.prototype, "toString", myToString);
    // Function.prototype:安装目标 → 函数总原型
    // 'toString':安装的属性名 → 覆盖 toString 方法
    // myToString:我们自己写的自定义 toString 逻辑(有标记就伪装原生,没标记就正常输出)

    // 6. 给全局挂载一键伪装工具
    globalThis.safefunction = (func) => {
        set_native(
            func, // 要伪装的函数
            myFunction_toString_symbol, // 要伪装的属性名
            `function ${func.name || ""}() { [native code] }`, // 要伪装的内容
        );
    };

    // 7. 自我保护:让我们的伪装工具也看起来像原生
    set_native(
        Function.prototype.toString,
        myFunction_toString_symbol,
        "function toString() { [native code] }",
    );
})();

function obj_toString(obj, tag) {
    // 标准原生属性配置(完全模拟浏览器原生行为,防检测)
    Object.defineProperty(obj, Symbol.toStringTag, {
        value: tag,
        enumerable: false, // 不可枚举(原生对象默认行为)
        configurable: true, // 可重新配置(防止重复定义报错)
        writable: false, // 不可修改(更贴近原生,防二次检测)
    });
}

// ========================= 补环境代码 =========================

// EventTarget
EventTarget = function EventTarget() {
};
safefunction(EventTarget);

EventTarget.prototype.addEventListener = function () {
};
safefunction(EventTarget.prototype.addEventListener);

// Node
Node = function Node() {
};
safefunction(Node);
Object.setPrototypeOf(Node.prototype, EventTarget.prototype);

// window
Window = function Window() {
};
safefunction(Window);
Object.setPrototypeOf(Window.prototype, EventTarget.prototype);

window = global;
self = top = globalThis = parent = window;
Object.setPrototypeOf(window, Window.prototype);

obj_toString(window, "Window");
// window = proxy(window, "window");

// HTMLHtmlElement
HTMLHtmlElement = function HTMLHtmlElement() {
};
safefunction(HTMLHtmlElement);
// Object.setPrototypeOf(HTMLHtmlElement.prototype, HTMLElement.prototype);

html = new HTMLHtmlElement();
obj_toString(html, "HTMLHtmlElement");
// html = proxy(html, "html");

// HTMLHeadElement
HTMLHeadElement = function HTMLHeadElement() {
};
safefunction(HTMLHeadElement);
// Object.setPrototypeOf(HTMLHeadElement.prototype, HTMLElement.prototype);

head = new HTMLHeadElement();
obj_toString(head, "HTMLHeadElement");
// head = proxy(head, "head");

// HTMLBodyElement
HTMLBodyElement = function HTMLBodyElement() {
};
safefunction(HTMLBodyElement);
// Object.setPrototypeOf(HTMLBodyElement.prototype, HTMLElement.prototype);
HTMLBodyElement.prototype.removeChild = function (args) {
    console.log("对象:HTMLHtmlElement.removeChild", args);
};
body = new HTMLBodyElement();
obj_toString(body, "HTMLBodyElement");
// body = proxy(body, "body");

// document
Document = function Document() {
};
safefunction(Document);
Object.setPrototypeOf(Document.prototype, Node.prototype);

Document.prototype.head = head;
Document.prototype.body = body;
Document.prototype.documentElement = html;
Document.prototype.createElement = function createElement(tag) {
    console.log(`特殊检测: createElement 被调用 (tag: ${tag})`);
};
safefunction(Document.prototype.createElement);

HTMLDocument = function HTMLDocument() {
};
safefunction(HTMLDocument);
Object.setPrototypeOf(HTMLDocument.prototype, Document.prototype);

document = new HTMLDocument();
obj_toString(document, "HTMLDocument");
// document = proxy(document, "document");

// navigator
Navigator = function Navigator() {
};
safefunction(Navigator);
Navigator.prototype = {
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
    ,
    platform: "Win32",
    hardwareConcurrency: 16,
    webdriver: false,
    languages: ["zh-CN", "zh"],
    appName: "Netscape",
    appVersion: '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
    plugins: {
        0: {
            0: {},
            1: {},
        },
        1: {
            0: {},
            1: {},
        },
        2: {
            0: {},
            1: {},
        },
        3: {
            0: {},
            1: {},
        },
        4: {
            0: {},
            1: {},
        },
        length: 5,
    },
};

navigator = new Navigator();
obj_toString(navigator, "Navigator");
// navigator = proxy(navigator, "navigator");

// location
Location = function Location() {
};
safefunction(Location);

location = new Location();

location = {
    ancestorOrigins: {},
    href: "https://gt4.geetest.com/",
    origin: "https://gt4.geetest.com",
    protocol: "https:",
    host: "gt4.geetest.com",
    hostname: "gt4.geetest.com",
    port: "",
    pathname: "/",
    search: "",
    hash: "",
};

obj_toString(location, "Location");
// location = proxy(location, "location");


// Screen
Screen = function Screen() {
};
safefunction(Screen);

screen = new Screen();
obj_toString(screen, "Screen");
// screen = proxy(screen, "screen");

// localStorage
Storage = function Storage() {
};
safefunction(Storage);

localStorage = new Storage();
obj_toString(localStorage, "Storage");
// localStorage = proxy(localStorage, "localStorage");

// crypto
Crypto = function Crypto() {
};
safefunction(Crypto);

Crypto.prototype.getRandomValues = function getRandomValues(typedArray) {
    for (let i = 0; i < typedArray.length; i++) {
        // 生成对应类型的随机数
        if (typedArray instanceof Uint8Array) {
            typedArray[i] = Math.floor(Math.random() * 256); // 0-255
        } else if (typedArray instanceof Uint16Array) {
            typedArray[i] = Math.floor(Math.random() * 65536); // 0-65535
        } else if (typedArray instanceof Uint32Array) {
            typedArray[i] = Math.floor(Math.random() * 4294967296); // 0-4294967295
        }
    }

    // ✅ 关键:必须返回传入的数组(原生规范)
    return typedArray;
};
safefunction(Crypto.prototype.getRandomValues);

crypto = new Crypto();
obj_toString(crypto, "Crypto");
// crypto = proxy(crypto, "crypto");

// ========================= 源码 =========================

require("./source.js");

// ========================= 测试 =========================

function get_w(x, hashfunc, lot_number, datetime, captcha_id) {
    let key;
    let pow_msg;
    let pow_sign;

    // 🔥 核心:循环生成 key,直到 pow_sign 以 00 开头
    do {
        // 每次循环重新生成 key
        key = window.key();
        // 拼接 pow_msg
        pow_msg = `1|8|${hashfunc}|${datetime}|${captcha_id}|${lot_number}||${key}`;
        // 计算 sha256
         if (hashfunc === 'md5') {
            pow_sign = CryptoJs.MD5(pow_msg).toString();
        } else if (hashfunc === 'sha1') {
            pow_sign = CryptoJs.SHA1(pow_msg).toString();
        } else if (hashfunc === 'sha256') {
            pow_sign = CryptoJs.SHA256(pow_msg).toString();
        }
        // 调试:打印看看是否满足条件
        // console.log("重试中...pow_sign:", pow_sign);
    } while (pow_sign.indexOf('00') !== 0); // 不满足就一直循环

    console.log("✅ 找到符合条件的pow_sign:", pow_sign);
    console.log("✅ 对应key:", key);

    str1 = lot_number.substr(19, 6) // 索引19开始截取6位
    str2 = lot_number.substr(23, 8) // 索引23开始截取8位
    str3 = lot_number.substr(5, 8)  // 索引5开始截取8位
    str4 = lot_number.substr(14, 6) // 索引14开始截取6位

    const passtime = Math.floor(Math.random() * 300) + 500; // 300-800ms 真人耗时

    aaa = {
        "setLeft": x, // x坐标
        "passtime": passtime, // 滑动耗时
        "userresponse": x / 1.0059466666666665 + 2,  // x坐标(setLeft) / 1.0059466666666665 + 2
        "device_id": "",
        "lot_number": lot_number, // load接口返回
        // hashfunc、datetime 由load接口返回, captch_id 由captcha_id接口返回
        "pow_msg": pow_msg,
        // 由pow_msg 加密得来
        "pow_sign": pow_sign,
        "geetest": "captcha", // 固定的
        "lang": "zh", // 固定的
        "ep": "123", // 固定的
        "biht": "1426265548", // 固定的
        "gee_guard": {
            "roe": {
                "aup": "3",
                "sep": "3",
                "egp": "3",
                "auh": "3",
                "rew": "3",
                "snh": "3",
                "res": "3",
                "cdc": "3"
            }
        },
        "4MTT": "0Qh0", // 动态的
        // 3个值都来自load接口,截取lot_number中的部分值
        [str1]: {
            [str2]: {
                [str3]: str4
            }
        },
        "em": { "ph": 0, "cp": 0, "ek": "11", "wd": 1, "nt": 0, "si": 0, "sc": 0 } // 固定的
    };

    console.log(aaa)


    arr = {
        options: { 'pt': '1' }
    }

    result = window.iii(JSON.stringify(aaa), arr);
    return result;
}

// console.log(get_w(196, 'sha256', "dbbcf007750e48bea942e4bb7d350bc9", '2026-04-03T13:29:07.115836+08:00', '54088bb07d2df3c46b79f80300b0abbe'));

Python代码

python 复制代码
import requests
import json
import time
import re
import execjs
import cv2
import random
import numpy as np


jscode = open("滑动拼图验证码.js", 'r', encoding="utf-8").read()

session = requests.Session()

headers = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Pragma": "no-cache",
    "Referer": "https://gt4.geetest.com/",
    "Sec-Fetch-Dest": "script",
    "Sec-Fetch-Mode": "no-cors",
    "Sec-Fetch-Site": "same-site",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
    "sec-ch-ua": "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\""
}
cookies = {
    "captcha_v4_user": "",
    "Hm_lvt_25b04a5e7a64668b9b88e2711fb5f0c4": "1775014501",
    "sensorsdata2015jssdkcross": ""
}
session.headers.clear()
session.headers.update(headers)
session.cookies.update(cookies)

# 动态生成challenge,uuid格式
def generate_challenge():
    def replace(c):
        r = random.randint(0, 15)
        v = r if c == 'x' else (r & 0x3 | 0x8)
        return hex(v)[2:]
    uuid_str = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    result = ''
    for char in uuid_str:
        result += replace(char) if char in ('x','y') else char
    return result

# 解析JSONP响应
def parse_jsonp(response_text):
    """把 geetest_xxx({...}) 转为 纯JSON"""
    left = response_text.find('(') + 1
    right = response_text.rfind(')')
    json_str = response_text[left:right]
    return json.loads(json_str)

# 把背景图、滑块图转成字节数据
def get_captcha_image(load_response):
    bg_url = "https://static.geetest.com/" + load_response['data']['bg']
    tp_url = "https://static.geetest.com/" + load_response['data']['slice']

    # 直接获取图片字节数据,不保存本地
    bg_bytes = session.get(bg_url).content
    slice_bytes = session.get(tp_url).content

    return bg_bytes, slice_bytes

# 识别缺口
def detect_gap(bg_bytes: bytes, slice_bytes: bytes) -> int:
    # 1. 解码图片
    bg_arr = np.frombuffer(bg_bytes, np.uint8)
    sl_arr = np.frombuffer(slice_bytes, np.uint8)
    bg_img = cv2.imdecode(bg_arr, cv2.IMREAD_COLOR)
    sl_img = cv2.imdecode(sl_arr, cv2.IMREAD_UNCHANGED)

    # 2. 从滑块 alpha 通道提取拼图轮廓
    alpha = sl_img[:, :, 3]  # 取 alpha 通道
    alpha_bin = (alpha > 128).astype(np.uint8) * 255  # 二值化
    alpha_edge = cv2.Canny(alpha_bin, 100, 200)  # Canny 边缘检测

    # 3. 对背景图做边缘检测
    bg_gray = cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY)
    bg_edge = cv2.Canny(bg_gray, 100, 200)

    # 4. 模板匹配 - 拼图轮廓 vs 背景边缘
    result = cv2.matchTemplate(bg_edge, alpha_edge, cv2.TM_CCOEFF_NORMED)
    _, _, _, max_loc = cv2.minMaxLoc(result)

    return max_loc[0]  # 缺口 x 坐标

# 请求 captcha_id 接口
def get_captcha_id():
    # 请求的js文件名是会变化的,有效时间大概6-8个小时
    url = "https://gt4.geetest.com/assets/index.1395bf87.js"
    response = session.get(url)

    pattern = r'captchaId:"([a-f0-9]{32})"'
    match = re.search(pattern, response.text)
    captcha_id = match.group(1)
    return captcha_id


# 请求验证码接口(load接口)
def get_captcha_load(captcha_id):
    # 动态生成callback(时间戳,极验要求)
    callback = f"geetest_{int(time.time() * 1000)}"

    # 动态生成 challenge
    challenge = generate_challenge()
    params = {
        "callback": callback,
        "captcha_id": captcha_id,
        "challenge": challenge,
        "client_type": "web",  # 验证码类型
        "risk_type": "slide",
        "lang": "zh",
    }
    url = "https://gcaptcha4.geetest.com/load"
    response = session.get(url, params=params)

    result = parse_jsonp(response.text)
    return result


# 请求验证接口(verify接口)
def get_captcha_verify(captcha_id, load_response, w):
    # 动态生成callback(时间戳,极验要求)
    callback = f"geetest_{int(time.time() * 1000)}"
    # 从load结果中拿【最新、未过期】的参数
    lot_number = load_response["data"]["lot_number"]
    payload = load_response["data"]["payload"]
    process_token = load_response["data"]["process_token"]
    params = {
        # 时间戳,动态
        "callback": callback,
        "captcha_id": captcha_id,  # 验证码id
        "client_type": "web",
        # 动态,由load接口返回
        "lot_number": lot_number,
        "risk_type": "slide",
        # 动态,由load接口返回
        "payload": payload,
        # 动态,由load接口返回
        "process_token": process_token,
        "payload_protocol": "1",
        "pt": "1",
        # 加密参数,1536位
        "w": w,
    }
    # print("verify接口请求参数>>>>",params)
    url = "https://gcaptcha4.geetest.com/verify"
    response = session.get(url, params=params)

    result = parse_jsonp(response.text)
    return result


# 封装核心验证流程
def run_captcha(load_response, captcha_id):
    # 1. 获取图片字节流
    bg_bytes, slice_bytes = get_captcha_image(load_response)
    # 2. 调用你想用的 detect_gap 计算滑动距离
    setLeft = detect_gap(bg_bytes, slice_bytes)
    print(f"✅ 正确缺口距离>>>> {setLeft}")

    hashfunc = load_response['data']['pow_detail']['hashfunc']
    lot_number = load_response["data"]["lot_number"]
    datetime = load_response['data']["pow_detail"]["datetime"]
    w = execjs.compile(jscode).call('get_w', setLeft, hashfunc, lot_number, datetime, captcha_id)
    # print(f"✅ 加密参数w>>>> {w[:100]}...")

    verify_response = get_captcha_verify(captcha_id, load_response, w)
    return verify_response


if __name__ == '__main__':

    captcha_id = get_captcha_id()
    # print("captcha_id>>>>", captcha_id)

    # 首次获取Load数据
    load_response = get_captcha_load(captcha_id)
    # print("load接口响应数据>>>>", load_response)

    # 执行验证流程
    verify_response = run_captcha(load_response, captcha_id)
    print("verify接口返回响应>>>>>", verify_response)

    # 判断验证结果
    if verify_response['data']['result'] == 'success':
        print("✅ 验证成功!")
    else:
        print("❌ 验证失败!")

如有错误请各位大佬指出,感谢

相关推荐
小江的记录本2 小时前
【Swagger】Swagger系统性知识体系全方位结构化总结
java·前端·后端·python·mysql·spring·docker
Keep Running *2 小时前
Angular_学习笔记
笔记·学习·angular.js
m0_738120722 小时前
网络安全编程——如何用Python实现SSH 服务端和SSH 反向 Shell(突破内网)
python·web安全·ssh
sinat_255487812 小时前
泛型·学习笔记
java·jvm·数据库·windows·python
猫咪老师2 小时前
Day9 Python 关于协程的最详细介绍!
python
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2025.04.06 题目:874. 模拟行走机器人
笔记·leetcode·机器人
单片机学习之路2 小时前
【Python】输入input函数
开发语言·python
不屈的铝合金2 小时前
Python入门:输入输出(I/O)指南
windows·python·i/o·input·print·输入输出
oi..2 小时前
Linux入门(2)
linux·笔记·测试工具·安全·网络安全