java+selenium實現(xiàn)滑塊驗證
背景
現(xiàn)在越來越多的網(wǎng)站都使用采用滑塊驗證來作為驗證機制,用于判斷用戶是否為人類而不是機器人。它需要用戶將滑塊拖動到指定位置來完成驗證。
網(wǎng)上上有很多python和node過滑塊的案例,但是java的特別少。
本篇文章一起來看下java怎么實現(xiàn)滑塊驗證。
思路
因為隱私問題,假設(shè)有一個網(wǎng)站 www.example.com, 打開后需要點擊,那么我們完整的登錄流程為:
- 打開網(wǎng)站www.example.com
- 點擊頁面右上角login
- 在彈出對話框輸入用戶名
- 點擊send code 發(fā)送郵箱驗證碼
- 彈出滑塊,拖動滑動滑塊到指定位置,松開鼠標
- 查看郵箱驗證碼,并輸入
- 點擊登錄
- 獲取登錄后cookie中返回的token,判斷是否登錄成功
代碼
chromedriver下載地址
下載與自己瀏覽器對應(yīng)版本的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>
滑塊驗證
- 先保存無缺口的圖片到本地,然后保存有缺的圖片到本地。
- 將兩張圖片轉(zhuǎn)換成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);
// 轉(zhuǎn)化成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;
}
移動滑塊代碼:
/**
* 移動滑塊,實現(xiàn)驗證
*
* @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);
}
// 模擬人的操作,超過區(qū)域
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();
}
為了防止每天頻繁登錄,可能會被封號。 我們還要實現(xiàn)每天只需要登錄一次,其余時間都是免登錄
// 每天重新登陸一次
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);
// 設(shè)置后臺靜默模式啟動瀏覽器
// options.addArguments("--headless=new");
log.info("設(shè)置請求頭完成");
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)");
// 調(diào)出驗證碼
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("點擊登錄發(fā)送郵件完成");
// 等待驗證碼出現(xiàn)
SeleniumUtil.waitMostSeconds(driver, By.cssSelector(".drag-verify-container.veriftyItem"));
// 等待5秒,網(wǎng)絡(luò)問題,等待圖片顯示出來
log.info("等待5秒,網(wǎng)絡(luò)問題,等待圖片顯示出來");
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("http://*[@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("http://*[@id="app"]/div[1]/div/form/div[3]/div/button"));
element.click();
log.info("登陸成功了");
}
/**
* 移動滑塊,實現(xiàn)驗證
*
* @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);
}
// 模擬人的操作,超過區(qū)域
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);
// 轉(zhuǎn)化成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); //執(zhí)行屏幕截取
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); //執(zhí)行屏幕截取
SeleniumUtil.savePng(screen, "有缺口");
// 展示滑塊
js.executeScript("document.querySelectorAll('canvas')[1].style='display: block;'");
log.info("保存有缺口的截圖完成");
}
/**
* 通過頁面是否有l(wèi)ogin按鈕來判斷是否登錄
* 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 驅(qū)動
* @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方法等待元素出現(xiàn)
*
* @param driver 驅(qū)動
* @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("元素控件未出現(xiàn)");
}
/**
* 保存截圖的方法
*
* @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");
}
}
}
到此這篇關(guān)于java+selenium實現(xiàn)滑塊驗證的文章就介紹到這了,更多相關(guān)java selenium滑塊驗證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中使用Session共享實現(xiàn)分布式部署的示例代碼
這篇文章主要介紹了SpringBoot中使用Session共享實現(xiàn)分布式部署的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
詳解mybatis-plus的 mapper.xml 路徑配置的坑
這篇文章主要介紹了詳解mybatis-plus的 mapper.xml 路徑配置的坑,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
SpringBoot?集成Resteasy實現(xiàn)RESTFul接口的詳細過程
這篇文章主要介紹了SpringBoot集成Resteasy實現(xiàn)RESTFul接口,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08
圖數(shù)據(jù)庫NebulaGraph的Java 數(shù)據(jù)解析實踐與指導(dǎo)詳解
這篇文章主要介紹了圖數(shù)據(jù)庫NebulaGraph的Java 數(shù)據(jù)解析實踐與指導(dǎo)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04
Java結(jié)構(gòu)型設(shè)計模式之組合模式Composite Pattern詳解
組合模式,又叫部分整體模式,它創(chuàng)建了對象組的數(shù)據(jù)結(jié)構(gòu)組合模式使得用戶對單個對象和組合對象的訪問具有一致性。本文將通過示例為大家詳細介紹一下組合模式,需要的可以參考一下2022-11-11

