java OpenCV挑战极验滑动拼图验证码

一丶解析验证码组成

在这里插入图片描述

从上面三张图来看,极验滑动拼图验证码是由一个小的拼图和一个大的背景图组成,拼图的形状各式各样,背景图中有一个阴影缺口,与拼图形状一致。
在这里插入图片描述
这里我们使用F12大法打开浏览器控制台,观察一下验证码的页面结构。
在这里插入图片描述
通过观察可以看到,验证码所包含的图片均以<canves>画布的形式呈现在页面中,且有三张图片,且第三张图片被加上了属性style=“display: none;”,即为隐藏不显示。那么我们修改下页面代码,看下这张图究竟是什么。

在这里插入图片描述
修改完代码发现,这不就是完整的背景图嘛。那么根据上面的命名来看,基本可以确定这三张图分别是什么了。

  • 第一张class为geetest_canvas_bg geetest_absolute,可以确定为带缺口的背景图。

  • 第二张class为geetest_canvas_slice geetest_absolute,可以确定为拼图。

  • 第三张便是完整的图片。

二丶分析出破解思路

  1. 首先根据这个验证码的组成,来分析一下我们人要做的事情:

按照正常的手动操作流程来看,我们需要看出背景图中与拼图对应的阴影缺口的位置,然后鼠标按住下方滑块来把拼图对正到缺口位置来完成验证。

  1. 然后根据人要做的事情,来分析一下程序要做的事情:

根据分析得出下面几个步骤:
1.获取到两张图片(带缺口背景图、完整背景图)
2.处理图片,得到阴影位置并计算滑动距离
3.根据滑动距离模拟滑动

三丶具体操作步骤

1丶获取到两张图片

由于这里的图片都是通过canvas画布呈现的,我们可以通过执行js代码来生成图片。
可以参考《如何抓取canvas画布中的图片》

在这里插入图片描述

2丶处理图片,计算滑动距离

通过第一步得到的两张图片可以看出,两张图有两处不同的地方,一处差异不大,一处差异较大,我们可以通过比较每一个像素点的差异度来确定阴影缺口的位置。缺口的位置横坐标减去小图距离边框的距离即为滑动距离。

以下是关键部分代码:

private final String INDEX_URL = "https://www.geetest.com/Register";
	// 延时加载
	private static WebElement waitWebElement(WebDriver driver, By by, int count) throws Exception {
		WebElement webElement = null;
		boolean isWait = false;
		for (int k = 0; k < count; k++) {
			try {
				webElement = driver.findElement(by);
				if (isWait)
					System.out.println(" ok!");
				return webElement;
			} catch (org.openqa.selenium.NoSuchElementException ex) {
				isWait = true;
				if (k == 0)
					System.out.print("waitWebElement(" + by.toString() + ")");
				else
					System.out.print(".");
				Thread.sleep(50);
			}
		}
		if (isWait)
			System.out.println(" outTime!");
		return null;
	}
	/**
	 * 计算需要平移的距离
	 * 
	 * @param driver
	 * @param fullImgPath完整背景图片文件名
	 * @param bgImgPath含有缺口背景图片文件名
	 * @return
	 * @throws IOException
	 */
	public static int getMoveDistance(WebDriver driver, String fullImgPath, String bgImgPath) throws IOException {
		File fullFile = new File(fullImgPath);
		File bgFile = new File(bgImgPath);
		try {
			BufferedImage fullBI = ImageIO.read(fullFile);
			BufferedImage bgBI = ImageIO.read(bgFile);
			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;
					}
				}
			}
		} catch (Exception e) {
			return 0;
		} finally {
			fullFile.delete();
			bgFile.delete();
		}
		return 0;
	}
	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]);
	}
	/**
	 * // 执行 JS 代码并生成图片
	 * 
	 * @param driver
	 * @param jsString
	 * @param input
	 * @return图片路径
	 */
	public static String getImgByJs(WebDriver driver, String jsString) {
		try {
			String imgFilePath = "c://GeeTest_" + System.currentTimeMillis() + "_" + (Math.random() * 9 + 1) * 100000 + ".jpg";
			String imgInfo = ((JavascriptExecutor) driver).executeScript(jsString).toString();
			if (imgInfo != null && imgInfo.contains("data")) {
				imgInfo = imgInfo.substring(imgInfo.indexOf(",") + 1);
				ByteArrayOutputStream outputStream = imgStrToFile(imgInfo);
				if (outputStream != null) {
					byte[] picBytes = outputStream.toByteArray();
					outPicToFile(picBytes, imgFilePath);
					return imgFilePath;
				}
			}
			return null;
		} catch (Exception e) {
			return null;
		}
	}
	/**
	 * 将base64字节码转byte输出流
	 * 
	 * @param imgBase64Str
	 * @return
	 */
	private static ByteArrayOutputStream imgStrToFile(String imgBase64Str) {
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		try {
			if (imgBase64Str != null) {
				BASE64Decoder decoder = new BASE64Decoder();
				byte[] data = decoder.decodeBuffer(imgBase64Str);
				outputStream.write(data);
				outputStream.flush();
			}
			return outputStream;
		} catch (Exception e) {
			return null;
		}
	}
	/**
	 * 图片流转图片
	 * 
	 * @param o
	 * @param imgFilePath
	 */
	private static void outPicToFile(Object o, String imgFilePath) {
		if (o == null)
			return;
		try {
			if (o instanceof byte[]) { // 转为图片
				if (((byte[]) o).length == 0)
					return;
				File imgFile = new File(imgFilePath);
				// byte数组到图片
				FileImageOutputStream imageOutput = new FileImageOutputStream(imgFile);
				imageOutput.write((byte[]) o, 0, ((byte[]) o).length);
				imageOutput.close();
			} else {
				return;
			}
		} catch (Exception e) {
		}
	}
	/**
	 * 模拟人工移动
	 * 
	 * @param driver
	 * @param element页面滑块
	 * @param distance需要移动距离
	 * @throws InterruptedException
	 */
	public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
		int randomTime = 0;
		if (distance > 90) {
			randomTime = 250;
		} else if (distance > 80 && distance <= 90) {
			randomTime = 150;
		}
		List<Integer> track = getMoveTrack(distance - 2);
		int moveY = 1;
		try {
			Actions actions = new Actions(driver);
			actions.clickAndHold(element).perform();
			Thread.sleep(200);
			for (int i = 0; i < track.size(); i++) {
				actions.moveByOffset(track.get(i), moveY).perform();
				Thread.sleep(new Random().nextInt(300) + randomTime);
			}
			Thread.sleep(200);
			actions.release(element).perform();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 根据距离获取滑动轨迹
	 * 
	 * @param distance需要移动的距离
	 * @return
	 */
	public static List<Integer> getMoveTrack(int distance) {
		List<Integer> track = new ArrayList<>();// 移动轨迹
		Random random = new Random();
		int current = 0;// 已经移动的距离
		int mid = (int) distance * 4 / 5;// 减速阈值
		int a = 0;
		int move = 0;// 每次循环移动的距离
		while (true) {
			a = random.nextInt(10);
			if (current <= mid) {
				move += a;// 不断加速
			} else {
				move -= a;
			}
			if ((current + move) < distance) {
				track.add(move);
			} else {
				track.add(distance - current);
				break;
			}
			current += move;
		}
		return track;
	}

	private void seleniumTest() {
		ChromeDriverManager manager = ChromeDriverManager.getInstance();
		int status = -1;
		String phone = "13814389438";
		try {
			WebDriver driver = manager.getDriver();
			driver.get(INDEX_URL);
			driver.manage().window().maximize(); // 设置浏览器窗口最大化
			Thread.sleep(2000);

			// 输入手机号
			WebElement phoneElemet = waitWebElement(driver, By.xpath("//input[@placeholder='手机号码']"), 20);
			phoneElemet.clear();
			for (int i = 0; i < phone.length(); i++) {
				char c = phone.charAt(i);
				phoneElemet.sendKeys(c + "");
				phoneElemet.click();
			}
			sleep(50);
			// 点击获取验证码
			waitWebElement(driver, By.className("sendCode"), 20).click();
			sleep(2000);
			// 完整背景图geetest_canvas_fullbg geetest_fade geetest_absolute
			String fullImgJs = "return document.getElementsByClassName(\"geetest_canvas_fullbg geetest_fade geetest_absolute\")[0].toDataURL(\"image/png\");";
			String fullImgPath = getImgByJs(driver, fullImgJs);
			// 含有缺口背景图geetest_canvas_bg geetest_absolute
			String bgImgJs = "return document.getElementsByClassName(\"geetest_canvas_bg geetest_absolute\")[0].toDataURL(\"image/png\");";
			String bgImgPath = getImgByJs(driver, bgImgJs);
			// 获取滑动按钮
			WebElement moveElemet = waitWebElement(driver, By.className("geetest_slider_button"), 20);
			// 获取滑动距离并删除图片
			int distance = getMoveDistance(driver, fullImgPath, bgImgPath);
			if (distance == 0) {
			}
			// 滑动
			move(driver, moveElemet, distance - 6);
			// 滑动结果
			sleep(2 * 1000);
			String gtInfo = waitWebElement(driver, By.className("sendCode"), 20).getAttribute("innerHTML");
			System.out.println(gtInfo);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			manager.closeDriver(status);
		}
	}

	protected static void sleep(long time) {
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
		}
	}

3丶根据滑动距离模拟滑动

得到滑动距离之后,我们再来看下滑动轨迹,如果滑动轨迹过于规律,则很容易被识别。所以我们就将滑动轨迹贴近人类的正常操作轨迹即可。
比如:先快后慢,慢慢对准缺口。在缺口处左右晃动。在缺口处停留,欣赏成果等。

四丶结果展示

在这里插入图片描述

五丶结果分析

目标:

识别阴影位置,推算出对应滑动距离,模拟滑动。

实现思路:

1.获取到两张图片(完整图、缺口图)
2.处理图片,得到阴影位置并计算滑动距离
3.根据滑动距离模拟滑动

识别耗时:

15 - 50毫秒

通过率:

>95%

 

posted @ 2021-02-04 10:28  锐洋智能  阅读(1351)  评论(0编辑  收藏  举报