This commit is contained in:
wangli 2026-01-17 13:30:02 +08:00
parent ee1d39e07b
commit 775cad7ac8
4 changed files with 388 additions and 0 deletions

View File

@ -0,0 +1,64 @@
package com.xianyu.autoreply.service.captcha;
import com.xianyu.autoreply.service.captcha.model.CaptchaResult;
import com.xianyu.autoreply.service.captcha.model.TrajectoryPoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* 滑块验证并发管理器
*/
@Slf4j
@Component
public class CaptchaConcurrencyManager {
private final int maxConcurrent = 3; // 最大并发数
private final Semaphore semaphore = new Semaphore(maxConcurrent);
private final Map<String, Long> activeSlots = new java.util.concurrent.ConcurrentHashMap<>();
/**
* 获取滑块验证槽位
*/
public boolean acquireSlot(String userId, int timeoutSeconds) {
try {
log.info("【{}】请求滑块验证槽位,当前活跃: {}/{}", userId, activeSlots.size(), maxConcurrent);
boolean acquired = semaphore.tryAcquire(timeoutSeconds, TimeUnit.SECONDS);
if (acquired) {
activeSlots.put(userId, System.currentTimeMillis());
log.info("【{}】已获取滑块验证槽位,当前活跃: {}/{}", userId, activeSlots.size(), maxConcurrent);
return true;
} else {
log.warn("【{}】获取滑块验证槽位超时", userId);
return false;
}
} catch (InterruptedException e) {
log.error("【{}】获取槽位被中断", userId, e);
return false;
}
}
/**
* 释放滑块验证槽位
*/
public void releaseSlot(String userId) {
activeSlots.remove(userId);
semaphore.release();
log.info("【{}】已释放滑块验证槽位,当前活跃: {}/{}", userId, activeSlots.size(), maxConcurrent);
}
/**
* 获取统计信息
*/
public Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("maxConcurrent", maxConcurrent);
stats.put("activeCount", activeSlots.size());
stats.put("availableSlots", maxConcurrent - activeSlots.size());
return stats;
}
}

View File

