init
This commit is contained in:
parent
d15443d0e3
commit
7b2991bc84
@ -0,0 +1,18 @@
|
||||
package top.biwin.xinayu.common.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 17:07
|
||||
*/
|
||||
@Data
|
||||
public class ChangePasswordRequest {
|
||||
@JsonProperty("current_password")
|
||||
private String currentPassword;
|
||||
@JsonProperty("new_password")
|
||||
private String newPassword;
|
||||
}
|
||||
@ -38,6 +38,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class BrowserService {
|
||||
@Value("${browser.persistence-data}")
|
||||
private String persistenceData;
|
||||
@Value("${browser.use-installed-browser}")
|
||||
private Boolean useInstalledBrowser;
|
||||
@Value("${browser.location}")
|
||||
private String browserLocation;
|
||||
@Value("${browser.ua}")
|
||||
@ -68,12 +70,21 @@ public class BrowserService {
|
||||
BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions()
|
||||
.setHeadless(false)
|
||||
.setArgs(args);
|
||||
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
String osArch = System.getProperty("os.arch").toLowerCase();
|
||||
if (osName.contains("mac") && osArch.contains("aarch64")) {
|
||||
Path chromePath = Paths.get("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||
if (chromePath.toFile().exists()) {
|
||||
if (Objects.equals(Boolean.TRUE, useInstalledBrowser)) {
|
||||
Path chromePath = Paths.get(browserLocation);
|
||||
if (!chromePath.toFile().exists()) {
|
||||
throw new IllegalArgumentException("系统启动失败,配置的浏览器软件路径不存在: " + browserLocation);
|
||||
}
|
||||
launchOptions.setExecutablePath(chromePath);
|
||||
} else {
|
||||
log.debug("尝试判断是否是开发机,直接强制使用安装的正式浏览器...");
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
String osArch = System.getProperty("os.arch").toLowerCase();
|
||||
if (osName.contains("mac") && (osArch.contains("aarch64") || osArch.contains("x86_64"))) {
|
||||
Path chromePath = Paths.get("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||
if (!chromePath.toFile().exists()) {
|
||||
throw new IllegalArgumentException("系统启动失败,请先主动安装 Chrome 浏览器!");
|
||||
}
|
||||
launchOptions.setExecutablePath(chromePath);
|
||||
}
|
||||
}
|
||||
@ -284,7 +295,7 @@ public class BrowserService {
|
||||
|
||||
public void closePersistentContext(String goofishId) {
|
||||
BrowserContext browserContext = persistentContexts.get(goofishId);
|
||||
if(Objects.isNull(browserContext)) {
|
||||
if (Objects.isNull(browserContext)) {
|
||||
return;
|
||||
}
|
||||
browserContext.close();
|
||||
|
||||
@ -15,6 +15,7 @@ import top.biwin.xianyu.goofish.util.CookieUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static top.biwin.xianyu.goofish.BrowserConstant.STEALTH_SCRIPT;
|
||||
import static top.biwin.xianyu.goofish.util.SliderUtils.attemptSolveSlider;
|
||||
@ -50,7 +51,9 @@ public class GoofishPwdLoginService {
|
||||
log.debug("【{}】正在导航至登录页... url: http://www.goofish.com/im", account);
|
||||
page.navigate("https://www.goofish.com/im");
|
||||
log.debug("【{}】等待页面加载,查找登录框... url: http://www.goofish.com/im", account);
|
||||
// 确保页面加载完成
|
||||
page.getByText("登录后可以更懂你,推荐你喜欢的商品!");
|
||||
|
||||
Frame loginFrame = findLoginFrame(page, account);
|
||||
if (Objects.nonNull(loginFrame)) {
|
||||
// 开始登录
|
||||
@ -80,8 +83,18 @@ public class GoofishPwdLoginService {
|
||||
// 这里点击后,可能并不能成功,会出现滑块
|
||||
if (Objects.nonNull(findLoginFrame(page, account))) {
|
||||
// 内部应该要有重试处理滑块的逻辑
|
||||
boolean resolveResult = attemptSolveSlider(loginFrame, account);
|
||||
boolean resolveResult = attemptSolveSlider(loginFrame, account, new AtomicInteger(3));
|
||||
log.debug("【{}】滑块结果: {}!", account, resolveResult ? "成功" : "失败");
|
||||
if(resolveResult) {
|
||||
log.debug("【{}】验证是否登录成功...", account);
|
||||
Frame lastCheckFrame = findLoginFrame(page, account);
|
||||
if(Objects.nonNull(lastCheckFrame)) {
|
||||
log.debug("【{}】登录失败,仍存在登录页面...", account);
|
||||
}
|
||||
} else {
|
||||
log.error("【{}】账号密码登录失败", account);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
@ -3,11 +3,14 @@ package top.biwin.xianyu.goofish.util;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.microsoft.playwright.ElementHandle;
|
||||
import com.microsoft.playwright.Frame;
|
||||
import com.microsoft.playwright.Mouse;
|
||||
import com.microsoft.playwright.options.BoundingBox;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 滑块验证码处理工具类
|
||||
@ -29,7 +32,7 @@ public final class SliderUtils {
|
||||
* @param goofishId 闲鱼账号ID(用于日志)
|
||||
* @return 是否成功解决滑块
|
||||
*/
|
||||
public static boolean attemptSolveSlider(Frame frame, String goofishId) {
|
||||
public static boolean attemptSolveSlider(Frame frame, String goofishId, AtomicInteger maxRetryCount) {
|
||||
try {
|
||||
String[] sliderSelectors = {"#nc_1_n1z", ".nc-container", ".nc_scale", ".nc-wrapper"};
|
||||
ElementHandle sliderButton = null;
|
||||
@ -46,7 +49,7 @@ public final class SliderUtils {
|
||||
// 如果当前Frame没有滑块,递归检查子Frame
|
||||
if (!containerFound && CollUtil.isNotEmpty(frame.childFrames())) {
|
||||
for (Frame childFrame : frame.childFrames()) {
|
||||
return attemptSolveSlider(childFrame, goofishId);
|
||||
return attemptSolveSlider(childFrame, goofishId, maxRetryCount);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +60,7 @@ public final class SliderUtils {
|
||||
if (sliderButton == null) sliderButton = frame.querySelector(".nc_iconfont");
|
||||
|
||||
if (sliderButton != null && sliderButton.isVisible()) {
|
||||
log.info("【Login Task】检测到滑块验证,Frame: {}", frame.url());
|
||||
log.info("【{}-处理滑块】检测到滑块验证,Frame: {}", goofishId, frame.url());
|
||||
BoundingBox box = sliderButton.boundingBox();
|
||||
if (box == null) return false;
|
||||
|
||||
@ -68,24 +71,34 @@ public final class SliderUtils {
|
||||
|
||||
BoundingBox trackBox = track.boundingBox();
|
||||
double distance = trackBox.width - box.width;
|
||||
log.info("【Login Task】准备解决滑块: 距离={}px", distance);
|
||||
log.info("【{}-处理滑块】准备解决滑块: 距离={}px", goofishId, distance);
|
||||
|
||||
// 执行人类模拟滑动(丝滑版)
|
||||
boolean success = performSmoothSlide(frame, box, distance);
|
||||
boolean success = performSmoothSlide(frame, box, distance, goofishId);
|
||||
|
||||
// 等待验证结果
|
||||
Thread.sleep(800 + ThreadLocalRandom.current().nextInt(400));
|
||||
|
||||
// 检查滑块是否消失(成功标志)
|
||||
if (!sliderButton.isVisible()) {
|
||||
log.info("【Login Task】滑块验证成功!(按钮已消失)");
|
||||
return true;
|
||||
// // 检查滑块是否消失(成功标志)
|
||||
// if (!sliderButton.isVisible()) {
|
||||
// log.info("【{}-处理滑块】滑块验证成功!(按钮已消失)", goofishId);
|
||||
// return true;
|
||||
// }
|
||||
ElementHandle elementHandle = frame.querySelector("#nc_1_refresh1");
|
||||
if (Objects.nonNull(elementHandle)) {
|
||||
log.info("【{}-处理滑块】滑块验证失败,点击滑块重试!", goofishId);
|
||||
elementHandle.click();
|
||||
if (maxRetryCount.decrementAndGet() > 0) {
|
||||
return attemptSolveSlider(frame, goofishId, maxRetryCount);
|
||||
} else {
|
||||
log.error("【{}-处理滑块】重试次数过多,放弃解决!", goofishId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("【Login Task】解决滑块时出错: {}", e.getMessage());
|
||||
log.warn("【{}-处理滑块】解决滑块时出错: {}", goofishId, e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -105,7 +118,7 @@ public final class SliderUtils {
|
||||
* @param distance 需要滑动的距离
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
private static boolean performSmoothSlide(Frame frame, BoundingBox buttonBox, double distance) {
|
||||
private static boolean performSmoothSlide(Frame frame, BoundingBox buttonBox, double distance, String goofishId) {
|
||||
try {
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
|
||||
@ -129,7 +142,7 @@ public final class SliderUtils {
|
||||
List<BrowserTrajectoryUtils.TrajectoryPoint> trajectory =
|
||||
BrowserTrajectoryUtils.generateFastTrajectory(distance);
|
||||
|
||||
log.debug("【Login Task】生成轨迹点数: {}", trajectory.size());
|
||||
log.debug("【{}-处理滑块】生成轨迹点数: {}", goofishId, trajectory.size());
|
||||
|
||||
// 步骤6:一气呵成快速滑动!!不加任何sleep!!
|
||||
for (BrowserTrajectoryUtils.TrajectoryPoint point : trajectory) {
|
||||
@ -140,11 +153,11 @@ public final class SliderUtils {
|
||||
// 步骤7:释放鼠标
|
||||
frame.page().mouse().up();
|
||||
|
||||
log.info("【Login Task】滑动执行完成,等待验证结果...");
|
||||
log.info("【{}-处理滑块】滑动执行完成,等待验证结果...", goofishId);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【Login Task】执行滑动时出错: {}", e.getMessage());
|
||||
log.error("【{}-处理滑块】执行滑动时出错: {}", goofishId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ package top.biwin.xinayu.server.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@ -12,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
import top.biwin.xianyu.core.repository.AdminUserRepository;
|
||||
import top.biwin.xinayu.common.dto.request.ChangePasswordRequest;
|
||||
import top.biwin.xinayu.common.dto.request.LoginCaptchaRequest;
|
||||
import top.biwin.xinayu.common.dto.request.LoginRequest;
|
||||
import top.biwin.xinayu.common.dto.request.RefreshRequest;
|
||||
@ -31,8 +34,11 @@ import top.biwin.xinayu.server.util.CurrentUserUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.awt.SystemColor.info;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* 提供登录、刷新令牌等认证相关的 REST API
|
||||
@ -46,6 +52,7 @@ import java.util.Optional;
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping
|
||||
public class AuthController {
|
||||
@ -65,6 +72,8 @@ public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private CaptchaService captchaService;
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* 用户登录接口
|
||||
@ -285,21 +294,50 @@ public class AuthController {
|
||||
* 对应 Python: /api/check-default-password
|
||||
*/
|
||||
@GetMapping("/api/check-default-password")
|
||||
public ResponseEntity<CheckDefaultPwdResponse> checkDefaultPassword(HttpServletRequest httpRequest) {
|
||||
public ResponseEntity<CheckDefaultPwdResponse> checkDefaultPassword() {
|
||||
|
||||
CheckDefaultPwdResponse build = CheckDefaultPwdResponse.builder()
|
||||
.usingDefault(false)
|
||||
.success(true)
|
||||
.build();
|
||||
CheckDefaultPwdResponse build = CheckDefaultPwdResponse.builder()
|
||||
.usingDefault(false)
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
if(!CurrentUserUtil.isSuperAdmin()) {
|
||||
if (!CurrentUserUtil.isSuperAdmin()) {
|
||||
return ResponseEntity.ok(build);
|
||||
}
|
||||
|
||||
AdminUserEntity adminUserEntity = authService.verifyUserPassword(ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD);
|
||||
build.setUsingDefault( adminUserEntity != null);
|
||||
build.setUsingDefault(adminUserEntity != null);
|
||||
return ResponseEntity.ok(build);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public ResponseEntity<BaseResponse> changeUserPassword(@RequestBody ChangePasswordRequest request) {
|
||||
AdminUserEntity currentUser = CurrentUserUtil.getCurrentUser();
|
||||
if (Objects.isNull(currentUser)) {
|
||||
return ResponseEntity.ok(new BaseResponse("用户未登录", false));
|
||||
}
|
||||
AdminUserEntity adminUserEntity = authService.verifyUserPassword(currentUser.getUsername(), request.getCurrentPassword());
|
||||
if ( Objects.isNull(adminUserEntity)) {
|
||||
return ResponseEntity.ok(new BaseResponse("当前密码错误", false));
|
||||
}
|
||||
|
||||
if(passwordEncoder.matches(JwtUtil.generatePasswordHash(request.getNewPassword()), adminUserEntity.getPasswordHash())) {
|
||||
return ResponseEntity.ok(new BaseResponse("新密码与旧密码相同", false));
|
||||
}
|
||||
|
||||
boolean success = authService.updateUserPassword(currentUser.getUsername(), request.getNewPassword());
|
||||
BaseResponse response = new BaseResponse();
|
||||
if (success) {
|
||||
log.info("【{}#{}】用户密码修改成功", adminUserEntity.getUsername(), adminUserEntity.getId());
|
||||
response.setMessage("密码修改成功");
|
||||
response.setSuccess(true);
|
||||
} else {
|
||||
response.setSuccess(false);
|
||||
response.setMessage("密码修改失败");
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -176,6 +176,10 @@ public class JwtUtil {
|
||||
return accessTokenExpiration / 1000;
|
||||
}
|
||||
|
||||
public static String generatePasswordHash(String password) {
|
||||
return new BCryptPasswordEncoder(12).encode(password);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
|
||||
String hash = encoder.encode("123456");
|
||||
|
||||
@ -230,4 +230,12 @@ public class AuthService {
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public boolean updateUserPassword(String username, String newPassword) {
|
||||
AdminUserEntity adminUserEntity = adminUserRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new IllegalStateException("用户不存在"));
|
||||
adminUserEntity.setPasswordHash(JwtUtil.generatePasswordHash(newPassword));
|
||||
adminUserRepository.save(adminUserEntity);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
app:
|
||||
ddl-auto: update # valid values: none, validate, update, create, create-drop
|
||||
ddl-auto: create # valid values: none, validate, update, create, create-drop
|
||||
# 应用验证码配置
|
||||
verification:
|
||||
code-length: 6 # 验证码长度(6位数字)
|
||||
@ -14,7 +14,7 @@ spring:
|
||||
|
||||
datasource:
|
||||
driver-class-name: org.sqlite.JDBC
|
||||
url: jdbc:sqlite:./db/xianyu_data.db
|
||||
url: jdbc:sqlite:db/xianyu_data.db
|
||||
username:
|
||||
password:
|
||||
|
||||
@ -69,9 +69,11 @@ goofish:
|
||||
assistant:
|
||||
static:
|
||||
uploads:
|
||||
images : static/uploads/images/
|
||||
images: static/uploads/images/
|
||||
|
||||
browser:
|
||||
# 使用已安装的浏览器,否则系统会尝试下载 playwright 绑定版本的浏览器,如果设置为 false,则系统首次启动会比较久
|
||||
use-installed-browser: true
|
||||
# chrome 浏览器软件安装位置
|
||||
location: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
||||
# 浏览器登录状态数据存储目录
|
||||
|
||||
Loading…
Reference in New Issue
Block a user