selenium+java破解极验滑动验证码的示例代码
摘要
分析验证码素材图片混淆原理,并采用selenium模拟人拖动滑块过程,进而破解验证码。
人工验证的过程
1、打开威锋网注册页面
2、移动鼠标至小滑块,一张完整的图片会出现(如下图1)
3、点击鼠标左键,图片中间会出现一个缺块(如下图2)
4、移动小滑块正上方图案至缺块处
5、验证通过
selenium模拟验证的过程
- 加载威锋网注册页面
- 下载图片1和缺块图片2
- 根据两张图片的差异计算平移的距离x
- 模拟鼠标点击事件,点击小滑块向右移动x
- 验证通过
- 详细分析
1、打开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
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,地址:
附上一张滑动效果图
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。