转自: https://www.jianshu.com/p/1466f1ba3275
selenium+java破解极验滑动验证码
摘要
分析验证码素材图片混淆原理,并采用selenium模拟人拖动滑块过程,进而破解验证码。
人工验证的过程
- 打开威锋网注册页面(https://passport.feng.com/?r=user/register)
- 移动鼠标至小滑块,一张完整的图片会出现(如下图1)
- 点击鼠标左键,图片中间会出现一个缺块(如下图2)
- 移动小滑块正上方图案至缺块处
- 验证通过
selenium模拟验证的过程
- 加载威锋网注册页面(https://passport.feng.com/?r=user/register)
- 下载图片1和缺块图片2
- 根据两张图片的差异计算平移的距离x
- 模拟鼠标点击事件,点击小滑块向右移动x
- 验证通过
详细分析
- 打开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张小图,然后拼接成一张完整的图。
1 /** 2 *还原图片 3 * @param type 4 */ 5 private static void restoreImage(String type) throws IOException { 6 //把图片裁剪为2 * 26份 7 for(int i = 0; i < 52; i++){ 8 cutPic(basePath + type +".jpg" 9 ,basePath + "result/" + type + i + ".jpg", -moveArray[i][0], -moveArray[i][1], 10, 58); 10 } 11 //拼接图片 12 String[] b = new String[26]; 13 for(int i = 0; i < 26; i++){ 14 b[i] = String.format(basePath + "result/" + type + "%d.jpg", i); 15 } 16 mergeImage(b, 1, basePath + "result/" + type + "result1.jpg"); 17 //拼接图片 18 String[] c = new String[26]; 19 for(int i = 0; i < 26; i++){ 20 c[i] = String.format(basePath + "result/" + type + "%d.jpg", i + 26); 21 } 22 mergeImage(c, 1, basePath + "result/" + type + "result2.jpg"); 23 mergeImage(new String[]{basePath + "result/" + type + "result1.jpg", 24 basePath + "result/" + type + "result2.jpg"}, 2, basePath + "result/" + type + "result3.jpg"); 25 //删除产生的中间图片 26 for(int i = 0; i < 52; i++){ 27 new File(basePath + "result/" + type + i + ".jpg").deleteOnExit(); 28 } 29 new File(basePath + "result/" + type + "result1.jpg").deleteOnExit(); 30 new File(basePath + "result/" + type + "result2.jpg").deleteOnExit(); 31 } 32 33 作者:卧颜沉默 34 链接:https://www.jianshu.com/p/1466f1ba3275 35 來源:简书 36 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 计算平移距离,遍历图片的每一个像素点,当两张图的R、G、B之差的和大于255,说明该点的差异过大,很有可能就是需要平移到该位置的那个点,代码如下。
1 BufferedImage fullBI = ImageIO.read(new File(basePath + "result/" + FULL_IMAGE_NAME + "result3.jpg")); 2 BufferedImage bgBI = ImageIO.read(new File(basePath + "result/" + BG_IMAGE_NAME + "result3.jpg")); 3 for (int i = 0; i < bgBI.getWidth(); i++){ 4 for (int j = 0; j < bgBI.getHeight(); j++) { 5 int[] fullRgb = new int[3]; 6 fullRgb[0] = (fullBI.getRGB(i, j) & 0xff0000) >> 16; 7 fullRgb[1] = (fullBI.getRGB(i, j) & 0xff00) >> 8; 8 fullRgb[2] = (fullBI.getRGB(i, j) & 0xff); 9 10 int[] bgRgb = new int[3]; 11 bgRgb[0] = (bgBI.getRGB(i, j) & 0xff0000) >> 16; 12 bgRgb[1] = (bgBI.getRGB(i, j) & 0xff00) >> 8; 13 bgRgb[2] = (bgBI.getRGB(i, j) & 0xff); 14 if(difference(fullRgb, bgRgb) > 255){ 15 return i; 16 } 17 } 18 } 19 20 作者:卧颜沉默 21 链接:https://www.jianshu.com/p/1466f1ba3275 22 來源:简书 23 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 模拟鼠标移动事件,这一步骤是最关键的步骤,极验验证码后台正是通过移动滑块的轨迹来判断是否为机器所为。整个移动轨迹的过程越随机越好,我这里提供一种成功率较高的移动算法,代码如下。
1 public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException { 2 int xDis = distance + 11; 3 System.out.println("应平移距离:" + xDis); 4 int moveX = new Random().nextInt(8) - 5; 5 int moveY = 1; 6 Actions actions = new Actions(driver); 7 new Actions(driver).clickAndHold(element).perform(); 8 Thread.sleep(200); 9 printLocation(element); 10 actions.moveToElement(element, moveX, moveY).perform(); 11 System.out.println(moveX + "--" + moveY); 12 printLocation(element); 13 for (int i = 0; i < 22; i++){ 14 int s = 10; 15 if (i % 2 == 0){ 16 s = -10; 17 } 18 actions.moveToElement(element, s, 1).perform(); 19 printLocation(element); 20 Thread.sleep(new Random().nextInt(100) + 150); 21 } 22 23 System.out.println(xDis + "--" + 1); 24 actions.moveByOffset(xDis, 1).perform(); 25 printLocation(element); 26 Thread.sleep(200); 27 actions.release(element).perform(); 28 } 29 30 作者:卧颜沉默 31 链接:https://www.jianshu.com/p/1466f1ba3275 32 來源:简书 33 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 完整代码如下
1 package com.github.wycm; 2 3 import org.apache.commons.io.FileUtils; 4 import org.jsoup.Jsoup; 5 import org.jsoup.nodes.Document; 6 import org.jsoup.nodes.Element; 7 import org.jsoup.select.Elements; 8 import org.openqa.selenium.By; 9 import org.openqa.selenium.Point; 10 import org.openqa.selenium.WebDriver; 11 import org.openqa.selenium.WebElement; 12 import org.openqa.selenium.chrome.ChromeDriver; 13 import org.openqa.selenium.interactions.Actions; 14 import org.openqa.selenium.support.ui.ExpectedCondition; 15 import org.openqa.selenium.support.ui.WebDriverWait; 16 17 import javax.imageio.ImageIO; 18 import javax.imageio.ImageReadParam; 19 import javax.imageio.ImageReader; 20 import javax.imageio.stream.ImageInputStream; 21 import java.awt.*; 22 import java.awt.image.BufferedImage; 23 import java.io.File; 24 import java.io.FileInputStream; 25 import java.io.IOException; 26 import java.net.URL; 27 import java.util.Iterator; 28 import java.util.Random; 29 import java.util.regex.Matcher; 30 import java.util.regex.Pattern; 31 32 public class GeettestCrawler { 33 private static String basePath = "src/main/resources/"; 34 private static String FULL_IMAGE_NAME = "full-image"; 35 private static String BG_IMAGE_NAME = "bg-image"; 36 private static int[][] moveArray = new int[52][2]; 37 private static boolean moveArrayInit = false; 38 private static String INDEX_URL = "https://passport.feng.com/?r=user/register"; 39 private static WebDriver driver; 40 41 static { 42 System.setProperty("webdriver.chrome.driver", "D:/dev/selenium/chromedriver_V2.30/chromedriver_win32/chromedriver.exe"); 43 if (!System.getProperty("os.name").toLowerCase().contains("windows")){ 44 System.setProperty("webdriver.chrome.driver", "/Users/wangyang/workspace/selenium/chromedriver_V2.30/chromedriver"); 45 } 46 driver = new ChromeDriver(); 47 } 48 49 public static void main(String[] args) throws InterruptedException { 50 for (int i = 0; i < 10; i++){ 51 try { 52 invoke(); 53 } catch (IOException e) { 54 e.printStackTrace(); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 } 59 driver.quit(); 60 } 61 private static void invoke() throws IOException, InterruptedException { 62 //设置input参数 63 driver.get(INDEX_URL); 64 65 //通过[class=gt_slider_knob gt_show] 66 By moveBtn = By.cssSelector(".gt_slider_knob.gt_show"); 67 waitForLoad(driver, moveBtn); 68 WebElement moveElemet = driver.findElement(moveBtn); 69 int i = 0; 70 while (i++ < 15){ 71 int distance = getMoveDistance(driver); 72 move(driver, moveElemet, distance - 6); 73 By gtTypeBy = By.cssSelector(".gt_info_type"); 74 By gtInfoBy = By.cssSelector(".gt_info_content"); 75 waitForLoad(driver, gtTypeBy); 76 waitForLoad(driver, gtInfoBy); 77 String gtType = driver.findElement(gtTypeBy).getText(); 78 String gtInfo = driver.findElement(gtInfoBy).getText(); 79 System.out.println(gtType + "---" + gtInfo); 80 /** 81 * 再来一次: 82 * 验证失败: 83 */ 84 if(!gtType.equals("再来一次:") && !gtType.equals("验证失败:")){ 85 Thread.sleep(4000); 86 System.out.println(driver); 87 break; 88 } 89 Thread.sleep(4000); 90 } 91 } 92 93 /** 94 * 移动 95 * @param driver 96 * @param element 97 * @param distance 98 * @throws InterruptedException 99 */ 100 public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException { 101 int xDis = distance + 11; 102 System.out.println("应平移距离:" + xDis); 103 int moveX = new Random().nextInt(8) - 5; 104 int moveY = 1; 105 Actions actions = new Actions(driver); 106 new Actions(driver).clickAndHold(element).perform(); 107 Thread.sleep(200); 108 printLocation(element); 109 actions.moveToElement(element, moveX, moveY).perform(); 110 System.out.println(moveX + "--" + moveY); 111 printLocation(element); 112 for (int i = 0; i < 22; i++){ 113 int s = 10; 114 if (i % 2 == 0){ 115 s = -10; 116 } 117 actions.moveToElement(element, s, 1).perform(); 118 // printLocation(element); 119 Thread.sleep(new Random().nextInt(100) + 150); 120 } 121 122 System.out.println(xDis + "--" + 1); 123 actions.moveByOffset(xDis, 1).perform(); 124 printLocation(element); 125 Thread.sleep(200); 126 actions.release(element).perform(); 127 } 128 private static void printLocation(WebElement element){ 129 Point point = element.getLocation(); 130 System.out.println(point.toString()); 131 } 132 /** 133 * 等待元素加载,10s超时 134 * @param driver 135 * @param by 136 */ 137 public static void waitForLoad(final WebDriver driver, final By by){ 138 new WebDriverWait(driver, 10).until(new ExpectedCondition<Boolean>() { 139 public Boolean apply(WebDriver d) { 140 WebElement element = driver.findElement(by); 141 if (element != null){ 142 return true; 143 } 144 return false; 145 } 146 }); 147 } 148 149 /** 150 * 计算需要平移的距离 151 * @param driver 152 * @return 153 * @throws IOException 154 */ 155 public static int getMoveDistance(WebDriver driver) throws IOException { 156 String pageSource = driver.getPageSource(); 157 String fullImageUrl = getFullImageUrl(pageSource); 158 FileUtils.copyURLToFile(new URL(fullImageUrl), new File(basePath + FULL_IMAGE_NAME + ".jpg")); 159 String getBgImageUrl = getBgImageUrl(pageSource); 160 FileUtils.copyURLToFile(new URL(getBgImageUrl), new File(basePath + BG_IMAGE_NAME + ".jpg")); 161 initMoveArray(driver); 162 restoreImage(FULL_IMAGE_NAME); 163 restoreImage(BG_IMAGE_NAME); 164 BufferedImage fullBI = ImageIO.read(new File(basePath + "result/" + FULL_IMAGE_NAME + "result3.jpg")); 165 BufferedImage bgBI = ImageIO.read(new File(basePath + "result/" + BG_IMAGE_NAME + "result3.jpg")); 166 for (int i = 0; i < bgBI.getWidth(); i++){ 167 for (int j = 0; j < bgBI.getHeight(); j++) { 168 int[] fullRgb = new int[3]; 169 fullRgb[0] = (fullBI.getRGB(i, j) & 0xff0000) >> 16; 170 fullRgb[1] = (fullBI.getRGB(i, j) & 0xff00) >> 8; 171 fullRgb[2] = (fullBI.getRGB(i, j) & 0xff); 172 173 int[] bgRgb = new int[3]; 174 bgRgb[0] = (bgBI.getRGB(i, j) & 0xff0000) >> 16; 175 bgRgb[1] = (bgBI.getRGB(i, j) & 0xff00) >> 8; 176 bgRgb[2] = (bgBI.getRGB(i, j) & 0xff); 177 if(difference(fullRgb, bgRgb) > 255){ 178 return i; 179 } 180 } 181 } 182 throw new RuntimeException("未找到需要平移的位置"); 183 } 184 private static int difference(int[] a, int[] b){ 185 return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]); 186 } 187 /** 188 * 获取move数组 189 * @param driver 190 */ 191 private static void initMoveArray(WebDriver driver){ 192 if (moveArrayInit){ 193 return; 194 } 195 Document document = Jsoup.parse(driver.getPageSource()); 196 Elements elements = document.select("[class=gt_cut_bg gt_show]").first().children(); 197 int i = 0; 198 for(Element element : elements){ 199 Pattern pattern = Pattern.compile(".*background-position: (.*?)px (.*?)px.*"); 200 Matcher matcher = pattern.matcher(element.toString()); 201 if (matcher.find()){ 202 String width = matcher.group(1); 203 String height = matcher.group(2); 204 moveArray[i][0] = Integer.parseInt(width); 205 moveArray[i++][1] = Integer.parseInt(height); 206 } else { 207 throw new RuntimeException("解析异常"); 208 } 209 } 210 moveArrayInit = true; 211 } 212 /** 213 *还原图片 214 * @param type 215 */ 216 private static void restoreImage(String type) throws IOException { 217 //把图片裁剪为2 * 26份 218 for(int i = 0; i < 52; i++){ 219 cutPic(basePath + type +".jpg" 220 ,basePath + "result/" + type + i + ".jpg", -moveArray[i][0], -moveArray[i][1], 10, 58); 221 } 222 //拼接图片 223 String[] b = new String[26]; 224 for(int i = 0; i < 26; i++){ 225 b[i] = String.format(basePath + "result/" + type + "%d.jpg", i); 226 } 227 mergeImage(b, 1, basePath + "result/" + type + "result1.jpg"); 228 //拼接图片 229 String[] c = new String[26]; 230 for(int i = 0; i < 26; i++){ 231 c[i] = String.format(basePath + "result/" + type + "%d.jpg", i + 26); 232 } 233 mergeImage(c, 1, basePath + "result/" + type + "result2.jpg"); 234 mergeImage(new String[]{basePath + "result/" + type + "result1.jpg", 235 basePath + "result/" + type + "result2.jpg"}, 2, basePath + "result/" + type + "result3.jpg"); 236 //删除产生的中间图片 237 for(int i = 0; i < 52; i++){ 238 new File(basePath + "result/" + type + i + ".jpg").deleteOnExit(); 239 } 240 new File(basePath + "result/" + type + "result1.jpg").deleteOnExit(); 241 new File(basePath + "result/" + type + "result2.jpg").deleteOnExit(); 242 } 243 /** 244 * 获取原始图url 245 * @param pageSource 246 * @return 247 */ 248 private static String getFullImageUrl(String pageSource){ 249 String url = null; 250 Document document = Jsoup.parse(pageSource); 251 String style = document.select("[class=gt_cut_fullbg_slice]").first().attr("style"); 252 Pattern pattern = Pattern.compile("url\("(.*)"\)"); 253 Matcher matcher = pattern.matcher(style); 254 if (matcher.find()){ 255 url = matcher.group(1); 256 } 257 url = url.replace(".webp", ".jpg"); 258 System.out.println(url); 259 return url; 260 } 261 /** 262 * 获取带背景的url 263 * @param pageSource 264 * @return 265 */ 266 private static String getBgImageUrl(String pageSource){ 267 String url = null; 268 Document document = Jsoup.parse(pageSource); 269 String style = document.select(".gt_cut_bg_slice").first().attr("style"); 270 Pattern pattern = Pattern.compile("url\("(.*)"\)"); 271 Matcher matcher = pattern.matcher(style); 272 if (matcher.find()){ 273 url = matcher.group(1); 274 } 275 url = url.replace(".webp", ".jpg"); 276 System.out.println(url); 277 return url; 278 } 279 public static boolean cutPic(String srcFile, String outFile, int x, int y, 280 int width, int height) { 281 FileInputStream is = null; 282 ImageInputStream iis = null; 283 try { 284 if (!new File(srcFile).exists()) { 285 return false; 286 } 287 is = new FileInputStream(srcFile); 288 String ext = srcFile.substring(srcFile.lastIndexOf(".") + 1); 289 Iterator<ImageReader> it = ImageIO.getImageReadersByFormatName(ext); 290 ImageReader reader = it.next(); 291 iis = ImageIO.createImageInputStream(is); 292 reader.setInput(iis, true); 293 ImageReadParam param = reader.getDefaultReadParam(); 294 Rectangle rect = new Rectangle(x, y, width, height); 295 param.setSourceRegion(rect); 296 BufferedImage bi = reader.read(0, param); 297 File tempOutFile = new File(outFile); 298 if (!tempOutFile.exists()) { 299 tempOutFile.mkdirs(); 300 } 301 ImageIO.write(bi, ext, new File(outFile)); 302 return true; 303 } catch (Exception e) { 304 e.printStackTrace(); 305 return false; 306 } finally { 307 try { 308 if (is != null) { 309 is.close(); 310 } 311 if (iis != null) { 312 iis.close(); 313 } 314 } catch (IOException e) { 315 e.printStackTrace(); 316 return false; 317 } 318 } 319 } 320 /** 321 * 图片拼接 (注意:必须两张图片长宽一致哦) 322 * @param files 要拼接的文件列表 323 * @param type 1横向拼接,2 纵向拼接 324 * @param targetFile 输出文件 325 */ 326 private static void mergeImage(String[] files, int type, String targetFile) { 327 int length = files.length; 328 File[] src = new File[length]; 329 BufferedImage[] images = new BufferedImage[length]; 330 int[][] ImageArrays = new int[length][]; 331 for (int i = 0; i < length; i++) { 332 try { 333 src[i] = new File(files[i]); 334 images[i] = ImageIO.read(src[i]); 335 } catch (Exception e) { 336 throw new RuntimeException(e); 337 } 338 int width = images[i].getWidth(); 339 int height = images[i].getHeight(); 340 ImageArrays[i] = new int[width * height]; 341 ImageArrays[i] = images[i].getRGB(0, 0, width, height, ImageArrays[i], 0, width); 342 } 343 int newHeight = 0; 344 int newWidth = 0; 345 for (int i = 0; i < images.length; i++) { 346 // 横向 347 if (type == 1) { 348 newHeight = newHeight > images[i].getHeight() ? newHeight : images[i].getHeight(); 349 newWidth += images[i].getWidth(); 350 } else if (type == 2) {// 纵向 351 newWidth = newWidth > images[i].getWidth() ? newWidth : images[i].getWidth(); 352 newHeight += images[i].getHeight(); 353 } 354 } 355 if (type == 1 && newWidth < 1) { 356 return; 357 } 358 if (type == 2 && newHeight < 1) { 359 return; 360 } 361 // 生成新图片 362 try { 363 BufferedImage ImageNew = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); 364 int height_i = 0; 365 int width_i = 0; 366 for (int i = 0; i < images.length; i++) { 367 if (type == 1) { 368 ImageNew.setRGB(width_i, 0, images[i].getWidth(), newHeight, ImageArrays[i], 0, 369 images[i].getWidth()); 370 width_i += images[i].getWidth(); 371 } else if (type == 2) { 372 ImageNew.setRGB(0, height_i, newWidth, images[i].getHeight(), ImageArrays[i], 0, newWidth); 373 height_i += images[i].getHeight(); 374 } 375 } 376 //输出想要的图片 377 ImageIO.write(ImageNew, targetFile.split("\.")[1], new File(targetFile)); 378 379 } catch (Exception e) { 380 throw new RuntimeException(e); 381 } 382 } 383 } 384 385 作者:卧颜沉默 386 链接:https://www.jianshu.com/p/1466f1ba3275 387 來源:简书 388 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- pom文件依赖如下
1 <dependency> 2 <groupId>org.seleniumhq.selenium</groupId> 3 <artifactId>selenium-server</artifactId> 4 <version>3.0.1</version> 5 </dependency> 6 <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup --> 7 <dependency> 8 <groupId>org.jsoup</groupId> 9 <artifactId>jsoup</artifactId> 10 <version>1.7.2</version> 11 </dependency> 12 13 作者:卧颜沉默 14 链接:https://www.jianshu.com/p/1466f1ba3275 15 來源:简书 16 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
最后
- 完整代码已上传至github,地址:https://github.com/wycm/selenium-geetest-crack
- 附上一张滑动效果图