selenium+java破解极验滑动验证码的示例代码

作者:卧颜沉默 时间:2022-11-19 21:52:01 

摘要

分析验证码素材图片混淆原理,并采用selenium模拟人拖动滑块过程,进而破解验证码。

人工验证的过程

1、打开威锋网注册页面

2、移动鼠标至小滑块,一张完整的图片会出现(如下图1)

selenium+java破解极验滑动验证码的示例代码

3、点击鼠标左键,图片中间会出现一个缺块(如下图2)

selenium+java破解极验滑动验证码的示例代码

4、移动小滑块正上方图案至缺块处

5、验证通过

selenium模拟验证的过程

  1. 加载威锋网注册页面

  2. 下载图片1和缺块图片2

  3. 根据两张图片的差异计算平移的距离x

  4. 模拟鼠标点击事件,点击小滑块向右移动x

  5. 验证通过

  6. 详细分析

1、打开chrome浏览器控制台,会发现图1所示的验证码图片并不是极验后台返回的原图。而是由多个div拼接而成(如下图3)

selenium+java破解极验滑动验证码的示例代码

通过图片显示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

selenium+java破解极验滑动验证码的示例代码

2、下载图片并还原,上一步骤分析了图片具体的混淆逻辑,具体还原图片的代码实现如下,主要逻辑是把原图裁剪为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。

3、计算平移距离,遍历图片的每一个像素点,当两张图的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;
     }
   }
 }

4、模拟鼠标移动事件,这一步骤是最关键的步骤,极验验证码后台正是通过移动滑块的轨迹来判断是否为机器所为。整个移动轨迹的过程越随机越好,我这里提供一种成功率较高的移动算法,代码如下。


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

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>

最后

完整代码已上传至github,地址:https://github.com/wycm/selenium-geetest-crack

附上一张滑动效果图

selenium+java破解极验滑动验证码的示例代码

来源:https://www.jianshu.com/p/1466f1ba3275

标签:selenium,java,滑动验证码
0
投稿

猜你喜欢

  • C#基础入门之值类型和引用类型的区别详析

    2022-02-22 00:14:04
  • java中怎样表示圆周率

    2022-11-30 11:20:00
  • 深入剖析Android系统中Service和IntentService的区别

    2023-06-29 17:55:16
  • ViewFlipper实现上下翻滚轮播效果

    2023-10-06 08:55:40
  • java 一个类实现两个接口的案例

    2023-08-09 12:24:35
  • Android编程实现简单文件浏览器功能

    2022-01-02 06:13:46
  • JAVA HashMap详细介绍和示例

    2023-04-07 00:17:43
  • C#任务并行Parellel.For和Parallel.ForEach

    2022-04-19 22:07:58
  • IntelliJ IDEA Run时报“无效的源发行版:16“错误问题及解决方法

    2022-06-04 18:08:35
  • Android GestureDetector用户手势检测实例讲解

    2022-12-28 17:24:46
  • Android仿ios年龄、生日、性别滚轮效果

    2022-02-22 11:59:03
  • Maven继承与聚合详解及作用介绍

    2023-03-08 00:14:36
  • Android如何遍历特定目录下所有文件

    2022-12-11 12:27:33
  • Java反射获取实例的速度对比分析

    2021-06-14 09:22:29
  • JAVA8之函数式编程Function接口用法

    2022-01-04 04:03:04
  • Maven的安装配置详解

    2023-11-24 08:52:05
  • c#实现的操作oracle通用类

    2022-01-31 15:08:37
  • eclipse中maven插件安装教程

    2023-06-23 17:08:23
  • gradle中的增量构建浅析

    2022-09-23 06:43:13
  • Spring boot中@Conditional和spring boot的自动配置实例详解

    2023-06-20 09:36:14
  • asp之家 软件编程 m.aspxhome.com