java+selenium实现滑块验证
背景
现在越来越多的网站都使用采用滑块验证来作为验证机制,用于判断用户是否为人类而不是机器人。它需要用户将滑块拖动到指定位置来完成验证。
网上上有很多python和node过滑块的案例,但是java的特别少。
本篇文章一起来看下java怎么实现滑块验证。
思路
因为隐私问题,假设有一个网站 www.example.com, 打开后需要点击,那么我们完整的登录流程为:
- 打开网站www.example.com
- 点击页面右上角login
- 在弹出对话框输入用户名
- 点击send code 发送邮箱验证码
- 弹出滑块,拖动滑动滑块到指定位置,松开鼠标
- 查看邮箱验证码,并输入
- 点击登录
- 获取登录后cookie中返回的token,判断是否登录成功
代码
chromedriver下载地址
下载与自己浏览器对应版本的chromedriverregistry.npmmirror.com/binary.html
最新版谷歌浏览器chromedriver下载地址googlechromelabs.github.io/chrome-for-testing/
maven增加如下依赖
<!-- selenium-java --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.8.0</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.15.1</version> </dependency> <!-- 获取邮箱验证码 --> <dependency> <groupId>javax.mail</groupId> <artifactId>javax.mail-api</artifactId> <version>1.6.2</version> </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>1.6.2</version> </dependency> <!-- 工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>
滑块验证
- 先保存无缺口的图片到本地,然后保存有缺的图片到本地。
- 将两张图片转换成RGB集合,比较两张图片像素点的RGB值是否相同。
- 只要RGB的集合大于一定的误差阈值,则认为该位置为缺口位置。
核心代码如下:
/** * 比较两张截图,找出有缺口的验证码截图中缺口所在位置 * 由于滑块是x轴方向位移,因此只需要x轴的坐标即可 * * @return 缺口起始点x坐标 * @throws Exception */ public int comparePicture() throws Exception { notchPicture = ImageIO.read(new File("有缺口.png")); fullPicture = ImageIO.read(new File("无缺口.png")); int width = notchPicture.getWidth(); int height = notchPicture.getHeight(); int pos = 70; // 小方块的固定起始位置 // 横向扫描 for (int i = pos; i < width; i++) { for (int j = 0; j < height - 10; j++) { if (!equalPixel(i, j)) { pos = i; return pos; } } } throw new Exception("未找到滑块缺口"); }
/** * 比较两张截图上的当前像素点的RGB值是否相同 * 只要满足一定误差阈值,便可认为这两个像素点是相同的 * * @param x 像素点的x坐标 * @param y 像素点的y坐标 * @return true/false */ public boolean equalPixel(int x, int y) { int rgbaBefore = notchPicture.getRGB(x, y); int rgbaAfter = fullPicture.getRGB(x, y); // 转化成RGB集合 Color colBefore = new Color(rgbaBefore, true); Color colAfter = new Color(rgbaAfter, true); int threshold = 220; // RGB差值阈值 if (Math.abs(colBefore.getRed() - colAfter.getRed()) < threshold && Math.abs(colBefore.getGreen() - colAfter.getGreen()) < threshold && Math.abs(colBefore.getBlue() - colAfter.getBlue()) < threshold) { return true; } return false; }
移动滑块代码:
/** * 移动滑块,实现验证 * * @param moveTrace 滑块的运动轨迹 * @throws Exception */ public void move(List<Integer> moveTrace) throws Exception { // 获取滑块对象 element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector("div.dv_handler.dv_handler_bg")); // 按下滑块 actions.clickAndHold(element).perform(); Iterator it = moveTrace.iterator(); while (it.hasNext()) { // 位移一次 int dis = (int) it.next(); moveWithoutWait(dis, 0); } // 模拟人的操作,超过区域 moveWithoutWait(5, 0); moveWithoutWait(-3, 0); moveWithoutWait(-2, 0); // 释放滑块 actions.release().perform(); Thread.sleep(500); } /** * 消除selenium中移动操作的卡顿感 * 这种卡顿感是因为selenium中自带的moveByOffset是默认有200ms的延时的 * 可参考:https://blog.csdn.net/fx9590/article/details/113096513 * * @param x x轴方向位移距离 * @param y y轴方向位移距离 */ public void moveWithoutWait(int x, int y) { PointerInput defaultMouse = new PointerInput(MOUSE, "default mouse"); actions.tick(defaultMouse.createPointerMove(Duration.ofMillis(5), PointerInput.Origin.pointer(), x, y)).perform(); }
为了防止每天频繁登录,可能会被封号。 我们还要实现每天只需要登录一次,其余时间都是免登录
// 每天重新登陆一次 File cookieFile = new File("example.cookie.txt" + DateUtil.today()); if (!cookieFile.exists()) { // 文件不存在则认为是当天首次登录,清空缓存文件 FileUtil.del(tempDirect); }
/** * 保存cookie * @throws IOException */ private void saveCookie() throws IOException { File cookieFile = new File("example.cookie.txt" + DateUtil.today()); cookieFile.delete(); cookieFile.createNewFile(); FileWriter fileWriter = new FileWriter(cookieFile); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); for (Cookie cookie : driver.manage().getCookies()) { bufferedWriter.write((cookie.getName() + ";" + cookie.getValue() + ";" + cookie.getDomain() + ";" + cookie.getPath() + ";" + cookie.getExpiry() + ";" + cookie.isSecure())); bufferedWriter.newLine(); } bufferedWriter.flush(); bufferedWriter.close(); fileWriter.close(); }
/** * 读取cookie加载到浏览器 * @throws IOException */ private void addCookie() throws IOException { File cookieFile = new File("example.cookie.txt" + DateUtil.today()); if (cookieFile.exists()) { FileReader fileReader = new FileReader(cookieFile); BufferedReader bufferedReader = new BufferedReader(fileReader); String line; while ((line = bufferedReader.readLine()) != null) { StringTokenizer stringTokenizer = new StringTokenizer(line, ";"); while (stringTokenizer.hasMoreTokens()) { String name = stringTokenizer.nextToken(); String value = stringTokenizer.nextToken(); String domain = stringTokenizer.nextToken(); String path = stringTokenizer.nextToken(); Date expiry = null; String dt; if (!(dt = stringTokenizer.nextToken()).equals("null")) { expiry = new Date(dt); } boolean isSecure = new Boolean(stringTokenizer.nextToken()).booleanValue(); Cookie cookie = new Cookie(name, value, domain, path, expiry, isSecure); driver.manage().addCookie(cookie); } } } }
完整代码
package com.fandf.selenium; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.fandf.email.EmailService; import com.fandf.utils.SeleniumUtil; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.*; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.interactions.PointerInput; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.time.Duration; import java.util.List; import java.util.*; import static org.openqa.selenium.interactions.PointerInput.Kind.MOUSE; /** * @author fandongfeng * @date 2023-12-10 13:48 **/ @Slf4j public class SliderAutomatic implements Closeable { private WebDriver driver; private Actions actions; private WebElement element; private JavascriptExecutor js; // 带有缺口的验证码 private BufferedImage notchPicture; // 不带有缺口的验证码 private BufferedImage fullPicture; // chromedriver地址 private final static String chromedriver = "C:\Users\Administrator\Desktop\chrome\chromedriver.exe"; // 浏览器缓存地址 private final static String tempDirect = "C:\Users\Administrator\Desktop\chrome\temp"; @Override public void close() { if (driver != null) { driver.quit(); } } public String login() throws Exception { log.info("开始登录"); System.setProperty("webdriver.chrome.driver", chromedriver); // 每天重新登陆一次 File cookieFile = new File("example.cookie.txt" + DateUtil.today()); if (!cookieFile.exists()) { // 文件不存在则认为是当天首次登录,清空缓存文件 FileUtil.del(tempDirect); } log.info("清除缓存成功"); ChromeOptions options = new ChromeOptions(); options.addArguments("--remote-allow-origins=*"); String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"; options.addArguments("--user-agent=" + userAgent); options.addArguments("--disable-gpu"); options.addArguments("--disable-dev-shm-usage"); options.addArguments("--no-sandbox"); options.addArguments("--single-process"); options.addArguments("--disable-setuid-sandbox"); // 启用自动化扩展 options.setExperimentalOption("excludeSwitches", Arrays.asList("enable-automation")); options.addArguments("--disable-blink-features=AutomationControlled"); // 禁用浏览器的安全性 options.addArguments("--disable-web-security"); options.addArguments("--allow-running-insecure-content"); //禁用浏览器的同源策略 options.addArguments("--disable-features=IsolateOrigins,site-per-process"); options.addArguments("--user-data-dir=" + tempDirect); // 设置后台静默模式启动浏览器 // options.addArguments("--headless=new"); log.info("设置请求头完成"); driver = new ChromeDriver(options); driver.manage().window().maximize(); js = (JavascriptExecutor) driver; js.executeScript("window.scrollTo(1,100)"); actions = new Actions(driver); // 先访问在在加载cookie 否则报错 invalid cookie domain driver.get("www.example.com"); // 读取cookie加载到浏览器 addCookie(); // 刷新页面 driver.navigate().refresh(); if (!isLogin()) { log.info("开始登录..."); // 登录 loginExample(); // 登录成功后先刷新 driver.navigate().refresh(); } else { log.info("免登录成功..."); } String token = null; Set<Cookie> cookies = driver.manage().getCookies(); for (Cookie cookie : cookies) { log.info("cookie= {}", JSONUtil.toJsonStr(cookie)); if (cookie.getName().equals("Example-Token")) { // 登录成功后会返回token log.info("Example-Token 的值为:" + cookie.getValue()); token = cookie.getValue(); } } // token存在则证明登录成功 if (StrUtil.isNotBlank(token)) { saveCookie(); } return token; } /** * 读取cookie加载到浏览器 * * @throws IOException */ private void addCookie() throws IOException { File cookieFile = new File("example.cookie.txt" + DateUtil.today()); if (cookieFile.exists()) { FileReader fileReader = new FileReader(cookieFile); BufferedReader bufferedReader = new BufferedReader(fileReader); String line; while ((line = bufferedReader.readLine()) != null) { StringTokenizer stringTokenizer = new StringTokenizer(line, ";"); while (stringTokenizer.hasMoreTokens()) { String name = stringTokenizer.nextToken(); String value = stringTokenizer.nextToken(); String domain = stringTokenizer.nextToken(); String path = stringTokenizer.nextToken(); Date expiry = null; String dt; if (!(dt = stringTokenizer.nextToken()).equals("null")) { expiry = new Date(dt); } boolean isSecure = new Boolean(stringTokenizer.nextToken()).booleanValue(); Cookie cookie = new Cookie(name, value, domain, path, expiry, isSecure); driver.manage().addCookie(cookie); } } } } /** * 保存cookie * * @throws IOException */ private void saveCookie() throws IOException { File cookieFile = new File("example.cookie.txt" + DateUtil.today()); cookieFile.delete(); cookieFile.createNewFile(); FileWriter fileWriter = new FileWriter(cookieFile); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); for (Cookie cookie : driver.manage().getCookies()) { bufferedWriter.write((cookie.getName() + ";" + cookie.getValue() + ";" + cookie.getDomain() + ";" + cookie.getPath() + ";" + cookie.getExpiry() + ";" + cookie.isSecure())); bufferedWriter.newLine(); } bufferedWriter.flush(); bufferedWriter.close(); fileWriter.close(); } private void loginExample() throws Exception { js.executeScript("window.scrollTo(1,100)"); // 调出验证码 WebElement element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector("div.login")); element.click(); element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector("input[name='username']")); element.clear(); element.sendKeys("123456789@163.com"); log.info("开始登录邮箱123456789@163.com"); element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector(".SendCode.correct")); element.click(); log.info("点击登录发送邮件完成"); // 等待验证码出现 SeleniumUtil.waitMostSeconds(driver, By.cssSelector(".drag-verify-container.veriftyItem")); // 等待5秒,网络问题,等待图片显示出来 log.info("等待5秒,网络问题,等待图片显示出来"); Thread.sleep(5000); // 保存验证码 saveCode(); int position = comparePicture(); log.info("滑块位置:{}", position); move(Collections.singletonList(position)); log.info("移动滑块位置成功,开始等待验证码"); // 等待10秒,收到邮件 Thread.sleep(20000); // 输入邮箱验证码 String emailLoginCode = EmailService.getLoginCode(); element = SeleniumUtil.waitMostSeconds(driver, By.xpath("//*[@id="app"]/div[1]/div/form/div[2]/div[2]/input")); element.clear(); element.sendKeys(emailLoginCode); log.info("邮箱验证码为:{}", emailLoginCode); // 点击登录 element = SeleniumUtil.waitMostSeconds(driver, By.xpath("//*[@id="app"]/div[1]/div/form/div[3]/div/button")); element.click(); log.info("登陆成功了"); } /** * 移动滑块,实现验证 * * @param moveTrace 滑块的运动轨迹 * @throws Exception */ private void move(List<Integer> moveTrace) throws Exception { // 获取滑块对象 element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector("div.dv_handler.dv_handler_bg")); // 按下滑块 actions.clickAndHold(element).perform(); Iterator it = moveTrace.iterator(); while (it.hasNext()) { // 位移一次 int dis = (int) it.next(); moveWithoutWait(dis, 0); } // 模拟人的操作,超过区域 moveWithoutWait(5, 0); moveWithoutWait(-3, 0); moveWithoutWait(-2, 0); // 释放滑块 actions.release().perform(); Thread.sleep(500); } /** * 消除selenium中移动操作的卡顿感 * 这种卡顿感是因为selenium中自带的moveByOffset是默认有200ms的延时的 * 可参考:https://blog.csdn.net/fx9590/article/details/113096513 * * @param x x轴方向位移距离 * @param y y轴方向位移距离 */ private void moveWithoutWait(int x, int y) { PointerInput defaultMouse = new PointerInput(MOUSE, "default mouse"); actions.tick(defaultMouse.createPointerMove(Duration.ofMillis(5), PointerInput.Origin.pointer(), x, y)).perform(); } /** * 比较两张截图,找出有缺口的验证码截图中缺口所在位置 * 由于滑块是x轴方向位移,因此只需要x轴的坐标即可 * * @return 缺口起始点x坐标 * @throws Exception */ private int comparePicture() throws Exception { notchPicture = ImageIO.read(new File("有缺口.png")); fullPicture = ImageIO.read(new File("无缺口.png")); int width = notchPicture.getWidth(); int height = notchPicture.getHeight(); int pos = 70; // 小方块的固定起始位置 // 横向扫描 for (int i = pos; i < width; i++) { for (int j = 0; j < height - 10; j++) { if (!equalPixel(i, j)) { pos = i; return pos; } } } throw new Exception("未找到滑块缺口"); } /** * 比较两张截图上的当前像素点的RGB值是否相同 * 只要满足一定误差阈值,便可认为这两个像素点是相同的 * * @param x 像素点的x坐标 * @param y 像素点的y坐标 * @return true/false */ private boolean equalPixel(int x, int y) { int rgbaBefore = notchPicture.getRGB(x, y); int rgbaAfter = fullPicture.getRGB(x, y); // 转化成RGB集合 Color colBefore = new Color(rgbaBefore, true); Color colAfter = new Color(rgbaAfter, true); int threshold = 220; // RGB差值阈值 if (Math.abs(colBefore.getRed() - colAfter.getRed()) < threshold && Math.abs(colBefore.getGreen() - colAfter.getGreen()) < threshold && Math.abs(colBefore.getBlue() - colAfter.getBlue()) < threshold) { return true; } return false; } /** * 获取无缺口的验证码和带有缺口的验证码 */ private void saveCode() { // 隐藏缺口 // 隐藏滑块 js.executeScript("document.querySelectorAll('canvas')[0].style='display'"); js.executeScript("document.querySelectorAll('canvas')[1].hidden='true'"); WebElement element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector("canvas.main-canvas")); File screen = element.getScreenshotAs(OutputType.FILE); //执行屏幕截取 SeleniumUtil.savePng(screen, "无缺口"); log.info("保存无缺口的截图完成"); // 获取有缺口的截图 // 隐藏滑块 js.executeScript("document.querySelectorAll('canvas')[1].style='display'"); js.executeScript("document.querySelectorAll('canvas')[1].hidden='true'"); WebElement element = SeleniumUtil.waitMostSeconds(driver, By.cssSelector("canvas.main-canvas")); File screen = element.getScreenshotAs(OutputType.FILE); //执行屏幕截取 SeleniumUtil.savePng(screen, "有缺口"); // 展示滑块 js.executeScript("document.querySelectorAll('canvas')[1].style='display: block;'"); log.info("保存有缺口的截图完成"); } /** * 通过页面是否有login按钮来判断是否登录 * T 登录了, F 未登录 */ private boolean isLogin() { return !SeleniumUtil.containElement(driver, By.cssSelector("div.login")); } }
package com.fandf.utils; import org.apache.commons.io.FileUtils; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.io.File; import java.io.IOException; import java.time.Duration; /** * @author fandongfeng * @date 2023/12/9 18:29 */ public class SeleniumUtil { /** * 判断元素是否存在 * * @param driver 驱动 * @param by 元素定位方式 * @return 元素控件 */ public static boolean containElement(WebDriver driver, By by) { try { WebDriverWait AppiumDriverWait = new WebDriverWait(driver, Duration.ofSeconds(5)); AppiumDriverWait.until(ExpectedConditions .presenceOfElementLocated(by)); return true; } catch (Exception ignore) { } return false; } /** * Selenium方法等待元素出现 * * @param driver 驱动 * @param by 元素定位方式 * @return 元素控件 */ public static WebElement waitMostSeconds(WebDriver driver, By by) { try { WebDriverWait AppiumDriverWait = new WebDriverWait(driver, Duration.ofSeconds(5)); return (WebElement) AppiumDriverWait.until(ExpectedConditions .presenceOfElementLocated(by)); } catch (Exception e) { e.printStackTrace(); } throw new NoSuchElementException("元素控件未出现"); } /** * 保存截图的方法 * * @param screen 元素截图 * @param name 截图保存名字 */ public static void savePng(File screen, String name) { String screenShortName = name + ".png"; try { System.out.println("save screenshot"); FileUtils.copyFile(screen, new File(screenShortName)); } catch (IOException e) { System.out.println("save screenshot fail"); e.printStackTrace(); } finally { System.out.println("save screenshot finish"); } } }
到此这篇关于java+selenium实现滑块验证的文章就介绍到这了,更多相关java selenium滑块验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
SpringBoot项目拦截器获取Post方法的请求body实现
本文主要介绍了SpringBoot项目拦截器获取Post方法的请求body,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2022-01-01@Resource和@Autowired两个注解的区别及说明
这篇文章主要介绍了@Resource和@Autowired两个注解的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2024-06-06Java中字符数组、String类、StringBuffer三者之间相互转换
这篇文章主要介绍了Java中字符数组、String类、StringBuffer三者之间相互转换,需要的朋友可以参考下2018-05-05深入了解Maven Settings.xml文件的结构和功能
这篇文章主要为大家介绍了Maven Settings.xml文件基本结构和功能详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-11-11mybatis对象List<String> List<Integer>属性映射方式
这篇文章主要介绍了mybatis对象List<String> List<Integer>属性映射方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2023-12-12SpringBoot集成WebSocket实现前后端消息互传的方法
这篇文章主要介绍了SpringBoot集成WebSocket实现前后端消息互传的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2019-10-10Java中使用WebUploader插件上传大文件单文件和多文件的方法小结
这篇文章主要介绍了Java中使用WebUploader插件上传大文件单文件和多文件的方法小结的相关资料,需要的朋友可以参考下2016-06-06
最新评论