selenium+java破解滑動驗證碼

2019-04-16更新

  • 修復極驗頁面改版,此次採用極驗官方的demo地址:https://www.geetest.com/demo/slide-bind.html
  • 截止2019-04-16,極驗和騰訊的兩個滑動驗證碼都是能保證比較高的成功率經過的
  • 如何在無界面服務器上搭建selenium運行環境,能夠參考個人另外一篇文章:https://www.cnblogs.com/w-y-c-m/p/10533361.html
  • 完整代碼github地址見底部

2018-09-20更新

  • 新增對騰訊滑動驗證碼https://007.qq.com/online.html?ADTAG=capt.slide的支持,本地測試經過率50左右,有待優化中,主要失敗在計算距離上(其中企鵝背景會計算失敗),你們有好的想法歡迎提出。
  • Run with TencentCrawler.java
  • 騰訊滑動驗證碼破解的思路和極驗滑動驗證碼略微不一樣,騰訊只會返回一張完整圖片,致使無法經過兩張圖片比對的方式來計算移動距離。因此只能經過一張圖來計算距離,這裏計算的方式是經過y軸上至少找到一條長度爲30px的白線。另外此處直接經過http請求的方式來下載的原圖,這麼作有兩個緣由
    1. 截圖的方式會對那條關鍵白線的像素點有所幹擾,計算的時候不太方便
    2. 返回到前端的圖片並無像極驗那樣對圖片作混淆

2018-09-18更新

  • 更新移動軌跡算法,成功率90左右,感謝Ouyang-Wenbin提供的代碼。

2018-09-08更新

  • 頁面改版,致使截圖拿到的兩張圖片同樣。
  • 另外原有的移動軌跡算法成功率有所降低,目前本地測試成功率50%左右。

2018-06-12更新

  • 17年8月份初次分享出來的時候仍是可用的,在極驗後臺更新之後。成功率急劇降低。
  • 再加上威鋒網也更新了,以前的demo也不可用了。我的緣由也一直沒有更新。
  • 如今是極驗官網做爲demo。
  • 如今的破解思路和以前大體相同,不過也省了不少事。以前版本其實有點麻煩彎路,不知道selenium提供了截圖的api。致使本身經過css去還原圖片,比較麻煩。如今極驗也是經過canvas的方式對圖片作的還原,沒有之前那麼容易還原了。因此當前版本直接採用截圖的形式。
  • 另外須要注意的是selenium截圖的一個坑,不肯定是否由我本身環境致使的,我當前測試環境是mac pro。具體表現網頁截圖分辨率大小和網頁本來的分辨率大小不一致,這種狀況可能致使在取某個element時拿不到想要的元素。
  • 最核心的依然是移動軌跡的算法,此次採用的一種看起來很簡單的軌跡,就是一個像素點的移動。本地測試了10次,經過了10次,測試經過率100%。css

    Quick Start

  1. 修改GeetestCrawlerV2中本身環境的ChromeDriver地址。
  2. Run with GeetestCrawlerV2.javahtml

    最後

  • github地址:https://github.com/wycm/selenium-geetest-crack
  • 若是以爲不錯,請給個star。前端

    分割線如下是原文


摘要

分析驗證碼素材圖片混淆原理,並採用selenium模擬人拖動滑塊過程,進而破解驗證碼。java