@ -0,0 +1,278 @@
package com.xianyu.autoreply.service.captcha;
import com.microsoft.playwright.options.BoundingBox;
import com.microsoft.playwright.options.Cookie;
import com.xianyu.autoreply.service.captcha.model.CaptchaResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import com.microsoft.playwright.*;
import java.util.*;
/**
* 滑块验证处理器 - 基于Playwright
*/
@Slf4j
@Component
public class CaptchaHandler {
private Playwright playwright;
private Browser browser;
private BrowserContext context;
private Page page;
/**
* 处理滑块验证
*
* @param verificationUrl 验证URL
* @param cookieId 账号ID
* @return 验证结果
*/
public CaptchaResult handleCaptcha(String verificationUrl, String cookieId) {
long startTime = System.currentTimeMillis();
try {
log.info("【{}】开始处理滑块验证...", cookieId);
log.info("【{}】验证URL: {}", cookieId, verificationUrl);
// 初始化浏览器
initBrowser(cookieId);
// 导航到验证页面
navigateToCaptchaPage(verificationUrl, cookieId);
// 等待页面加载
Thread.sleep(2000);
// 查找滑块元素
log.info("【{}】查找滑块元素...", cookieId);
ElementHandle sliderElement = findSliderElement(cookieId);
if (sliderElement == null) {
return CaptchaResult.failure("未找到滑块元素");
}
// 计算移动距离
int distance = calculateDistance(sliderElement, cookieId);
log.info("【{}】滑块移动距离: {}px", cookieId, distance);
// 执行拖动
dragSlider(sliderElement, distance, cookieId);
// 检查是否成功
Thread.sleep(2000);
boolean success = checkSuccess(cookieId);
if (success) {
// 提取cookies
Map<String, String> cookies = extractCookies(cookieId);
long duration = System.currentTimeMillis() - startTime;
log.info("【{}】✅ 滑块验证成功!耗时: {}ms", cookieId, duration);
return CaptchaResult.success(cookies, duration);
} else {
return CaptchaResult.failure("滑块验证失败");
}
} catch (Exception e) {
log.error("【{}】滑块验证异常", cookieId, e);
return CaptchaResult.failure("异常: " + e.getMessage());
} finally {
// 清理资源
cleanup(cookieId);
}
}
/**
* 初始化浏览器
*/
private void initBrowser(String cookieId) {
log.info("【{}】初始化Playwright浏览器...", cookieId);
playwright = Playwright.create();
browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(false)
.setArgs(Arrays.asList(
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-blink-features=AutomationControlled",
"--disable-gpu",
"--disable-web-security"
))
);
// 创建上下文
context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.setViewportSize(1920, 1080)
);
// 创建页面
page = context.newPage();
// 注入反检测脚本
injectStealthScript(cookieId);
log.info("【{}】浏览器初始化完成", cookieId);
}
/**
* 注入反检测脚本
*/
private void injectStealthScript(String cookieId) {
String script = """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
window.chrome = {
runtime: {}
};
""";
page.addInitScript(script);
log.debug("【{}】反检测脚本已注入", cookieId);
}
/**
* 导航到验证页面
*/
private void navigateToCaptchaPage(String url, String cookieId) {
log.info("【{}】导航到验证页面...", cookieId);
page.navigate(url, new Page.NavigateOptions().setTimeout(30000));
log.info("【{}】页面加载完成", cookieId);
}
/**
* 查找滑块元素
*/
private ElementHandle findSliderElement(String cookieId) {
// 多种选择器策略
List<String> selectors = Arrays.asList(
"#nc_1_n1z",
".nc-lang-cnt",
"[id^='nc_'][id$='_n1z']",
".btn_slide"
);
for (String selector : selectors) {
try {
ElementHandle element = page.querySelector(selector);
if (element != null) {
log.info("【{}】找到滑块元素: {}", cookieId, selector);
return element;
}
} catch (Exception e) {
log.debug("【{}】选择器{}未找到", cookieId, selector);
}
}
log.warn("【{}】未找到滑块元素", cookieId);
return null;
}
/**
* 计算移动距离
*/
private int calculateDistance(ElementHandle slider, String cookieId) {
// 获取滑块位置信息
BoundingBox box = slider.boundingBox();
// 简化计算移动到右侧实际应该根据页面计算
int distance = 300; // 默认300px
log.info("【{}】计算移动距离: {}px", cookieId, distance);
return distance;
}
/**
* 拖动滑块
*/
private void dragSlider(ElementHandle slider, int distance, String cookieId) throws InterruptedException {
log.info("【{}】开始拖动滑块...", cookieId);
BoundingBox box = slider.boundingBox();
double startX = box.x + box.width / 2;
double startY = box.y + box.height / 2;
// 移动到滑块
page.mouse().move(startX, startY);
page.mouse().down();
// 简单拖动后续优化为贝塞尔曲线
int steps = 3;
for (int i = 1; i <= steps; i++) {
double x = startX + (distance * i / (double) steps);
page.mouse().move(x, startY);
}
page.mouse().up();
log.info("【{}】滑块拖动完成", cookieId);
}
/**
* 检查验证是否成功
*/
private boolean checkSuccess(String cookieId) {
try {
// 等待成功提示
page.waitForSelector(".nc-lang-cnt:has-text('验证通过')",
new Page.WaitForSelectorOptions().setTimeout(5000));
log.info("【{}】检测到验证成功提示", cookieId);
return true;
} catch (Exception e) {
log.warn("【{}】未检测到验证成功", cookieId);
return false;
}
}
/**
* 提取cookies
*/
private Map<String, String> extractCookies(String cookieId) {
log.info("【{}】提取验证后的cookies...", cookieId);
List<Cookie> cookies = context.cookies();
Map<String, String> result = new HashMap<>();
// 只提取x5相关cookies
for (Cookie cookie : cookies) {
String name = cookie.name.toLowerCase();
if (name.startsWith("x5") || name.contains("x5sec")) {
result.put(cookie.name, cookie.value);
log.info("【{}】提取x5 cookie: {}", cookieId, cookie.name);
}
}
log.info("【{}】成功提取{}个x5 cookies", cookieId, result.size());
return result;
}
/**
* 清理资源
*/
private void cleanup(String cookieId) {
try {
if (page != null) {
page.close();
}
if (context != null) {
context.close();
}
if (browser != null) {
browser.close();
}
if (playwright != null) {
playwright.close();
}
log.info("【{}】浏览器资源已清理", cookieId);
} catch (Exception e) {
log.warn("【{}】清理资源时出错", cookieId, e);
}
}
}

View File

@ -0,0 +1,31 @@
package com.xianyu.autoreply.service.captcha.model;
import lombok.Data;
import java.util.Map;
/**
* 滑块验证结果
*/
@Data
public class CaptchaResult {
private boolean success;
private String message;
private Map<String, String> cookies;
private long duration; // 处理耗时毫秒
public static CaptchaResult success(Map<String, String> cookies, long duration) {
CaptchaResult result = new CaptchaResult();
result.setSuccess(true);
result.setMessage("滑块验证成功");
result.setCookies(cookies);
result.setDuration(duration);
return result;
}
public static CaptchaResult failure(String message) {
CaptchaResult result = new CaptchaResult();
result.setSuccess(false);
result.setMessage(message);
return result;
}
}

View File

@ -0,0 +1,15 @@
package com.xianyu.autoreply.service.captcha.model;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 轨迹点
*/
@Data
@AllArgsConstructor
public class TrajectoryPoint {
private int x; // X坐标
private int y; // Y坐标
private double delay; // 延迟
}