人工驗證的過程

  1. 打開威鋒網註冊頁面(https://passport.feng.com/?r=user/register)
  2. 移動鼠標至小滑塊,一張完整的圖片會出現(以下圖1)

  3. 點擊鼠標左鍵,圖片中間會出現一個缺塊(以下圖2)

  4. 移動小滑塊正上方圖案至缺塊處
  5. 驗證經過

selenium模擬驗證的過程

  1. 加載威鋒網註冊頁面(https://passport.feng.com/?r=user/register)
  2. 下載圖片1和缺塊圖片2
  3. 根據兩張圖片的差別計算平移的距離x
  4. 模擬鼠標點擊事件,點擊小滑塊向右移動x
  5. 驗證經過

詳細分析

  • 打開chrome瀏覽器控制檯,會發現圖1所示的驗證碼圖片並非極驗後臺返回的原圖。而是由多個div拼接而成(以下圖3)


    經過圖片顯示div的style屬性可知,極驗後臺把圖片進行切割加錯位處理。把素材圖片切割成10 * 58大小的52張小圖,再進行錯位處理。在網頁上顯示的時候,再經過css的background-position屬性對圖片進行還原。以上的圖1和圖2都是通過了這種處理。在這種狀況下,使用selenium模擬驗證是須要對下載的驗證碼圖片進行還原。如上圖3的第一個div.gt_cut_fullbg_slice標籤,它的大小爲10px * 58px,其中style屬性爲background-image: url("http://static.geetest.com/pictures/gt/969ffa43c/969ffa43c.webp"); background-position: -157px -58px;會把該屬性對應url的圖片進行一個平移操做,以左上角爲參考,向左平移157px,向上平移58px,圖片超出部分不會顯示。因此上圖1所示圖片是由26 * 2個10px * 58px大小的div組成(以下圖4)。每個小方塊的大小58 * 10

  • 下載圖片並還原,上一步驟分析了圖片具體的混淆邏輯,具體還原圖片的代碼實現以下,主要邏輯是把原圖裁剪爲52張小圖,而後拼接成一張完整的圖。
/**
 *還原圖片
 * @param type
 */
private static void restoreImage(String type) throws IOException {
    //把圖片裁剪爲2 * 26份
    for(int i = 0; i < 52; i++){
        cutPic(basePath + type +".jpg"
                ,basePath + "result/" + type + i + ".jpg", -moveArray[i][0], -moveArray[i][1], 10, 58);
    }
    //拼接圖片
    String[] b = new String[26];
    for(int i = 0; i < 26; i++){
        b[i] = String.format(basePath + "result/" + type + "%d.jpg", i);
    }
    mergeImage(b, 1, basePath + "result/" + type + "result1.jpg");
    //拼接圖片
    String[] c = new String[26];
    for(int i = 0; i < 26; i++){
        c[i] = String.format(basePath + "result/" + type + "%d.jpg", i + 26);
    }
    mergeImage(c, 1, basePath + "result/" + type + "result2.jpg");
    mergeImage(new String[]{basePath + "result/" + type + "result1.jpg",
            basePath + "result/" + type + "result2.jpg"}, 2, basePath + "result/" + type + "result3.jpg");
    //刪除產生的中間圖片
    for(int i = 0; i < 52; i++){
        new File(basePath + "result/" + type + i + ".jpg").deleteOnExit();
    }
    new File(basePath + "result/" + type + "result1.jpg").deleteOnExit();
    new File(basePath + "result/" + type + "result2.jpg").deleteOnExit();
}

還原過程須要注意的是,後臺返回錯位的圖片是312 * 116大小的。而網頁上圖片div的大小是260 * 116。node

  • 計算平移距離,遍歷圖片的每個像素點,當兩張圖的R、G、B之差的和大於255,說明該點的差別過大,頗有可能就是須要平移到該位置的那個點,代碼以下。
BufferedImage fullBI = ImageIO.read(new File(basePath + "result/" + FULL_IMAGE_NAME + "result3.jpg"));
    BufferedImage bgBI = ImageIO.read(new File(basePath + "result/" + BG_IMAGE_NAME + "result3.jpg"));
    for (int i = 0; i < bgBI.getWidth(); i++){
        for (int j = 0; j < bgBI.getHeight(); j++) {
            int[] fullRgb = new int[3];
            fullRgb[0] = (fullBI.getRGB(i, j)  & 0xff0000) >> 16;
            fullRgb[1] = (fullBI.getRGB(i, j)  & 0xff00) >> 8;
            fullRgb[2] = (fullBI.getRGB(i, j)  & 0xff);

            int[] bgRgb = new int[3];
            bgRgb[0] = (bgBI.getRGB(i, j)  & 0xff0000) >> 16;
            bgRgb[1] = (bgBI.getRGB(i, j)  & 0xff00) >> 8;
            bgRgb[2] = (bgBI.getRGB(i, j)  & 0xff);
            if(difference(fullRgb, bgRgb) > 255){
                return i;
            }
        }
    }
  • 模擬鼠標移動事件,這一步驟是最關鍵的步驟,極驗驗證碼後臺正是經過移動滑塊的軌跡來判斷是否爲機器所爲。整個移動軌跡的過程越隨機越好,我這裏提供一種成功率較高的移動算法,代碼以下。
public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
    int xDis = distance + 11;
    System.out.println("應平移距離:" + xDis);
    int moveX = new Random().nextInt(8) - 5;
    int moveY = 1;
    Actions actions = new Actions(driver);
    new Actions(driver).clickAndHold(element).perform();
    Thread.sleep(200);
    printLocation(element);
    actions.moveToElement(element, moveX, moveY).perform();
    System.out.println(moveX + "--" + moveY);
    printLocation(element);
    for (int i = 0; i < 22; i++){
        int s = 10;
        if (i % 2 == 0){
            s = -10;
        }
        actions.moveToElement(element, s, 1).perform();
        printLocation(element);
        Thread.sleep(new Random().nextInt(100) + 150);
    }

    System.out.println(xDis + "--" + 1);
    actions.moveByOffset(xDis, 1).perform();
    printLocation(element);
    Thread.sleep(200);
    actions.release(element).perform();
}
  • 完整代碼以下
package com.github.wycm;

import org.apache.commons.io.FileUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openqa.selenium.By;
import org.openqa.selenium.Point;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class GeettestCrawler {
    private static String basePath = "src/main/resources/";
    private static String FULL_IMAGE_NAME = "full-image";
    private static String BG_IMAGE_NAME = "bg-image";
    private static int[][] moveArray = new int[52][2];
    private static boolean moveArrayInit = false;
    private static String INDEX_URL = "https://passport.feng.com/?r=user/register";
    private static WebDriver driver;

    static {
        System.setProperty("webdriver.chrome.driver", "D:/dev/selenium/chromedriver_V2.30/chromedriver_win32/chromedriver.exe");
        if (!System.getProperty("os.name").toLowerCase().contains("windows")){
            System.setProperty("webdriver.chrome.driver", "/Users/wangyang/workspace/selenium/chromedriver_V2.30/chromedriver");
        }
        driver = new ChromeDriver();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++){
            try {
                invoke();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        driver.quit();
    }
    private static void invoke() throws IOException, InterruptedException {
        //設置input參數
        driver.get(INDEX_URL);

        //經過[class=gt_slider_knob gt_show]
        By moveBtn = By.cssSelector(".gt_slider_knob.gt_show");
        waitForLoad(driver, moveBtn);
        WebElement moveElemet = driver.findElement(moveBtn);
        int i = 0;
        while (i++ < 15){
            int distance = getMoveDistance(driver);
            move(driver, moveElemet, distance - 6);
            By gtTypeBy = By.cssSelector(".gt_info_type");
            By gtInfoBy = By.cssSelector(".gt_info_content");
            waitForLoad(driver, gtTypeBy);
            waitForLoad(driver, gtInfoBy);
            String gtType = driver.findElement(gtTypeBy).getText();
            String gtInfo = driver.findElement(gtInfoBy).getText();
            System.out.println(gtType + "---" + gtInfo);
            /**
             * 再來一次:
             * 驗證失敗:
             */
            if(!gtType.equals("再來一次:") && !gtType.equals("驗證失敗:")){
                Thread.sleep(4000);
                System.out.println(driver);
                break;
            }
            Thread.sleep(4000);
        }
    }

    /**
     * 移動
     * @param driver
     * @param element
     * @param distance
     * @throws InterruptedException
     */
    public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
        int xDis = distance + 11;
        System.out.println("應平移距離:" + xDis);
        int moveX = new Random().nextInt(8) - 5;
        int moveY = 1;
        Actions actions = new Actions(driver);
        new Actions(driver).clickAndHold(element).perform();
        Thread.sleep(200);
        printLocation(element);
        actions.moveToElement(element, moveX, moveY).perform();
        System.out.println(moveX + "--" + moveY);
        printLocation(element);
        for (int i = 0; i < 22; i++){
            int s = 10;
            if (i % 2 == 0){
                s = -10;
            }
            actions.moveToElement(element, s, 1).perform();
//            printLocation(element);
            Thread.sleep(new Random().nextInt(100) + 150);
        }

        System.out.println(xDis + "--" + 1);
        actions.moveByOffset(xDis, 1).perform();
        printLocation(element);
        Thread.sleep(200);
        actions.release(element).perform();
    }
    private static void printLocation(WebElement element){
        Point point  = element.getLocation();
        System.out.println(point.toString());
    }
    /**
     * 等待元素加載,10s超時
     * @param driver
     * @param by
     */
    public static void waitForLoad(final WebDriver driver, final By by){
        new WebDriverWait(driver, 10).until(new ExpectedCondition<Boolean>() {
            public Boolean apply(WebDriver d) {
                WebElement element = driver.findElement(by);
                if (element != null){
                    return true;
                }
                return false;
            }
        });
    }

    /**
     * 計算須要平移的距離
     * @param driver
     * @return
     * @throws IOException
     */
    public static int getMoveDistance(WebDriver driver) throws IOException {
        String pageSource = driver.getPageSource();
        String fullImageUrl = getFullImageUrl(pageSource);
        FileUtils.copyURLToFile(new URL(fullImageUrl), new File(basePath + FULL_IMAGE_NAME + ".jpg"));
        String getBgImageUrl = getBgImageUrl(pageSource);
        FileUtils.copyURLToFile(new URL(getBgImageUrl), new File(basePath + BG_IMAGE_NAME + ".jpg"));
        initMoveArray(driver);
        restoreImage(FULL_IMAGE_NAME);
        restoreImage(BG_IMAGE_NAME);
        BufferedImage fullBI = ImageIO.read(new File(basePath + "result/" + FULL_IMAGE_NAME + "result3.jpg"));
        BufferedImage bgBI = ImageIO.read(new File(basePath + "result/" + BG_IMAGE_NAME + "result3.jpg"));
        for (int i = 0; i < bgBI.getWidth(); i++){
            for (int j = 0; j < bgBI.getHeight(); j++) {
                int[] fullRgb = new int[3];
                fullRgb[0] = (fullBI.getRGB(i, j)  & 0xff0000) >> 16;
                fullRgb[1] = (fullBI.getRGB(i, j)  & 0xff00) >> 8;
                fullRgb[2] = (fullBI.getRGB(i, j)  & 0xff);

                int[] bgRgb = new int[3];
                bgRgb[0] = (bgBI.getRGB(i, j)  & 0xff0000) >> 16;
                bgRgb[1] = (bgBI.getRGB(i, j)  & 0xff00) >> 8;
                bgRgb[2] = (bgBI.getRGB(i, j)  & 0xff);
                if(difference(fullRgb, bgRgb) > 255){
                    return i;
                }
            }
        }
        throw new RuntimeException("未找到須要平移的位置");
    }
    private static int difference(int[] a, int[] b){
        return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
    }
    /**
     * 獲取move數組
     * @param driver
     */
    private static void initMoveArray(WebDriver driver){
        if (moveArrayInit){
            return;
        }
        Document document = Jsoup.parse(driver.getPageSource());
        Elements elements = document.select("[class=gt_cut_bg gt_show]").first().children();
        int i = 0;
        for(Element element : elements){
            Pattern pattern = Pattern.compile(".*background-position: (.*?)px (.*?)px.*");
            Matcher matcher = pattern.matcher(element.toString());
            if (matcher.find()){
                String width = matcher.group(1);
                String height = matcher.group(2);
                moveArray[i][0] = Integer.parseInt(width);
                moveArray[i++][1] = Integer.parseInt(height);
            } else {
                throw new RuntimeException("解析異常");
            }
        }
        moveArrayInit = true;
    }
    /**
     *還原圖片
     * @param type
     */
    private static void restoreImage(String type) throws IOException {
        //把圖片裁剪爲2 * 26份
        for(int i = 0; i < 52; i++){
            cutPic(basePath + type +".jpg"
                    ,basePath + "result/" + type + i + ".jpg", -moveArray[i][0], -moveArray[i][1], 10, 58);
        }
        //拼接圖片
        String[] b = new String[26];
        for(int i = 0; i < 26; i++){
            b[i] = String.format(basePath + "result/" + type + "%d.jpg", i);
        }
        mergeImage(b, 1, basePath + "result/" + type + "result1.jpg");
        //拼接圖片
        String[] c = new String[26];
        for(int i = 0; i < 26; i++){
            c[i] = String.format(basePath + "result/" + type + "%d.jpg", i + 26);
        }
        mergeImage(c, 1, basePath + "result/" + type + "result2.jpg");
        mergeImage(new String[]{basePath + "result/" + type + "result1.jpg",
                basePath + "result/" + type + "result2.jpg"}, 2, basePath + "result/" + type + "result3.jpg");
        //刪除產生的中間圖片
        for(int i = 0; i < 52; i++){
            new File(basePath + "result/" + type + i + ".jpg").deleteOnExit();
        }
        new File(basePath + "result/" + type + "result1.jpg").deleteOnExit();
        new File(basePath + "result/" + type + "result2.jpg").deleteOnExit();
    }
    /**
     * 獲取原始圖url
     * @param pageSource
     * @return
     */
    private static String getFullImageUrl(String pageSource){
        String url = null;
        Document document = Jsoup.parse(pageSource);
        String style = document.select("[class=gt_cut_fullbg_slice]").first().attr("style");
        Pattern pattern = Pattern.compile("url\\(\"(.*)\"\\)");
        Matcher matcher = pattern.matcher(style);
        if (matcher.find()){
            url = matcher.group(1);
        }
        url = url.replace(".webp", ".jpg");
        System.out.println(url);
        return url;
    }
    /**
     * 獲取帶背景的url
     * @param pageSource
     * @return
     */
    private static String getBgImageUrl(String pageSource){
        String url = null;
        Document document = Jsoup.parse(pageSource);
        String style = document.select(".gt_cut_bg_slice").first().attr("style");
        Pattern pattern = Pattern.compile("url\\(\"(.*)\"\\)");
        Matcher matcher = pattern.matcher(style);
        if (matcher.find()){
            url = matcher.group(1);
        }
        url = url.replace(".webp", ".jpg");
        System.out.println(url);
        return url;
    }
    public static boolean cutPic(String srcFile, String outFile, int x, int y,
                                 int width, int height) {
        FileInputStream is = null;
        ImageInputStream iis = null;
        try {
            if (!new File(srcFile).exists()) {
                return false;
            }
            is = new FileInputStream(srcFile);
            String ext = srcFile.substring(srcFile.lastIndexOf(".") + 1);
            Iterator<ImageReader> it = ImageIO.getImageReadersByFormatName(ext);
            ImageReader reader = it.next();
            iis = ImageIO.createImageInputStream(is);
            reader.setInput(iis, true);
            ImageReadParam param = reader.getDefaultReadParam();
            Rectangle rect = new Rectangle(x, y, width, height);
            param.setSourceRegion(rect);
            BufferedImage bi = reader.read(0, param);
            File tempOutFile = new File(outFile);
            if (!tempOutFile.exists()) {
                tempOutFile.mkdirs();
            }
            ImageIO.write(bi, ext, new File(outFile));
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (iis != null) {
                    iis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
    }
    /**
     * 圖片拼接 (注意:必須兩張圖片長寬一致哦)
     * @param files 要拼接的文件列表
     * @param type  1橫向拼接,2 縱向拼接
     * @param targetFile 輸出文件
     */
    private static void mergeImage(String[] files, int type, String targetFile) {
        int length = files.length;
        File[] src = new File[length];
        BufferedImage[] images = new BufferedImage[length];
        int[][] ImageArrays = new int[length][];
        for (int i = 0; i < length; i++) {
            try {
                src[i] = new File(files[i]);
                images[i] = ImageIO.read(src[i]);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            int width = images[i].getWidth();
            int height = images[i].getHeight();
            ImageArrays[i] = new int[width * height];
            ImageArrays[i] = images[i].getRGB(0, 0, width, height, ImageArrays[i], 0, width);
        }
        int newHeight = 0;
        int newWidth = 0;
        for (int i = 0; i < images.length; i++) {
            // 橫向
            if (type == 1) {
                newHeight = newHeight > images[i].getHeight() ? newHeight : images[i].getHeight();
                newWidth += images[i].getWidth();
            } else if (type == 2) {// 縱向
                newWidth = newWidth > images[i].getWidth() ? newWidth : images[i].getWidth();
                newHeight += images[i].getHeight();
            }
        }
        if (type == 1 && newWidth < 1) {
            return;
        }
        if (type == 2 && newHeight < 1) {
            return;
        }
        // 生成新圖片
        try {
            BufferedImage ImageNew = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
            int height_i = 0;
            int width_i = 0;
            for (int i = 0; i < images.length; i++) {
                if (type == 1) {
                    ImageNew.setRGB(width_i, 0, images[i].getWidth(), newHeight, ImageArrays[i], 0,
                            images[i].getWidth());
                    width_i += images[i].getWidth();
                } else if (type == 2) {
                    ImageNew.setRGB(0, height_i, newWidth, images[i].getHeight(), ImageArrays[i], 0, newWidth);
                    height_i += images[i].getHeight();
                }
            }
            //輸出想要的圖片
            ImageIO.write(ImageNew, targetFile.split("\\.")[1], new File(targetFile));

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  1. pom文件依賴以下
<dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-server</artifactId>
      <version>3.0.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
    <dependency>
      <groupId>org.jsoup</groupId>
      <artifactId>jsoup</artifactId>
      <version>1.7.2</version>
    </dependency>

效果

  1. 附上一張滑動效果圖

    image

最後

  • github地址:https://github.com/wycm/selenium-geetest-crack
  • 若是以爲不錯,請給個star
  • 歡迎加入爬蟲qq交流羣:633925314

版權聲明
做者:wycm
出處:https://www.cnblogs.com/w-y-c-m/p/7359455.html
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸做者全部,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。
一個程序員平常分享,包括但不限於爬蟲、Java後端技術,歡迎關注
歡迎加入我新開的知識星球(解答和分享各類爬蟲&Java問題git

相關文章
相關標籤/搜索