From ba5c07746a7b239be609ebe050e40018866fc3d1 Mon Sep 17 00:00:00 2001 From: Hydrogen <1620682458@qq.com> Date: Mon, 18 Aug 2025 00:15:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(gui,login):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 +- build.gradle | 2 + gradle.properties | 1 + .../innovators/box/AxisInnovatorsBox.java | 58 +- .../innovators/box/tools/LoadResource.java | 42 ++ .../box/util/UserLocalInformation.java | 47 -- .../box/verification/CasdoorServer.java | 102 ++++ .../box/verification/LoginData.java | 21 + .../box/verification/LoginResult.java | 28 + .../box/verification/OnlineVerification.java | 33 -- .../innovators/box/verification/Result.java | 25 + .../innovators/box/verification/UserTags.java | 46 -- .../box/verification/VerificationService.java | 38 -- .../box/verification/api/UserApi.java | 89 +++ .../box/verification/api/UserApiImpl.java | 65 +++ .../box/verification/api/UserApiResult.java | 25 + .../box/window/CasdoorLoginWindow.java | 268 +++++++++ .../innovators/box/window/LoginWindow.java | 539 ------------------ src/main/java/config/CasdoorConfig.java | 17 + src/main/resources/cert/casdoor_cert.pem | 29 + state/toolbox.properties | 0 21 files changed, 761 insertions(+), 723 deletions(-) create mode 100644 gradle.properties create mode 100644 src/main/java/com/axis/innovators/box/tools/LoadResource.java delete mode 100644 src/main/java/com/axis/innovators/box/util/UserLocalInformation.java create mode 100644 src/main/java/com/axis/innovators/box/verification/CasdoorServer.java create mode 100644 src/main/java/com/axis/innovators/box/verification/LoginData.java create mode 100644 src/main/java/com/axis/innovators/box/verification/LoginResult.java delete mode 100644 src/main/java/com/axis/innovators/box/verification/OnlineVerification.java create mode 100644 src/main/java/com/axis/innovators/box/verification/Result.java delete mode 100644 src/main/java/com/axis/innovators/box/verification/UserTags.java delete mode 100644 src/main/java/com/axis/innovators/box/verification/VerificationService.java create mode 100644 src/main/java/com/axis/innovators/box/verification/api/UserApi.java create mode 100644 src/main/java/com/axis/innovators/box/verification/api/UserApiImpl.java create mode 100644 src/main/java/com/axis/innovators/box/verification/api/UserApiResult.java create mode 100644 src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java delete mode 100644 src/main/java/com/axis/innovators/box/window/LoginWindow.java create mode 100644 src/main/java/config/CasdoorConfig.java create mode 100644 src/main/resources/cert/casdoor_cert.pem create mode 100644 state/toolbox.properties diff --git a/.gitignore b/.gitignore index b63da45..968cfb8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,11 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +### logs ### +*.log +logs/ + +### JCEF Dlls ### +library/jcef/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3c9ee76..6f8a5a8 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ configurations { proguardLib } + // JDK 版本检查 def requiredJavaVersion = 20 def currentJavaVersion = JavaVersion.current().majorVersion.toInteger() @@ -113,6 +114,7 @@ dependencies { implementation 'jflac:jflac:1.3' implementation 'com.github.axet:TarsosDSP:2.4' implementation 'org.json:json:20231013' + implementation 'org.casbin:casdoor-java-sdk:1.37.0' } configurations.all { diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..195297f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.home=E:\\Softwares\\Java\\jdk-20.0.2 \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java index 33467b6..d204f06 100644 --- a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java +++ b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java @@ -14,8 +14,9 @@ import com.axis.innovators.box.register.RegistrationTopic; import com.axis.innovators.box.tools.*; import com.axis.innovators.box.util.PythonResult; import com.axis.innovators.box.util.Tray; -import com.axis.innovators.box.util.UserLocalInformation; -import com.axis.innovators.box.verification.UserTags; +import com.axis.innovators.box.verification.LoginResult; +import com.axis.innovators.box.verification.CasdoorServer; +import com.axis.innovators.box.verification.LoginData; import com.formdev.flatlaf.themes.FlatMacDarkLaf; import com.formdev.flatlaf.themes.FlatMacLightLaf; import com.sun.management.HotSpotDiagnosticMXBean; @@ -31,6 +32,7 @@ import org.apache.logging.log4j.core.appender.FileAppender; import org.apache.logging.log4j.core.appender.RollingFileAppender; import org.apache.logging.log4j.core.config.Configuration; import org.api.dog.agent.VirtualMachine; +import org.casbin.casdoor.entity.User; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -58,7 +60,8 @@ public class AxisInnovatorsBox { private static final Logger logger = LogManager.getLogger(AxisInnovatorsBox.class); private static final String VERSIONS = "0.1.2"; private static final String[] AUTHOR = new String[]{ - "tzdwindows 7" + "tzdwindows 7", + "lyxyz5223", }; /** 我是总任务数 **/ @@ -77,10 +80,11 @@ public class AxisInnovatorsBox { private final RegistrationTopic registrationTopic = new RegistrationTopic(this); private final List windowsJDialogList = new ArrayList<>(); private final StateManager stateManager = new StateManager(); - private UserTags userTags; private final boolean isDebug; private static DebugWindow debugWindow; + private LoginData loginData; + public AxisInnovatorsBox(String[] args, boolean isDebug) { this.args = args; this.isDebug = isDebug; @@ -88,6 +92,37 @@ public class AxisInnovatorsBox { organizingCrashReports(throwable instanceof Exception ? (Exception) throwable : new Exception(throwable)); }); + + // 加载登录信息,如果没有,弹出登录弹窗,后续可以删掉默认弹出 + // TODO: login window + try { + StateManager stateManager = new StateManager(); + String token = stateManager.getState("loginToken"); + if (token == null || token.isEmpty()) { + LoginResult loginResult = CasdoorLoginWindow.showLoginDialogAndGetLoginResult(); + if (loginResult == null) { + // 用户取消登录 + JOptionPane.showMessageDialog(null, "取消登录", "登录", + JOptionPane.INFORMATION_MESSAGE); + } else if (loginResult.success()) { + loginData = loginResult.loginData(); + stateManager.saveState("loginToken", loginResult.token()); + logger.info("Login result: token: " + loginResult.token() + ", user: " + loginResult.user()); + JOptionPane.showMessageDialog(null, "登录成功", "登录", + JOptionPane.INFORMATION_MESSAGE); + } else { + // 登录失败,弹出错误提醒,这里只是输出登录错误信息 + logger.error("Login error: " + loginResult.message()); + JOptionPane.showMessageDialog(null, "登录失败: \n" + loginResult.message(), "登录失败", JOptionPane.ERROR_MESSAGE); + } + } else { + CasdoorServer casdoorServer = new CasdoorServer(); + User user = casdoorServer.parseJwtToken(token); + loginData = new LoginData(token, user); + } + } catch (Exception e) { + logger.error("Failed to load login information", e); + } } /** @@ -959,14 +994,6 @@ public class AxisInnovatorsBox { main.thread = new Thread(() -> { try { - UserLocalInformation userLocalInformation = new UserLocalInformation(main); - main.userTags = userLocalInformation.getUserTags(); - if (main.userTags == null) { - // 登录窗口 - main.userTags = LoginWindow.createAndShow(); - userLocalInformation.setUserTags(main.userTags); - } - // 主任务1:加载插件 logger.info("Loaded plugins Started"); main.progressBarManager.updateMainProgress(++main.completedTasks); @@ -1092,13 +1119,6 @@ public class AxisInnovatorsBox { return AUTHOR; } - /** - * 获取用户标签 - * @return 用户标签 - */ - public UserTags getUserTags() { - return userTags; - } /** * 获取状态管理器 diff --git a/src/main/java/com/axis/innovators/box/tools/LoadResource.java b/src/main/java/com/axis/innovators/box/tools/LoadResource.java new file mode 100644 index 0000000..e97b082 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/tools/LoadResource.java @@ -0,0 +1,42 @@ +package com.axis.innovators.box.tools; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.apache.logging.log4j.LogManager; + +public class LoadResource { + + public static String readString(String srcPath) { + try { + java.net.URL url = LoadResource.class.getResource(srcPath); + if (url == null) { + LogManager.getLogger(LoadResource.class).error("资源文件未找到: " + srcPath); + return null; + } + java.nio.file.Path path = java.nio.file.Paths.get(url.toURI()); + return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } catch (Exception e) { + LogManager.getLogger(LoadResource.class).error("读取资源文件失败", e); + return null; + } + } + + public static String loadString(Class clazz, String srcPath) { + try { + java.net.URL url = clazz.getResource(srcPath); + if (url == null) { + LogManager.getLogger(LoadResource.class).error("资源文件未找到: " + srcPath); + return null; + } + java.nio.file.Path path = java.nio.file.Paths.get(url.toURI()); + return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } catch (Exception e) { + LogManager.getLogger(LoadResource.class).error("读取资源文件失败", e); + return null; + } + + } +} diff --git a/src/main/java/com/axis/innovators/box/util/UserLocalInformation.java b/src/main/java/com/axis/innovators/box/util/UserLocalInformation.java deleted file mode 100644 index 91ea57e..0000000 --- a/src/main/java/com/axis/innovators/box/util/UserLocalInformation.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.axis.innovators.box.util; - -import com.axis.innovators.box.AxisInnovatorsBox; -import com.axis.innovators.box.verification.OnlineVerification; -import com.axis.innovators.box.verification.UserTags; -import com.axis.innovators.box.verification.VerificationService; - -/** - * 用于存储用户信息 - * @author tzdwindows 7 - */ -public class UserLocalInformation { - private final AxisInnovatorsBox main; - - public UserLocalInformation(AxisInnovatorsBox main){ - this.main = main; - } - - /** - * 设置用户信息 - * @param userTags 用户信息 - */ - public void setUserTags(UserTags userTags){ - OnlineVerification onlineVerification = userTags.getUser(); - main.getStateManager().saveState("password", onlineVerification.password); - main.getStateManager().saveState("verification", onlineVerification.onlineVerification); - } - - /** - * 获取用户信息 - * @return 用户信息 - */ - public UserTags getUserTags(){ - String verification = main.getStateManager().getState("verification"); - String password = main.getStateManager().getState("password"); - if (verification == null || password == null){ - return null; - } - OnlineVerification onlineVerification = OnlineVerification.validateLogin( - verification, - password); - if (onlineVerification == null){ - return null; - } - return VerificationService.determineUserType(onlineVerification); - } -} diff --git a/src/main/java/com/axis/innovators/box/verification/CasdoorServer.java b/src/main/java/com/axis/innovators/box/verification/CasdoorServer.java new file mode 100644 index 0000000..8dc05bc --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/CasdoorServer.java @@ -0,0 +1,102 @@ +package com.axis.innovators.box.verification; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.casbin.casdoor.config.Config; +import org.casbin.casdoor.entity.User; +import org.casbin.casdoor.service.AuthService; + +import com.axis.innovators.box.tools.LoadResource; + +import config.CasdoorConfig; + +public class CasdoorServer { + private static final Logger logger = LogManager.getLogger(CasdoorServer.class); + + private final AuthService authService; + private final Config config; + private final String certificate; + + public CasdoorServer() { + this.certificate = LoadResource.readString("/cert/casdoor_cert.pem"); + this.config = new Config( + CasdoorConfig.CASDOOR_API_URL, + CasdoorConfig.CASDOOR_CLIENT_ID, + CasdoorConfig.CASDOOR_CLIENT_SECRET, + this.certificate, + CasdoorConfig.CASDOOR_ORGANIZATION_NAME, + CasdoorConfig.CASDOOR_APPLICATION_NAME); + this.authService = new AuthService(this.config); + } + + public AuthService getAuthService() { + return authService; + } + + public Config getConfig() { + return config; + } + + public String getCertificate() { + return certificate; + } + + /** + * 获取登录 URL + * @return 登录 URL + */ + public String getSigninUrl() { + return authService.getSigninUrl(CasdoorConfig.CASDOOR_LOGIN_REDIRECT_URI); + } + + /** + * 获取注册 URL + * @return 注册 URL + */ + public String getSignupUrl() { + String redirectUrl = CasdoorConfig.CASDOOR_SIGNUP_REDIRECT_URI; + if (redirectUrl == null) { + redirectUrl = getSigninUrl(); // 如果没有设置注册回调地址,则使用登录回调地址 + } + return authService.getSignupUrl(redirectUrl); + } + + /** + * 获取 OAuth Token + * @param code 登录回调的 code 参数 + * @param state 登录回调的 state 参数 + * @return + */ + public String getOAuthToken(String code, String state) { + try { + return authService.getOAuthToken(code, state); + } catch (Exception e) { + logger.error("获取 OAuth Token 失败: " + e.getMessage()); + return null; + } + } + + /** + * 解析 JWT Token + * @param token 令牌 + * @return User 用户信息 + */ + public User parseJwtToken(String token) { + try { + return authService.parseJwtToken(token); + } catch (Exception e) { + logger.error("解析 JWT Token 失败: " + e.getMessage()); + return null; + } + } + + /** + * 获取用户信息,同 parseJwtToken + * @param token 令牌 + * @return User 用户信息 + */ + public User getUserInfo(String token) { + return parseJwtToken(token); + } + +} diff --git a/src/main/java/com/axis/innovators/box/verification/LoginData.java b/src/main/java/com/axis/innovators/box/verification/LoginData.java new file mode 100644 index 0000000..2f0fee1 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/LoginData.java @@ -0,0 +1,21 @@ +package com.axis.innovators.box.verification; + +import org.casbin.casdoor.entity.User; + +public class LoginData { + private String token; + private User user; + + public LoginData(String token, User user) { + this.token = token; + this.user = user; + } + + public String getToken() { + return token; + } + + public User getUser() { + return user; + } +} diff --git a/src/main/java/com/axis/innovators/box/verification/LoginResult.java b/src/main/java/com/axis/innovators/box/verification/LoginResult.java new file mode 100644 index 0000000..b3400af --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/LoginResult.java @@ -0,0 +1,28 @@ +package com.axis.innovators.box.verification; + +import org.casbin.casdoor.entity.User; + +public class LoginResult extends Result { + + public LoginResult(boolean success, String message, LoginData data) { + super(success, message, data); + } + + public String token() { + LoginData loginData = (LoginData)this.m_data; + return loginData != null ? loginData.getToken() : null; + } + + public User user() { + LoginData loginData = (LoginData) this.m_data; + return loginData != null ? loginData.getUser() : null; + } + + public LoginData loginData() { + return (LoginData) this.m_data; + } + + public LoginData data() { + return (LoginData) this.m_data; + } +} diff --git a/src/main/java/com/axis/innovators/box/verification/OnlineVerification.java b/src/main/java/com/axis/innovators/box/verification/OnlineVerification.java deleted file mode 100644 index 56febc3..0000000 --- a/src/main/java/com/axis/innovators/box/verification/OnlineVerification.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.axis.innovators.box.verification; - -/** - * 在线验证用户身份 - * 用于报错用户的验证信息 - * @author tzdwindows 7 - */ -public class OnlineVerification { - public String onlineVerification; - public String password; - /* 我是错误信息,要返回错误请修改我 */ - public static String errorMessage = "用户不存在"; - - /** - * 验证登录 - * @param identifier 账号 - * @param password 密码 - */ - OnlineVerification(String identifier, String password){ - this.onlineVerification = identifier; - this.password = password; - } - - /** - * 验证登录 - * @param identifier 账号 - * @param password 密码 - * @return 验证结果,如果返回null则表示验证失败,使用errorMessage获取验证失败的原因 - */ - public static OnlineVerification validateLogin(String identifier, String password){ - return new OnlineVerification(identifier, password); - } -} diff --git a/src/main/java/com/axis/innovators/box/verification/Result.java b/src/main/java/com/axis/innovators/box/verification/Result.java new file mode 100644 index 0000000..71319f4 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/Result.java @@ -0,0 +1,25 @@ +package com.axis.innovators.box.verification; + +public class Result { + protected final boolean m_success; + protected final String m_message; + protected final Object m_data; + + public Result(boolean success, String message, Object data) { + this.m_success = success; + this.m_message = message; + this.m_data = data; + } + + public boolean success() { + return m_success; + } + + public String message() { + return m_message; + } + + public Object data() { + return m_data; + } +} diff --git a/src/main/java/com/axis/innovators/box/verification/UserTags.java b/src/main/java/com/axis/innovators/box/verification/UserTags.java deleted file mode 100644 index 2d5d5f4..0000000 --- a/src/main/java/com/axis/innovators/box/verification/UserTags.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.axis.innovators.box.verification; - -/** - * 用户标签组 - * @author tzdwindows 7 - */ -public enum UserTags { - /** - * 没有登录的标签 - */ - None, - /** - * 普通用户标签 - */ - RegularUsers, - /** - * 管理员标签 - */ - AdminUsers, - /** - * VIP用户标签 - */ - VipUsers, - /** - * SVip用户标签 - */ - SVipUsers, - /** - * 企业用户标签 - */ - EnterpriseUsers; - - private OnlineVerification onlineVerification; - - /** - * 设置用户组信息 - * @param onlineVerification 用户验证结果信息 - */ - void setUser(OnlineVerification onlineVerification) { - this.onlineVerification = onlineVerification; - } - - public OnlineVerification getUser() { - return onlineVerification; - } -} diff --git a/src/main/java/com/axis/innovators/box/verification/VerificationService.java b/src/main/java/com/axis/innovators/box/verification/VerificationService.java deleted file mode 100644 index 0dd038f..0000000 --- a/src/main/java/com/axis/innovators/box/verification/VerificationService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.axis.innovators.box.verification; - -/** - * @author tzdwindows 7 - */ -public class VerificationService { - - /** - * 确定用户类型 - * @param identifier 用户 - * @return 用户类型 - */ - public static UserTags determineUserType(OnlineVerification identifier) { - UserTags userTags = UserTags.RegularUsers; - userTags.setUser(identifier); - return userTags; - } - - /** - * 发送密码重置链接给用户 - * @param text - * @return - */ - public static boolean sendPasswordReset(String text) { - return true; - } - - /** - * 注册用户 - * @param text - * @param text1 - * @param pwd - * @return - */ - public static boolean registerUser(String text, String text1, String pwd) { - return true; - } -} diff --git a/src/main/java/com/axis/innovators/box/verification/api/UserApi.java b/src/main/java/com/axis/innovators/box/verification/api/UserApi.java new file mode 100644 index 0000000..c706f55 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/api/UserApi.java @@ -0,0 +1,89 @@ +package com.axis.innovators.box.verification.api; + +import java.util.concurrent.CompletableFuture; + +/** + * 用户API接口 + * @author lyxyz5223 + */ +public interface UserApi { + /** + * 发送验证码 + * @param email 用户邮箱 + * @return 发送结果 + */ + CompletableFuture sendVerificationCode(String email); + + /** + * 验证验证码 + * @param email 用户邮箱 + * @param verificationCode 验证码 + * @return 验证结果 + */ + CompletableFuture verifyCode(String email, String verificationCode); + + /** + * 检验登录token有效性,用于检验登录状态 + * @param token 登录token + * @return 检验结果 + */ + CompletableFuture checkTokenValidity(String token); + + /** + * 申请新的token,可以定期申请新的token保证安全性 + * @param oldToken 旧的登录token + * @return 申请结果 + */ + CompletableFuture applyNewToken(String oldToken); + + /** + * 登录接口 + * @param username 用户名 + * @param password 密码(加密) + * @return { token: String, expiresIn: Number } 登录结果(含token和有效期,临近到期需要手动申请新的token,每次登陆服务器将更新token有效期) + */ + CompletableFuture login(String username, String password); + + /** + * 登出接口 + * @param username 用户名 + * @return 登出结果 + */ + CompletableFuture logout(String username); + + /** + * 注册接口 + * @param username 用户名 + * @param password 密码(加密) + * @param email 邮箱 + * @return 注册结果 + */ + CompletableFuture register(String username, String password, String email); + + /** + * 申请重置密码接口 + * @param email 用户邮箱 + * @return { resetToken: String, expiresIn: Number } 重置结果(包含有效期内的重置token和有效期,24小时内有效) + */ + CompletableFuture postResetPasswordRequest(String email); + + /** + * 重置密码接口 + * @param email 用户邮箱 + * @param resetToken 重置token(有效期24小时内) + * @param newPassword 新密码(加密) + * @return 重置结果 + */ + CompletableFuture resetPassword(String email, String resetToken, String newPassword); + + /** + * 修改密码接口 + * @param username 用户名 + * @param oldPassword 旧密码(加密) + * @param newPassword 新密码(加密) + * @return 修改结果 + */ + CompletableFuture changePassword(String username, String oldPassword, String newPassword); + + +} diff --git a/src/main/java/com/axis/innovators/box/verification/api/UserApiImpl.java b/src/main/java/com/axis/innovators/box/verification/api/UserApiImpl.java new file mode 100644 index 0000000..fdda647 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/api/UserApiImpl.java @@ -0,0 +1,65 @@ +package com.axis.innovators.box.verification.api; + +import java.util.concurrent.CompletableFuture; + +public class UserApiImpl implements UserApi { + @Override + public CompletableFuture sendVerificationCode(String email) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Code sent", null)); + } + + @Override + public CompletableFuture verifyCode(String email, String verificationCode) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Code verified", null)); + } + + @Override + public CompletableFuture checkTokenValidity(String token) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Token is valid", null)); + } + + @Override + public CompletableFuture applyNewToken(String oldToken) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "New token applied", null)); + } + + @Override + public CompletableFuture login(String username, String password) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Login successful", null)); + } + + @Override + public CompletableFuture logout(String username) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Logout successful", null)); + } + + @Override + public CompletableFuture register(String username, String password, String email) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Registration successful", null)); + } + + @Override + public CompletableFuture postResetPasswordRequest(String email) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Reset password request sent", null)); + } + + @Override + public CompletableFuture resetPassword(String email, String resetToken, String newPassword) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Password reset successful", null)); + } + + @Override + public CompletableFuture changePassword(String username, String oldPassword, String newPassword) { + // Implementation here + return CompletableFuture.completedFuture(new UserApiResult(true, "Password changed successfully", null)); + } +} diff --git a/src/main/java/com/axis/innovators/box/verification/api/UserApiResult.java b/src/main/java/com/axis/innovators/box/verification/api/UserApiResult.java new file mode 100644 index 0000000..3d075ee --- /dev/null +++ b/src/main/java/com/axis/innovators/box/verification/api/UserApiResult.java @@ -0,0 +1,25 @@ +package com.axis.innovators.box.verification.api; + +public class UserApiResult { + private final boolean m_success; + private final String m_message; + private final Object m_data; + + public UserApiResult(boolean success, String message, Object data) { + this.m_success = success; + this.m_message = message; + this.m_data = data; + } + + public boolean success() { + return m_success; + } + + public String message() { + return m_message; + } + + public Object data() { + return m_data; + } +} diff --git a/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java b/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java new file mode 100644 index 0000000..70de0ca --- /dev/null +++ b/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java @@ -0,0 +1,268 @@ +package com.axis.innovators.box.window; + +import com.axis.innovators.box.browser.CefAppManager; +import com.axis.innovators.box.tools.LoadResource; +import com.axis.innovators.box.verification.CasdoorServer; +import com.axis.innovators.box.verification.LoginData; +import com.axis.innovators.box.verification.LoginResult; +import com.axis.innovators.box.verification.Result; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.awt.Desktop; +import java.awt.FlowLayout; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.casbin.casdoor.config.Config; +import org.casbin.casdoor.entity.User; +import org.casbin.casdoor.service.AuthService; + +import javax.swing.*; +import config.CasdoorConfig; + +import org.cef.CefApp; +import org.cef.CefClient; +import org.cef.browser.CefBrowser; + + +public class CasdoorLoginWindow { + private final Logger logger = LogManager.getLogger(CasdoorLoginWindow.class); + private final CasdoorServer casdoorServer; + + private CefBrowser browser; // 便于复用 + private HttpServer server; // 后端处理登陆成功后的跳转 + + private JDialog dialog; + + private LoginResult loginResult = null; + + private boolean windowVisible = true; + private boolean isModal = true; + + public CasdoorLoginWindow() { + casdoorServer = new CasdoorServer(); + } + + // 启动本地 HTTP 服务监听 Casdoor 回调 + private void startLocalCallbackServer() { + try { + server = HttpServer.create(new java.net.InetSocketAddress(CasdoorConfig.CASDOOR_WEB_SERVER_PORT), 0); + server.createContext("/casdoor/callback", this::handleCallback); + server.setExecutor(java.util.concurrent.Executors.newSingleThreadExecutor()); + server.start(); + } catch (IOException e) { + System.err.println("本地回调服务启动失败: " + e.getMessage()); + } + } + + // 处理回调请求,自动填充 code 和 state + private void handleCallback(HttpExchange exchange) throws IOException { + String query = exchange.getRequestURI().getQuery(); + AtomicReference code = new AtomicReference<>(""); + AtomicReference state = new AtomicReference<>(""); + if (query != null) { + for (String param : query.split("&")) { + String[] kv = param.split("="); + if (kv.length == 2) { + if (kv[0].equals("code")) + code.set(kv[1]); + if (kv[0].equals("state")) + state.set(kv[1]); + logger.info("Received callback with code: " + code.get() + ", state: " + state.get()); + loginResult = parseUserInfo(code.get(), state.get()); + String response = "Login success, please close this window."; + exchange.sendResponseHeaders(200, response.getBytes().length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } + + } + } + } + } + + private void initUI() { + dialog = new JDialog(); + dialog.setTitle("AXIS 认证"); + dialog.setSize(500, 750); + dialog.setLocationRelativeTo(null); // 居中显示 + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + + + JButton backButton = new JButton(""); + backButton.setIcon(LoadIcon.loadIcon("back.png", 24)); + backButton.addActionListener(e -> browser.goBack()); + backButton.setFocusPainted(false); + backButton.setBorderPainted(false); + JButton forwardButton = new JButton(""); + forwardButton.setIcon(LoadIcon.loadIcon("forward.png", 24)); + forwardButton.addActionListener(e -> browser.goForward()); + forwardButton.setFocusPainted(false); + forwardButton.setBorderPainted(false); + JButton refreshButton = new JButton(""); + refreshButton.setIcon(LoadIcon.loadIcon("refresh.png", 24)); + refreshButton.addActionListener(e -> browser.reload()); + refreshButton.setFocusPainted(false); + refreshButton.setBorderPainted(false); + // 将按钮面板添加到主panel顶部 + buttonPanel.add(backButton); + buttonPanel.add(forwardButton); + buttonPanel.add(refreshButton); + panel.add(buttonPanel, 0); + + if (browser == null) { + // 初始化浏览器 + CefApp cefApp = CefAppManager.getInstance(); + CefClient client = cefApp.createClient(); + browser = client.createBrowser( + casdoorServer.getSigninUrl(), + false, + false); + } + + panel.add(browser.getUIComponent()); + dialog.add(panel); + dialog.addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosed(java.awt.event.WindowEvent e) { + if (server != null) { + server.stop(0); + server = null; + } + } + }); + } + + private void openLoginAndListen() { + if (server == null) { + startLocalCallbackServer(); + } + browser.loadURL(casdoorServer.getSigninUrl()); + } + + /** + * 在默认浏览器打开 Casdoor 登录页面 + */ + private void openCasdoorLoginPageInDefaultBrowser() { + String loginUrl = casdoorServer.getSigninUrl(); + try { + Desktop.getDesktop().browse(new URI(loginUrl)); + } catch (Exception ex) { + JOptionPane.showMessageDialog(dialog, "无法打开浏览器: " + ex.getMessage()); + } + } + + private LoginResult parseUserInfo(String code, String state) { + if (code.isEmpty() || state.isEmpty()) { + return new LoginResult(false, "Login failed with error: Invalid code or state.", null); + } + try { + String token = casdoorServer.getOAuthToken(code, state); + User user = casdoorServer.parseJwtToken(token); + return new LoginResult(true, "Login successful.", new LoginData(token, user)); + } catch (Exception ex) { + logger.error("解析登录信息失败: " + ex.getMessage()); + return new LoginResult(false, "Login failed with error: " + ex.getMessage(), null); + } + } + + private void resetResult() { + loginResult = null; + } + + /** + * 以阻塞形式显示(除非setVisible为false)窗口,同时在退出时获取返回值 + * @return 返回登录结果 + */ + public LoginResult exec() { + resetResult(); + initUI(); // 每次显示窗口都先调用一次 + openLoginAndListen(); + dialog.setModal(true); + isModal = true; + dialog.setVisible(windowVisible); + return getLoginResult(); + } + + /** + * 非模态形式显示窗口(与setVisible无关) + */ + public CompletableFuture show() { + resetResult(); + initUI(); // 每次显示窗口都先调用一次 + CompletableFuture future = new CompletableFuture<>(); + dialog.addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosed(java.awt.event.WindowEvent e) { + future.complete(getLoginResult()); + } + }); + openLoginAndListen(); + dialog.setModal(false); + isModal = false; + dialog.setVisible(true); + + return future; + } + + /** + * 窗口是否模态窗口 + * @return true or false + */ + public boolean isModal() { + return isModal; + } + + /** + * 设置窗口是否可见 + * @param b 是否可见 + */ + public void setVisible(boolean b) { + this.windowVisible = b; + dialog.setVisible(b); + + } + + /** + * 获取登录结果 + * @return 登录结果 + */ + public LoginResult getLoginResult() { + return loginResult; + } + + public static LoginResult showLoginDialogAndGetLoginResult() throws InterruptedException, InvocationTargetException { + AtomicReference result = new AtomicReference<>(); + SwingUtilities.invokeAndWait(() -> { + CasdoorLoginWindow window = new CasdoorLoginWindow(); + result.set(window.exec()); + }); + return result.get(); + } + + + public static void main(String[] args) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + Result result = showLoginDialogAndGetLoginResult(); + System.out.println("Login result: " + result); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/axis/innovators/box/window/LoginWindow.java b/src/main/java/com/axis/innovators/box/window/LoginWindow.java deleted file mode 100644 index e0e6664..0000000 --- a/src/main/java/com/axis/innovators/box/window/LoginWindow.java +++ /dev/null @@ -1,539 +0,0 @@ -package com.axis.innovators.box.window; - -import com.axis.innovators.box.verification.OnlineVerification; -import com.axis.innovators.box.verification.UserTags; -import com.axis.innovators.box.verification.VerificationService; -import com.formdev.flatlaf.FlatDarculaLaf; - -import javax.swing.*; -import javax.swing.border.LineBorder; -import javax.swing.text.JTextComponent; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.lang.reflect.InvocationTargetException; -import java.util.concurrent.atomic.AtomicReference; - -/** - * 重构后的现代化单窗口登录/注册/找回密码界面(带平滑切换动画) - * 保留原有验证逻辑接口调用(OnlineVerification / VerificationService) - * - * 说明: - * - 单窗口(JDialog)内使用滑动动画切换视图(仿微软登录体验)。 - * - 所有子界面(登录/注册/找回密码)都在同一容器中切换,不再弹新窗口。 - * - 按钮与输入框固定宽度,避免被挤压变形。 - * - * 注意:需要 flatlaf 依赖以呈现更现代的外观。 - */ -public class LoginWindow { - private static final AtomicReference loginResult = new AtomicReference<>(UserTags.None); - - private final JDialog dialog; - private final JLayeredPane layeredPane; - private final int DIALOG_WIDTH = 460; - private final int DIALOG_HEIGHT = 560; - - // 登录面板中的控件需要在类域以便访问 - private JTextField loginEmailField; - private JPasswordField loginPasswordField; - - public static UserTags createAndShow() throws InterruptedException, InvocationTargetException { - AtomicReference result = new AtomicReference<>(UserTags.None); - SwingUtilities.invokeAndWait(() -> { - LoginWindow window = new LoginWindow(); - window.dialog.setVisible(true); - result.set(loginResult.get()); - if (result.get() == UserTags.None) { - System.exit(0); - } - }); - return result.get(); - } - - public LoginWindow() { - setupLookAndFeel(); - dialog = new JDialog((Frame) null, "AXIS 安全认证", true); - dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - dialog.setSize(DIALOG_WIDTH, DIALOG_HEIGHT); - dialog.setResizable(false); - dialog.setLocationRelativeTo(null); - - // 根容器:深色背景并居中卡片 - JPanel root = new JPanel(new GridBagLayout()); - root.setBackground(new Color(0x202225)); - root.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); - dialog.setContentPane(root); - - // 卡片容器(居中) - JPanel cardWrapper = new JPanel(null) { - @Override - public Dimension getPreferredSize() { - return new Dimension(DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40); - } - }; - cardWrapper.setOpaque(false); - cardWrapper.setPreferredSize(new Dimension(DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - root.add(cardWrapper, gbc); - - // 分层面板用于动画 - layeredPane = new JLayeredPane(); - layeredPane.setBounds(0, 0, DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40); - cardWrapper.add(layeredPane); - - // 卡片背景(圆角) - JPanel backgroundCard = new JPanel(); - backgroundCard.setBackground(new Color(0x2A2E31)); - backgroundCard.setBorder(new RoundBorder(16, new Color(0x3A3F42))); - backgroundCard.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight()); - backgroundCard.setLayout(null); - layeredPane.add(backgroundCard, Integer.valueOf(0)); - - // 创建三个面板(宽度等于容器宽度),初始将登录面板放置在0位置 - JPanel loginPanel = buildLoginPanel(); - JPanel registerPanel = buildRegisterPanel(); - JPanel forgotPanel = buildForgotPanel(); - - int w = layeredPane.getWidth(); - int h = layeredPane.getHeight(); - - loginPanel.setBounds(0, 0, w, h); - registerPanel.setBounds(w, 0, w, h); // 右侧预置 - forgotPanel.setBounds(w * 2, 0, w, h); - - layeredPane.add(loginPanel, Integer.valueOf(1)); - layeredPane.add(registerPanel, Integer.valueOf(1)); - layeredPane.add(forgotPanel, Integer.valueOf(1)); - - dialog.pack(); - // ensure layered sizes match after pack - SwingUtilities.invokeLater(() -> { - layeredPane.setBounds(0, 0, cardWrapper.getWidth(), cardWrapper.getHeight()); - backgroundCard.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight()); - int nw = layeredPane.getWidth(), nh = layeredPane.getHeight(); - loginPanel.setBounds(0, 0, nw, nh); - registerPanel.setBounds(nw, 0, nw, nh); - forgotPanel.setBounds(nw * 2, 0, nw, nh); - }); - } - - // 切换动画:direction = 1 表示向左滑动进入下一页(当前 -> 右侧),-1 表示向右滑动进入上一页 - private void slideTo(int targetIndex) { - Component[] comps = layeredPane.getComponents(); - // 每个面板宽度 - int w = layeredPane.getWidth(); - // 当前最左边的x位置(找到最左的那个主要面板) - // 我们采用简单方式:目标面板应该位于 x = targetIndex * w (0,1,2) - int targetX = -targetIndex * w; // 我们会将所有面板整体平移,使目标显示在 x=0 - - // 获取当前 offset (使用第一个面板的 x 来代表整体偏移) - int startOffset = 0; - // find current offset by checking bounds of first panel (assume index 0 is login) - if (comps.length > 0) { - startOffset = comps[0].getX(); - } - - int start = startOffset; - int end = targetX; - - int duration = 300; // ms - int fps = 60; - int delay = 1000 / fps; - int steps = Math.max(1, duration / delay); - final int[] step = {0}; - - Timer timer = new Timer(delay, null); - timer.addActionListener((ActionEvent e) -> { - step[0]++; - double t = (double) step[0] / steps; - // ease in-out cubic - double tt = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; - int cur = (int) Math.round(start + (end - start) * tt); - - // 平移所有在 layeredPane 中(除背景)组件 - for (Component c : layeredPane.getComponents()) { - if (c instanceof JPanel && c.isVisible()) { - // 计算原始索引根据名字 - String name = c.getName(); - // 我们之前把panel放在 x = idx * w ; 现在把它设置为 idx*w + cur - int idx = 0; - if ("login".equals(name)) idx = 0; - else if ("register".equals(name)) idx = 1; - else if ("forgot".equals(name)) idx = 2; - c.setLocation(idx * w + cur, 0); - } - } - - layeredPane.repaint(); - - if (step[0] >= steps) { - timer.stop(); - } - }); - timer.setInitialDelay(0); - timer.start(); - } - - private JPanel buildLoginPanel() { - JPanel p = new JPanel(null); - p.setOpaque(false); - p.setName("login"); - int w = DIALOG_WIDTH - 40; - int h = DIALOG_HEIGHT - 40; - - // 标题区 - JLabel appTitle = new JLabel("AXIS"); - appTitle.setFont(new Font("微软雅黑", Font.BOLD, 28)); - appTitle.setForeground(Color.WHITE); - appTitle.setBounds(28, 20, w - 56, 36); - - JLabel subtitle = new JLabel("安全认证 — 请登录以继续"); - subtitle.setFont(new Font("微软雅黑", Font.PLAIN, 12)); - subtitle.setForeground(new Color(0xA7AEB5)); - subtitle.setBounds(28, 56, w - 56, 18); - - p.add(appTitle); - p.add(subtitle); - - // 卡片内控件起始 y - int startY = 96; - int fieldW = Math.min(360, w - 56); - int fieldX = (w - fieldW) / 2; - - // Email - JLabel emailLabel = new JLabel("账号"); - emailLabel.setForeground(new Color(0x99A0A7)); - emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - emailLabel.setBounds(fieldX, startY, fieldW, 18); - - loginEmailField = new JTextField(); - styleInput(loginEmailField); - loginEmailField.setBounds(fieldX, startY + 22, fieldW, 44); - loginEmailField.putClientProperty("JTextField.placeholderText", "邮箱或手机号"); - - // Password - JLabel pwdLabel = new JLabel("密码"); - pwdLabel.setForeground(new Color(0x99A0A7)); - pwdLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - pwdLabel.setBounds(fieldX, startY + 22 + 44 + 12, fieldW, 18); - - loginPasswordField = new JPasswordField(); - styleInput(loginPasswordField); - loginPasswordField.setBounds(fieldX, startY + 22 + 44 + 12 + 20, fieldW - 48, 44); - loginPasswordField.putClientProperty("JTextField.placeholderText", "请输入密码"); - - // eye toggle - JToggleButton eyeBtn = new JToggleButton("显示"); - eyeBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12)); - eyeBtn.setFocusable(false); - eyeBtn.setBorderPainted(false); - eyeBtn.setContentAreaFilled(true); - eyeBtn.setBackground(new Color(0x39424A)); - eyeBtn.setForeground(Color.WHITE); - eyeBtn.setBounds(fieldX + fieldW - 44, startY + 22 + 44 + 12 + 20, 44, 44); - eyeBtn.addActionListener(e -> { - if (eyeBtn.isSelected()) loginPasswordField.setEchoChar((char) 0); - else loginPasswordField.setEchoChar('•'); - }); - - // 登录按钮(占满宽度) - JButton loginBtn = new JButton("立即登录"); - stylePrimaryButton(loginBtn); - loginBtn.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 22, fieldW, 48); - loginBtn.addActionListener(e -> doLogin()); - - // 链接区域(注册 / 忘记密码) — 点击切换到对应面板 - JButton toRegister = createTextLink("注册账号"); - toRegister.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 22 + 60, 120, 20); - toRegister.addActionListener(e -> slideTo(1)); - - JButton toForgot = createTextLink("忘记密码"); - toForgot.setBounds(fieldX + fieldW - 120, startY + 22 + 44 + 12 + 20 + 44 + 22 + 60, 120, 20); - toForgot.addActionListener(e -> slideTo(2)); - - // footer - JLabel footer = new JLabel("使用你的 AXIS 账户进行登录。"); - footer.setForeground(new Color(0x8F969C)); - footer.setFont(new Font("微软雅黑", Font.PLAIN, 11)); - footer.setBounds(fieldX, h - 44, fieldW, 18); - - p.add(emailLabel); - p.add(loginEmailField); - p.add(pwdLabel); - p.add(loginPasswordField); - p.add(eyeBtn); - p.add(loginBtn); - p.add(toRegister); - p.add(toForgot); - p.add(footer); - - return p; - } - - private JPanel buildRegisterPanel() { - JPanel p = new JPanel(null); - p.setOpaque(false); - p.setName("register"); - int w = DIALOG_WIDTH - 40; - int h = DIALOG_HEIGHT - 40; - - JLabel title = new JLabel("创建账号"); - title.setFont(new Font("微软雅黑", Font.BOLD, 24)); - title.setForeground(Color.WHITE); - title.setBounds(28, 20, w - 56, 36); - - JLabel desc = new JLabel("快速创建你的 AXIS 帐号"); - desc.setFont(new Font("微软雅黑", Font.PLAIN, 12)); - desc.setForeground(new Color(0xA7AEB5)); - desc.setBounds(28, 56, w - 56, 18); - - p.add(title); - p.add(desc); - - int startY = 96; - int fieldW = Math.min(360, w - 56); - int fieldX = (w - fieldW) / 2; - - // 用户名 - JLabel nameLabel = new JLabel("用户名"); - nameLabel.setForeground(new Color(0x99A0A7)); - nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - nameLabel.setBounds(fieldX, startY, fieldW, 18); - - JTextField nameField = new JTextField(); - styleInput(nameField); - nameField.setBounds(fieldX, startY + 22, fieldW, 44); - - // 邮箱 - JLabel emailLabel = new JLabel("邮箱"); - emailLabel.setForeground(new Color(0x99A0A7)); - emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - emailLabel.setBounds(fieldX, startY + 22 + 44 + 12, fieldW, 18); - - JTextField emailField = new JTextField(); - styleInput(emailField); - emailField.setBounds(fieldX, startY + 22 + 44 + 12 + 20, fieldW, 44); - - // 密码 - JLabel pwdLabel = new JLabel("密码"); - pwdLabel.setForeground(new Color(0x99A0A7)); - pwdLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - pwdLabel.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12, fieldW, 18); - - JPasswordField pwdField = new JPasswordField(); - styleInput(pwdField); - pwdField.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20, fieldW, 44); - - // 确认密码 - JLabel confirmLabel = new JLabel("确认密码"); - confirmLabel.setForeground(new Color(0x99A0A7)); - confirmLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - confirmLabel.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12, fieldW, 18); - - JPasswordField confirmField = new JPasswordField(); - styleInput(confirmField); - confirmField.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12 + 20, fieldW, 44); - - // 注册按钮 - JButton regBtn = new JButton("创建账号"); - stylePrimaryButton(regBtn); - regBtn.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 18, fieldW, 48); - - regBtn.addActionListener(e -> { - String name = nameField.getText().trim(); - String email = emailField.getText().trim(); - String pwd = new String(pwdField.getPassword()); - String confirm = new String(confirmField.getPassword()); - - if (name.isEmpty() || email.isEmpty() || pwd.isEmpty() || confirm.isEmpty()) { - JOptionPane.showMessageDialog(dialog, "请完整填写注册信息", "注册错误", JOptionPane.ERROR_MESSAGE); - return; - } - if (!pwd.equals(confirm)) { - JOptionPane.showMessageDialog(dialog, "两次输入的密码不一致", "注册错误", JOptionPane.ERROR_MESSAGE); - return; - } - boolean success = VerificationService.registerUser(name, email, pwd); - if (success) { - JOptionPane.showMessageDialog(dialog, "注册成功,请登录", "注册成功", JOptionPane.INFORMATION_MESSAGE); - slideTo(0); // 回到登录页面 - } else { - JOptionPane.showMessageDialog(dialog, "注册失败,请检查信息", "注册错误", JOptionPane.ERROR_MESSAGE); - } - }); - - // 返回登录链接 - JButton back = createTextLink("返回登录"); - back.setBounds(fieldX, regBtn.getY() + regBtn.getHeight() + 12, 120, 20); - back.addActionListener(e -> slideTo(0)); - - p.add(nameLabel); - p.add(nameField); - p.add(emailLabel); - p.add(emailField); - p.add(pwdLabel); - p.add(pwdField); - p.add(confirmLabel); - p.add(confirmField); - p.add(regBtn); - p.add(back); - - return p; - } - - private JPanel buildForgotPanel() { - JPanel p = new JPanel(null); - p.setOpaque(false); - p.setName("forgot"); - int w = DIALOG_WIDTH - 40; - int h = DIALOG_HEIGHT - 40; - - JLabel title = new JLabel("找回密码"); - title.setFont(new Font("微软雅黑", Font.BOLD, 24)); - title.setForeground(Color.WHITE); - title.setBounds(28, 20, w - 56, 36); - - JLabel desc = new JLabel("通过注册邮箱重置密码"); - desc.setFont(new Font("微软雅黑", Font.PLAIN, 12)); - desc.setForeground(new Color(0xA7AEB5)); - desc.setBounds(28, 56, w - 56, 18); - - p.add(title); - p.add(desc); - - int startY = 110; - int fieldW = Math.min(360, w - 56); - int fieldX = (w - fieldW) / 2; - - JLabel emailLabel = new JLabel("注册邮箱"); - emailLabel.setForeground(new Color(0x99A0A7)); - emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); - emailLabel.setBounds(fieldX, startY, fieldW, 18); - - JTextField emailField = new JTextField(); - styleInput(emailField); - emailField.setBounds(fieldX, startY + 22, fieldW, 44); - - JButton sendBtn = new JButton("发送重置邮件"); - stylePrimaryButton(sendBtn); - sendBtn.setBounds(fieldX, startY + 22 + 44 + 22, fieldW, 48); - - sendBtn.addActionListener(e -> { - String email = emailField.getText().trim(); - if (email.isEmpty()) { - JOptionPane.showMessageDialog(dialog, "请输入注册邮箱", "错误", JOptionPane.ERROR_MESSAGE); - return; - } - if (VerificationService.sendPasswordReset(email)) { - JOptionPane.showMessageDialog(dialog, "重置邮件已发送,请查收", "成功", JOptionPane.INFORMATION_MESSAGE); - slideTo(0); - } else { - JOptionPane.showMessageDialog(dialog, "发送失败或邮箱未注册", "失败", JOptionPane.ERROR_MESSAGE); - } - }); - - JButton back = createTextLink("返回登录"); - back.setBounds(fieldX, sendBtn.getY() + sendBtn.getHeight() + 12, 120, 20); - back.addActionListener(e -> slideTo(0)); - - p.add(emailLabel); - p.add(emailField); - p.add(sendBtn); - p.add(back); - - return p; - } - - private void doLogin() { - String email = loginEmailField.getText().trim(); - String password = new String(loginPasswordField.getPassword()).trim(); - - if (email.isEmpty() || password.isEmpty()) { - JOptionPane.showMessageDialog(dialog, "请输入完整的登录信息", "验证错误", JOptionPane.ERROR_MESSAGE); - return; - } - - OnlineVerification onlineVerification = OnlineVerification.validateLogin(email, password); - if (onlineVerification == null) { - String err = OnlineVerification.errorMessage != null && !OnlineVerification.errorMessage.trim().isEmpty() - ? OnlineVerification.errorMessage - : "验证失败,请重试"; - JOptionPane.showMessageDialog(dialog, err, "验证错误", JOptionPane.ERROR_MESSAGE); - return; - } - - loginResult.set(VerificationService.determineUserType(onlineVerification)); - dialog.dispose(); - } - - - // 通用输入框样式 - private void styleInput(JComponent comp) { - comp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); - comp.setBackground(new Color(0x2F3336)); - comp.setForeground(new Color(0xE8ECEF)); - comp.setBorder(BorderFactory.createCompoundBorder( - new RoundBorder(8, new Color(0x3A3F42)), - BorderFactory.createEmptyBorder(10, 12, 10, 12) - )); - if (comp instanceof JTextComponent) ((JTextComponent) comp).setCaretColor(new Color(0x9AA0A6)); - } - - // 主要操作按钮样式(主色) - private void stylePrimaryButton(AbstractButton b) { - b.setFont(new Font("微软雅黑", Font.BOLD, 14)); - b.setForeground(Color.WHITE); - b.setBackground(new Color(0x2B79D0)); - b.setBorderPainted(false); - b.setFocusPainted(false); - b.setOpaque(true); - b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - } - - private JButton createTextLink(String text) { - JButton btn = new JButton(text); - btn.setFont(new Font("微软雅黑", Font.PLAIN, 12)); - btn.setForeground(new Color(0x79A6FF)); - btn.setBorderPainted(false); - btn.setContentAreaFilled(false); - btn.setFocusPainted(false); - btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - return btn; - } - - private void setupLookAndFeel() { - try { - UIManager.setLookAndFeel(new FlatDarculaLaf()); - UIManager.put("Component.arc", 12); - UIManager.put("TextComponent.arc", 12); - UIManager.put("Button.arc", 10); - - UIManager.put("Panel.background", new Color(0x202225)); - UIManager.put("TextComponent.background", new Color(0x2F3336)); - UIManager.put("TextComponent.foreground", new Color(0xE8ECEF)); - } catch (UnsupportedLookAndFeelException ex) { - ex.printStackTrace(); - } - } - - // 圆角边框 - private static class RoundBorder extends LineBorder { - private final int radius; - - public RoundBorder(int radius, Color color) { - super(color, 1); - this.radius = radius; - } - - @Override - public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2.setColor(lineColor); - g2.drawRoundRect(x, y, width - 1, height - 1, radius, radius); - g2.dispose(); - } - } -} diff --git a/src/main/java/config/CasdoorConfig.java b/src/main/java/config/CasdoorConfig.java new file mode 100644 index 0000000..feb5e97 --- /dev/null +++ b/src/main/java/config/CasdoorConfig.java @@ -0,0 +1,17 @@ +package config; + +public class CasdoorConfig { + // Casdoor Config Contructor + public static final String CASDOOR_API_URL = "https://casdoor.lingqi.vip"; + public static final String CASDOOR_CLIENT_ID = "efb6af7c9517f19340da"; + public static final String CASDOOR_CLIENT_SECRET = "7396f7e4bcda83756641179108b89356bb4d6d3b"; + public static final String CASDOOR_CERTIFICATE_FILE = ""; + public static final String CASDOOR_ORGANIZATION_NAME = "灵启"; + public static final String CASDOOR_APPLICATION_NAME = "lingqi_box"; + + // Casdoor AuthService getSigninUrl + public static final String CASDOOR_LOGIN_REDIRECT_URI = "http://localhost:10487/casdoor/callback"; + public static final String CASDOOR_SIGNUP_REDIRECT_URI = null; // 如果为 null 则自动跳转到登录页面,如果不需要跳转可以设置为"" + public static final int CASDOOR_WEB_SERVER_PORT = 10487; // HUST + +} diff --git a/src/main/resources/cert/casdoor_cert.pem b/src/main/resources/cert/casdoor_cert.pem new file mode 100644 index 0000000..5e35395 --- /dev/null +++ b/src/main/resources/cert/casdoor_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE3TCCAsWgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMCgxDjAMBgNVBAoTBWFk +bWluMRYwFAYDVQQDEw1jZXJ0LWJ1aWx0LWluMB4XDTI1MDgxNTEzNDgwMVoXDTQ1 +MDgxNTEzNDgwMVowKDEOMAwGA1UEChMFYWRtaW4xFjAUBgNVBAMTDWNlcnQtYnVp +bHQtaW4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm56dxzpBRDyG7 +CNt3v51+ikP3dgAtgky5yUSeiQJW8eHaSJayNfIT42GCH3Nk6ZI3xR3nAZ1kaFyg +/ZiVw9QHzju3859osXoK+NMiv75G3Z0xEe2wg2gRJh0FvTDw6TKSv179C5BrxK5y +6O8caP5NAA5Kqn2OiQkIXRBxYU7i0fHrABeJpO396KtZ9oPoe9/ZWGRDTsilbyRB +8gz7JaVq9ECsgPDErmtvUhXLtNPKSfXN2UVVSpE+EET0DJv7dVlSowoaF4tNtA5e +QVTyAR559aPBnXZKD/9arTRCtU1YB2Uv3NuFELeJzchAmf/l+IPbBUfvGdaLE/kg +TB0T9hfiyRpIvjfdEpQTeaOSDVbnO165Z1AWhpxVHEHfaEDD/48aSSfAKty29CaV +wmmBA1aw90AtkyJR3BfrHExAMK4+s89P07ihe+YVUCgrrZk8J7xAQ+g3P4+FF4MI +NrhS/OgHss2e8pxoiv6rvlMXyZqFlocKuaNKCvTy0vvspj1wn6XdX8jZZnpZrKLy +Qbc1LN/HNmMe4RGjIBdlBdBHI0RIH7afLhghor+fZ/5lAa1Fzwv6TbD+QIx1wEq9 +zY6lRamN4iS/AxkIEovcKJq+W0Vcn+0bovcjjmojN2MnkHWH+XCBeyEixh+fuzSi +Zjt3ydhPzU2Vb1vPsVqFQjHHR+FYbwIDAQABoxAwDjAMBgNVHRMBAf8EAjAAMA0G +CSqGSIb3DQEBCwUAA4ICAQBTFfNo5BMqwwlSimxfT2uj1xfXZ/ek0suaY9xZIVVo +XlrbIuE1j4KS1MjOCs1IbkYZW5+MO4c4OcjHh/XusQ2t2PKP7Q0atdFjYeiw987a +oE9r5am2IPzfh3DI3jC7rfz6Jrq64oK23JAXisa21xoB4CflzlpgOFX3h6UfcNhB +bTSQjdyeTZYwCeSmO53iAW9oNlffnzaYtQXqiq7pCGcC7sGZBZJnM0nNvQRYqzcU +8E8elfEwdWqc9ZJ/ah8byobZQHvraRcYDEZ3oqfjMsoycbFfJztaZV1pfJOZNRzG +lypDUCjZeK1z91K6UFws6qOGc6j06pLIXxVmpuw62yCnG3KM55O3GcUc9q3oAyTb +Iq2g3CjIiaFu8AowVyFswvOIK2pvRBBA+54eP079SLqqznHV+AE91nvVLOMn7xVu +vfV2XbxzAk3to39WilEn3KpGavpQnyQnD/nVjpEzzInAjPO6vCkWXs1ZS2sqNciz +T+r9Q3cZUMWusgNNoGfWnjchLOaOPe1zGINwAsSDXjv1YD18pfp5hEPlwSTGrulI +7fN+cGQ5Spv5VhFcOyodgmIvMnM2EcgeH4CPkQ5aiE3qAGzRzfQiPeght/Wkdd4J ++SMykEE2VmLmVD8VQf2j/EVbGQ32TTllYfQEgS/S5pfUPr5RAEljCHlCUiNcHF9c ++g== +-----END CERTIFICATE----- diff --git a/state/toolbox.properties b/state/toolbox.properties new file mode 100644 index 0000000..e69de29 From 290425898359e196adc0edc53980de01ab9db4d4 Mon Sep 17 00:00:00 2001 From: Hydrogen <1620682458@qq.com> Date: Mon, 18 Aug 2025 01:33:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(CasdoorLoginWindow,resources):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=86=85=E5=B5=8C=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E5=88=9B=E5=BB=BA=E5=A4=B1=E8=B4=A5=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98=EF=BC=8C=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=80=BB=E8=BE=91=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=97=A0=E5=86=85?= =?UTF-8?q?=E5=B5=8C=E6=B5=8F=E8=A7=88=E5=99=A8=E6=97=B6=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=B5=8F=E8=A7=88=E5=99=A8=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E8=A1=A5=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E4=B8=8A=E6=AC=A1=E7=BC=BA=E5=A4=B1=E7=9A=84=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E4=B8=BA=E5=86=85=E5=B5=8C=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E6=B7=BB=E5=8A=A0=E8=BF=94=E5=9B=9E=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E7=95=8C=E9=9D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../innovators/box/AxisInnovatorsBox.java | 28 +++-- .../box/window/CasdoorLoginWindow.java | 97 ++++++++++++------ .../resources/icons/material-symbols-back.png | Bin 0 -> 12279 bytes .../icons/material-symbols-forward.png | Bin 0 -> 11901 bytes .../resources/icons/material-symbols-home.png | Bin 0 -> 13106 bytes .../icons/material-symbols-refresh.png | Bin 0 -> 17192 bytes 6 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 src/main/resources/icons/material-symbols-back.png create mode 100644 src/main/resources/icons/material-symbols-forward.png create mode 100644 src/main/resources/icons/material-symbols-home.png create mode 100644 src/main/resources/icons/material-symbols-refresh.png diff --git a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java index d204f06..a628f50 100644 --- a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java +++ b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java @@ -44,6 +44,7 @@ import java.awt.event.WindowEvent; import java.io.*; import java.lang.instrument.Instrumentation; import java.lang.management.*; +import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.List; @@ -83,7 +84,7 @@ public class AxisInnovatorsBox { private final boolean isDebug; private static DebugWindow debugWindow; - private LoginData loginData; + private static LoginData loginData; public AxisInnovatorsBox(String[] args, boolean isDebug) { this.args = args; @@ -92,9 +93,12 @@ public class AxisInnovatorsBox { organizingCrashReports(throwable instanceof Exception ? (Exception) throwable : new Exception(throwable)); }); - + // 初始化,这里为了能够在登录窗口使用主题,特意将初始化放在构造函数中 + main.initLog4j2(); + main.setTopic(); // 加载登录信息,如果没有,弹出登录弹窗,后续可以删掉默认弹出 - // TODO: login window + // TODO: login window should not be show when AxisInnovatorsBox initialize, + // it should be show when user click login button. try { StateManager stateManager = new StateManager(); String token = stateManager.getState("loginToken"); @@ -107,22 +111,29 @@ public class AxisInnovatorsBox { } else if (loginResult.success()) { loginData = loginResult.loginData(); stateManager.saveState("loginToken", loginResult.token()); - logger.info("Login result: token: " + loginResult.token() + ", user: " + loginResult.user()); + logger.info( + "Login result: token: " + loginResult.token() + ", user: " + loginResult.user()); JOptionPane.showMessageDialog(null, "登录成功", "登录", JOptionPane.INFORMATION_MESSAGE); } else { // 登录失败,弹出错误提醒,这里只是输出登录错误信息 logger.error("Login error: " + loginResult.message()); - JOptionPane.showMessageDialog(null, "登录失败: \n" + loginResult.message(), "登录失败", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(null, "登录失败: \n" + loginResult.message(), "登录失败", + JOptionPane.ERROR_MESSAGE); } } else { CasdoorServer casdoorServer = new CasdoorServer(); User user = casdoorServer.parseJwtToken(token); loginData = new LoginData(token, user); } + } catch (InterruptedException e) { + logger.error("InterruptedException: Failed to load login information", e); + } catch (InvocationTargetException e) { + logger.error("InvocationTargetException: Failed to load login information", e); } catch (Exception e) { - logger.error("Failed to load login information", e); - } + logger.error("Exception: Failed to load login information", e); + } // 添加了更详细的异常处理 + } /** @@ -978,8 +989,6 @@ public class AxisInnovatorsBox { main = new AxisInnovatorsBox(args,isDebug); try { - main.initLog4j2(); - main.setTopic(); List> validFiles = ArgsParser.parseArgs(args); for (Map fileInfo : validFiles) { @@ -993,6 +1002,7 @@ public class AxisInnovatorsBox { } main.thread = new Thread(() -> { + try { // 主任务1:加载插件 logger.info("Loaded plugins Started"); diff --git a/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java b/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java index 70de0ca..3fbaab2 100644 --- a/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java +++ b/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java @@ -91,6 +91,37 @@ public class CasdoorLoginWindow { } private void initUI() { + if (browser == null) { + try { + // 初始化浏览器 + CefApp cefApp = CefAppManager.getInstance(); + CefClient client = cefApp.createClient(); + browser = client.createBrowser( + casdoorServer.getSigninUrl(), + false, + false); + } catch (Throwable e) { + logger.error("Failed to initialize CefBrowser", e); + // 浏览器初始化失败,弹出默认浏览器替代,并提示用户 + // 如果浏览器初始化失败,改为在默认浏览器打开登录页面 + openCasdoorLoginPageInDefaultBrowser(); + // JOptionPane.showMessageDialog(dialog, + // "浏览器初始化失败,已在默认浏览器打开登录页面,请手动完成登录。", + // "内嵌浏览器初始化失败", + // JOptionPane.WARNING_MESSAGE + // ); + String message = "浏览器初始化失败,已在默认浏览器打开登录页面,请手动完成登录。\n或者手动复制下面链接在浏览器打开进行登录:\n" + + casdoorServer.getSigninUrl(); + JTextArea textArea = new JTextArea(message); + textArea.setEditable(false); + textArea.setLineWrap(true); + textArea.setWrapStyleWord(true); + textArea.setBackground(null); + textArea.setSize(600, 400); + JOptionPane.showMessageDialog(dialog, new JScrollPane(textArea), "内嵌浏览器初始化失败", JOptionPane.WARNING_MESSAGE); + } + } + dialog = new JDialog(); dialog.setTitle("AXIS 认证"); dialog.setSize(500, 750); @@ -100,42 +131,40 @@ public class CasdoorLoginWindow { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); - JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + if (browser != null) { + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - JButton backButton = new JButton(""); - backButton.setIcon(LoadIcon.loadIcon("back.png", 24)); - backButton.addActionListener(e -> browser.goBack()); - backButton.setFocusPainted(false); - backButton.setBorderPainted(false); - JButton forwardButton = new JButton(""); - forwardButton.setIcon(LoadIcon.loadIcon("forward.png", 24)); - forwardButton.addActionListener(e -> browser.goForward()); - forwardButton.setFocusPainted(false); - forwardButton.setBorderPainted(false); - JButton refreshButton = new JButton(""); - refreshButton.setIcon(LoadIcon.loadIcon("refresh.png", 24)); - refreshButton.addActionListener(e -> browser.reload()); - refreshButton.setFocusPainted(false); - refreshButton.setBorderPainted(false); - // 将按钮面板添加到主panel顶部 - buttonPanel.add(backButton); - buttonPanel.add(forwardButton); - buttonPanel.add(refreshButton); - panel.add(buttonPanel, 0); + JButton homeButton = new JButton(""); + homeButton.setIcon(LoadIcon.loadIcon("material-symbols-home.png", 24)); + homeButton.addActionListener(e -> browser.loadURL(casdoorServer.getSigninUrl())); + homeButton.setFocusPainted(false); + homeButton.setBorderPainted(false); + JButton backButton = new JButton(""); + backButton.setIcon(LoadIcon.loadIcon("material-symbols-back.png", 24)); + backButton.addActionListener(e -> browser.goBack()); + backButton.setFocusPainted(false); + backButton.setBorderPainted(false); + JButton forwardButton = new JButton(""); + forwardButton.setIcon(LoadIcon.loadIcon("material-symbols-forward.png", 24)); + forwardButton.addActionListener(e -> browser.goForward()); + forwardButton.setFocusPainted(false); + forwardButton.setBorderPainted(false); + JButton refreshButton = new JButton(""); + refreshButton.setIcon(LoadIcon.loadIcon("material-symbols-refresh.png", 24)); + refreshButton.addActionListener(e -> browser.reload()); + refreshButton.setFocusPainted(false); + refreshButton.setBorderPainted(false); + // 将按钮面板添加到主panel顶部 + buttonPanel.add(homeButton); + buttonPanel.add(backButton); + buttonPanel.add(forwardButton); + buttonPanel.add(refreshButton); + panel.add(buttonPanel, 0); - if (browser == null) { - // 初始化浏览器 - CefApp cefApp = CefAppManager.getInstance(); - CefClient client = cefApp.createClient(); - browser = client.createBrowser( - casdoorServer.getSigninUrl(), - false, - false); + panel.add(browser.getUIComponent()); } - - panel.add(browser.getUIComponent()); dialog.add(panel); dialog.addWindowListener(new java.awt.event.WindowAdapter() { @Override @@ -152,7 +181,9 @@ public class CasdoorLoginWindow { if (server == null) { startLocalCallbackServer(); } - browser.loadURL(casdoorServer.getSigninUrl()); + if (browser != null) { + browser.loadURL(casdoorServer.getSigninUrl()); + } } /** diff --git a/src/main/resources/icons/material-symbols-back.png b/src/main/resources/icons/material-symbols-back.png new file mode 100644 index 0000000000000000000000000000000000000000..95035419192989dc6f0a19bf001cebcad8b004a5 GIT binary patch literal 12279 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/material-symbols-forward.png b/src/main/resources/icons/material-symbols-forward.png new file mode 100644 index 0000000000000000000000000000000000000000..e30aa63f361ca093807540bf9af1407621b334a9 GIT binary patch literal 11901 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 a3VFheQVKt#tPQ^)M-Lkx$~oYC_5T2C=t+A3 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/material-symbols-home.png b/src/main/resources/icons/material-symbols-home.png new file mode 100644 index 0000000000000000000000000000000000000000..b075c92d8a62ef33c6ff8ad23495223bb4396dcd GIT binary patch literal 13106 zcmXwAcOcaN8$Z`6(NL5^Aw}hc?5u04es*N95|_@(+3Q?=n@FW>M=5(n=(4Vf%#uCJ zDzh>U$N4>T=crQW9-{|$CEC)h}*ol9ZGIaQ22EobLzR! zam@1!uCtgEPi${CU*~-kdiJ0Z`U?8t&U;TWa{rw@%S-BC4Ww5vrYc7JjD4&K!AA)d z!BeV%`4u6_{e6s;#-%Fy-!qNNkthx{hKqNXK8{1y`ZK3)DDrb-BR?t+jr?9zHYF5o zPkYW2hy4E2&a<=Jp$C5=1f0Dr2Pe&bqree_LHH35$q-K3o()yoMRB1h@>2{y>f{Tn z0v3xmH2-DpP~t`_Hpd6RG37GP6NIC-z-y5z^g!iJGd9m%z*9DJ=fO3ea=`QS=KPHy zE|eyU<_Qg&nA^0Sk9o25b^bLG9YtaD6`_{0rxi>U`8|><>I!U?>O- z-OuxIO`sqM%)0A~*bs`N1JfyEEN+eI8(`X=>Bshp*O3&|PxZI&#Mw;qBJDUlD#CnO z+Thv&nAaku!Uo!#frU0l`y}C04)h+LgQ#QrdCe?0d69enNTGbX7ye^u>LmqCm5R}8 zOx^AQ)2RDRY;DqksXm_+c1@cR&_Gyi6`ha;1-bRaVI*Mt*RtqJJ3 z;BEiT1U66%fcqk@vtsBA=N6FYkrBJX25vxXd~^`A2KdfrD?hXcJC z)=^NOlF5gJ-%oTkM3qWk<3m=crdV)r;)*imudS)`7}QDssT$2z=P;^;~?k9EU{3gjwFjw*vWnSAEsv8Ehm;uy(XqkB1#-ZFD++J=d(b_x%_aN8Nfl_M=X zyto5pd{r=v7J!b-FebUox#ut^#4|&)ZS?3~ z4s<)WE+Y+XZ)uRa&HTi`lkxqLdCXU1k-5yJ))+Tcd}EU8$NPk)+Qp4thh)7CxkEEgN1Uuo}<%>5j5rq`0)sN;UVo4#Cc&E^j)n3`lWNaxt}ifVN3;Lk>yr3i8wdlM=s4gInzE-m>XmB>cdwnU7P*d zybch!pBk%vmLB-MQ8CH3N*d28`trahZewwrJ&Ei;Z-S~HylR5J&6T)1vT*md@VB3p zcja#4F1aaSS|plpLkvc$-+N|5-WF4RCY;(U^{w7aAeI0ksh1;k27Jia+mTDIDpIJs z8tY4SAkC0#A*Q3E1Rv0H(W6z1j;hOTNoxIK>knZref7_HC+-nW3#vYd8YxmdN*>2x zNp=@5dbWO0!#D{TqU!Z0Fqo;0m#N*Qy@A48%WJrCE|+F$8$v{73u%cjrBEZ^6qU$o zz?bSsWi+aV^d8_%yf-U>RRg3Ll^&! zG`=pNZBI8wVV_P2S8!ie?JE7^U6`;Vv_o>kcsW?d8R1fxg6m8eed$?Jz&P02g zxwt+jO>nG{`ii#rUZbu;C%JpEYkQ62BjBxTC9iCS-oSkjwFOfYNJK~b`le5GkEPCb zT@k=o%L~YsTc5B`A|6DwUGr^o#1tluclXS-Ev7M|=Zpeu+jSl6cDVwPSGFl6;SF)RmSw7WJ%-&HX7gtLph{t68TRqb0P0Z~ykw43`dN%0)+N zTWRrllt==WzM4O-74ow;C_1=s`lS29Fa^=Z^(FPpkB+SH$*?e&63H7|(Py{wF|$={VwniWa0S?P#& zkk|fn%qQG7vW^>X+8btL$m`JI_FkucOA*ITrW!%byDoOO+GuRR zL|ZjcuY~gU_-H#F{Uvz@eOlS2{co1?FC=F>P9%zt#*Yi<-iM8Boj}#L zig;Ttu+yC31rC=VSY~h~YFQWcylG;JPtNGP`HW9 z>ogs4q1=EFS+5s#@}iY(Z&HqXW@I5Yd3Z5^dZkX4I8}e~3!k#b(RP=0=PXJ0#+z^) z1tDAN?Mrdd#=!}1`bNLZ`Glk{&UNLrdQR1g5iJN?tTfz_xET@b8xw9IctEQ?AVAZ8 zuSN26QuR(Xwsy6&@F87JT8YYbf8lr}&UmEciK?sPUg|Al2%NspiS-Wa&{nb6(e^wR zvbV#~i`t95bJ``vUyg7sR|h0H(xsaM8iqHDmc?*u3|Kalf>`4Fqt(&*w6P6#YYs=Zm}45wq+4pPfsOZsnwz9+ewWFBBf@Zl0sAE(8Fe%Bb7aYJ{?D zyH2)Tz2bBZYN)G8L$+NtTxUQVTos|NnRpKJM9gq=oc@=If+}-)QW10`6*1A~+l?=c zA4tpmQKJE=gZ=@M621l99hZol*SXcDvOj7y0Xp zPLq__rnVYoDEV%*KVOE>7E|@^sLDy}7Y#=jyIZ@fEGpWgJGMJqnpFJcF!)*HVW2p@ z=B0y59tT-k&xoVoh0Cm3%*<`8Xf;N}-`6!EPqzfNj9EzJH}Fcolo{9L#|%9^|)Ehjm@D|{!z_0Jo;8{gIW8CN}P zj)L5vS)~Lleo2WKY^s&l7s428%GA87Ga+~9W5)WjAC26&sidBhW%GYD$x(3Lll-IV zcN?;uR9&2!&+hrj6o90!aHB0(J zZjV)6uqm?@Rg$~cIu{tuaD&ixAKoiu_Viaxsj@#OZQk?ikMKqjeTzJoYfNIJYF$@k z249e+x2qP}{3F(fu}7}amRb!tks>5{^ z##^J&X)<~E}-{xVM8eFg!;N@yyN0EU!OwjB**dT-ik1T`}}vDT^xdjczgGb zP$ra$XV#~oV%tkgzZCWS?EW-ovGNmQx^=azq)Q$BGU8g!sOrhGR3}bRy3^DbpVXgT zBUYPrI0{*c)>xS$My|c)fiYz5(K=x95F#(u=rUI4_w$Ar(nNF1oCGttiUc z`O<1pBjZzR(Iy)q*LCRH)1HrdTq z$5OFsBs#jz9_I&Ej(>_BosaR=jT{ZBA zt^&S%Z0$JreQzwE4Bdh1ZfrVVq()M?O!delmUvR|ZKsOLm*a|C zyLiv$Jt`W?i`XrrT-P%QrL0ND*A(CN9D123XA7_M(Hne;B-h}_j>A})cAZ=?@(`A) zvqdO=exI>-lgzsypOU#2a1!OI|7grGu77==GP5RtF@1sL=AElY9y+3`d!#$uzS063 zcX|kgVV0Ns3?*&hZgK0<_Otp?-9xYQYxE;O$$$5H zFL4G=mvJSjl1amwt<1Tg)#rU7F=G}=c@b?-ah?gnlI?qL`JO)uiMmLv=}y#o&)<8< zJ3}s@UOjlxGU^vX#8andzbpxU#~)5q&SBAXWF)VrR(LDVw0LnK(~M%}g%7Fk|JJL-Pc74wO1MLn{(4Pd zixSi0t8Aym$0o5GR`p$r4{Q$2+krP*&*cPZ(b`aH7+o~t8QKedB?b{L}9x@-!y)>7Nk1JIY zBwKNM<_^Wz78A!CZ%jVG>w=4j>v<}wJa(WaN5B2;nGz1U03p;nou8{cz)QB4r}}tq z-s9QB#RwSbc`zKUc^>uZac(xT!87sda+XftV)a~I*@e~KF+s8e25AsK7kkzJ(GZE+ z5vm^echi_^W1fRC>Q!9dOoB|^HCJuf{+uG0gyHU{_-TIay}F|9=kqeHt!~^5A-wiq z|AMzh{#RQPqyK3x*AA5HuvAg;^3I#%470ZGd{d3O(&8^%x)Kivhucl_Ue>Okye!)E zT$?Wwbw%o$g`2UfU2@UZ{L=o4rI~C3W`wt<_z3~q-X%O(s_q2qlRYgk1;K9I0M z3e{E9vDolrxJ1dRya$_Xn;uXxwp#K=+kPY2nm`@#oRq#+8Qu60Zvx&avW%xcAYMSZ zXiY}9`-~o}d$}0ZrBx6pkjxuIc>DEYTC}_VkFw`l=sCt+bXd?p7>c_x~W8cur0A#chrq&&Pnt6J0GI> zWIBWSsI{1ZUahcYCXFQ<+lV(`$K|zJ7g2qzVyDj&>x-nDUaOBsLx*fE%4w`AD4a0%2t=9I)g0ki4w*)mp z^ywn?C2B%K+{SIOXGGfr@)oTbGl@@gjS$GoTnk@WD^A+qb3yILhP%z$8yhPpCMK|} z0@)UOb7KU_7vcOeiu8!O&B~Z~{0~Oo??r8`tAUnqT%Do+@IJQc<|k&tcrZ47VTBI6 zKpPyE94JbEcju4Y{l(Pr2(z3#>HSdnxR$1zHOosDZ{Uh|m#kbq%`DAYEgKW$K;!XL z0Z)UWzFnPz7Y;A*a}+zL7HjIj5q$S@k$2XYsues{p3{(duK5RCHNTd65V()7J@v56 zeKWN-=J|45sMrm=V^`qwAJhUnXKIV>L+=<}oMhGbVeK4$bYNzd-sr7w+MXb09dqV)ew#r3F_PWSfjjm&s(o~u_q9hNOQ;#X>FoRm(Ljn#3rNdnb?u4l}K|` zkjIxw-#Gz@a$Xf{QM&%r(;l7MBE9q5EI9otN=|k#Cw8D_Mh{jkuGTIZ6vfG_=Q{oE zY88R-#tnFpdvvJjp6*GSmTKbVEk5a8)%E?bWk*(^!-{{WVTIQOrui12Tm|=pI$;yLk z&I;UAZjS@CJxwGyP`uJb@6c(SYe{QBTm;&_Pq(tJ{Js07S&@8;a??4iahtX{&fbaK zL!gd&PB#Q)wcc*h+E%`_KOhNOfy(;I6PuF~UMcmK$mG=&Vi&~`@0jb0(ygZ|dT!Ju zc`wQj?~cd+`it-L{}iGLw_!1L%Nl)@V6BXwe@T6RHn6_86{Xuwz3mCBOAd~cA3?r9 ziWi!|zTJZz`uMIPxyxqaG7sfo-TlnZt1H(>y$+)>K0HIV^%`-3M7r%{NMR=4xh&ia zpND#V^=PXaRV-bQAT=0dq&L)>gi9r~lMWQ|MF(2#VIfMf{B84mF;2 z(<~_sn_nl@L(gWe!nWsfF<>>3J=xs}y?^Apnu}%^L`(dhqwOg<%MF6_FS6*(Oe@Ym z@vT}_Z?r>c^PHafBb0`UvX#z0&G+g$GrASQ27$s5_bPV?(@CqdeSvO))mX_91Hsjw zCF#yS$VF0(Mna%6{r8K=_~mPSP2VJj-C|#%2i7O@X$CCv-S7SURK$?4*7H*Ij`0_8 zPwarE`VzN}+Pb-nnWc5Vm!JEeA))FiDpqoxv|Y|>@mbr{B_uEKUV*6eoch(%ovU3$ z#$V0$DjUs3b+{5gOUn3uek-+?(Q(W-dNV1f1RSyg=YqzHq`>lz8J59mZe5lfvIYT` zIi{<}jmk-LS~(CV&B?f_$(})KyH%Lm9R4c7o>6ZGla;UE8LzxvY8Jn%Q@$aSlMDK- zqN(k~rqMq416|RS%-mSYA=JpZ*1CH632bV~yl5=v+Z4=tz8ZHXm2R~$a2S78O80hy zF^5NNOa*)LApjR#N4_59LPi9!Iw|efcAzSpOs!dSh=KJ`MmDQxu1$BAU}Vzl?si%m zx!TA0K(FQVb%FJNZGoSvA)L6T&K*Lmp+JWi^o1OQuI%|m$~4R`f=A7~*i0>fX)(5M zZJZVWldtB$TOZG`80Eo*ll6@s(rZ9u^0P8({vR6%6M_NmyUNH+0%jZAewd-cA2`af zhk`m_hK^{+8q-h6_(Gd<`W%btUP$TcwC@|(Ozn|rO6q0HL^jjwz!ZHe2Khnu4NV2^ zq(0RapJn-mrU9C2aLcR-e255q?D~=|unYv2@hYw1%n{t@(kpD-Nub@{wR{4Rp2`yr z6B_$h;bAd|*SH?T1vsz5nkwa>%HXi|2evA@90U7UQ=IRd+89Grc&xV^YdZ8B*nOAb zK*3kLZ0zd+<1k|LjV1XtB!b^OxCJ)p8)BK0Z4Zm|3P?X^d63}~=pY0-^z?)!B08*x zW`9#+&{JRzetZZsqVUAvmn=~QAnKyAj0fl!eni#>CPXi1WCB>)nm|gv^5Z6qonSts z4XY-*v_mw6MLG|p%Uzso(uaJM$7W>yi6V2rmB2a)n;MpOxMe(^8)e493Xolf0I~mi z6K2BXmz<|WVBW>=h42!UjRn06{^LQsmCAEx^**f6{Tp^{#~OlqPrpi=v(yu15@CI) z|4Fkso&}DEg*X4l((ePw4(@FF-$}7~h6B%76CPyPiST>|JfGcaN5tXmd4Y-O-JKG^ zMO%P(^#+L8ps{?46*;#cayC75Iu1*2@5S-Q4jgB_^N&1RATk}mJ-T^zP2dF(h|C=j zrYV775dF~3fqHbl$zYY+96blFD66uT_~wdKH(e4Mq#y2A&SMH>uYQ5<&k7S11Q~tc z*pBIKuPp{-+N+ZdSInm7r}HOcOi5mIzM0JROP#{&Uq<|iNlZ!#cArbmWa_kPj*$ta=P_T8HDxgGw$4meDRtdL=NpbPQ^rQ6w)iK}$D|`@dpN3W zVWPR-V_6lo_AcOc-^Vh{5$^H}E59F)*;%g`wHCW_U)~{_%~9K`=0?2dblP4G+%h@z zPPgRH?`w*=?wQPjF-svd>1o=ccdJ(F8>Mnos11CAjHx|*<~vR-JRY-5K#N$kFe{O6 zqa9B7dB^|kByV4!j7?kF5Ud6^6PB8C^EMwAo#+K%uaf3&vw>8p+1qAt-t_y$El%<) zLaN6o5Rvh|FXk`3PHVDJr^;vl?~HrHy;+2l8@c8sVDJ$*hKY}t(>XAG7eja5^gDygn^y5+X8(4BIcbl?(9AZ>UW?94l1c#0Lfn;v9l`j`Zhh#XBdO4ZGX-gd)_~4To+S6Y% z_`VO8y8>?>RJzf8eWhMTf75VfOAj|e3pj=7&yShWN@*d)KQBpBhBft6lKwr?lJ z2CN45o=uwCN4JqMHDo~Yo_|tkmyfRGoR9D99(=^;j+prJ5|Y#45VxxgIi`akqc(R? zl)lI5!dV7PZ01V~C0TL*QyT_AdWSh7yFnuOK!Op=0IxKIofY-rt}bc+`CV%}{x&ql zTV@KC*}xM3JT1t1d<~cZKtnkOdvLu4fP3~Eas2~=#4XB13=4S07JxzAob1u`2NLY& zfnAE#>>Tq6yb&SUJPQdz_D$$QB&?6{=*U6VzHf#cnDT2jP94}Fd;2BClEbhtyVMAR zXA~0Ejp11ZjHZ8Rfe?@KBym=v!4<)mPR95jYwDy!`09TlV%h3ICTza)Ll=2gfMgQ| zaJ+NKeTjm{w5YLDGHqv@tO;2VepVpBMOAHtRW8*q<-u8?x5@Epw!!N`@68eokJt1z zfEY=1yTW>aAljS=dQ%_A$*$?m5852!j{wKPF9eZEeY9-L0k+=BupF`xsYXcJ3B?)1 zmuX)e3)l2E2LGo$QrN=Q8!}|GVWBSaI0p149Rt0+qP3CY&Vg4B$aX z?D-aR-~-ug8)P%_yGQlb(ylY8-Vz`ffoQ{}0HyxQK0OI2wMaN<6+^1MYBacwRRGq* zs&3w2%fDu1IvAub6aL4y%PMI;YHlaCa(!Hq{X;D_)dqD+5a zH1k@J^{$7@1+HQWe$B=T!>7oyT7%KZ@R;QuB`mZa9-g|0c8HalASI|o8+q5jflh#N zkc*O{1S|sSiZ{xsTkqFomO7Xi|z#&LLB%6U_VaUr02paORB0)Fo9N2|af+lJ3 zk5va!ci|UE&DbpQ@iF{>#Na5HS?@lviA^K|vEjvs|JH~xP*RYYESrdo+9M8p@cKuD z9giKHz_N+RV(PtkfB7E~auW8yq&l03^cb5Dyf^trgs8ycCw19Gc|_*(cc=al`GJ@@ zY0V~zBQp2DQ}{;&pJ2x)UD!lEMCS9i`~MLkhLJHRo45{hEnvkw(TVJ4Me0yxF5o$GjZVC$Ie4ZWxHoI(a%LL>g z{m9oA2?NmrDAo3-gVJI7kTtP`8}9;2g*2H})E@JV(X;@2d)YucB6jmVJK#b|OddEfW;~b!1!L#pIxLG5*(f!Rx+= zU$#{lx0YIwW&p#Lc*LG_!0Ww~IJ6=Ubmwnf2(3N1`BW$_0Ug!Q#Z9~Xn7-jWs;4w| zsWmEnZ?hlb7{dXE)JT7Ro*pEX*j=;lJo^2=7b9ndUHbOVLaY3rUE4%F2X$lP<#!^+ z#hUK#{%kr8I&9+%iilomT%(b#ay&C?e`he!Ksk63hCn0IeF!%<*u%S_RK^0@@=t8Z{xE>*?2Ip=MVe8-{8 zlepkadFXFSQ3-#~Zi5thHSLT}YE-__9xx;)Ys`l3pu0QgNgY+4J3v+!nh7tTEfE=o z@UK5m7T9Bu5lMj?9gjP-CXvFK&kSbI=TYCKR?L0PYm~eGN_#az*KLKZEbQhpwoU8& zx}dY(5i~J$)3M8=Pw2&H$W(&r!N`Y7Dsw*RIVf{UUpwRUU@!Qp5^M^7F;N;%dnnmh zB-`{se5-EN zyx0{r&hw~=Wsg*mph@jfN6T)wN0(`;bgg%4wt$Jx!v(DiEq-pGrXyLhRp?i-bJmJi z!+duZ;Rcs5uM@@Fe`Jrx9h*_u`t?r(dSXe>wlMnO01}3l4q<2|ua+iO6&&y|6GiTI zr1iPb>RXXZj87s(czm(y}6y9?I5G6I-S!oHGdATJQ!3VdZh|X6RNjK{B zo}3U}P41K_pQVa)@ARX`Gp$UgD}no5I%fyU)+m_YOW2vtsO;YkLdr8|5{`aYSYx{D4@lt9Q z$-p~T3_7%vwQm$!(_5QF3|B6;_UA5{T`=3ke?oln41r?t#lNUavv}#sWZaJ5Rwj|h zv12dV?%+aNnnWgo7CJjEq8U|h9{MMY8~_UrJXlX!o{T4k%ouF@?KyXhG%T^YM_%^qK~uRfn@tY8TQ)@gZh=YU`XP%4_!E@P z{@Ozp5AoWRJ^r_r{~R-_G!F`KB|TlL7&+YyJGNxTw|ioSv@eRJJ3=H>FY8^W=TBOh zk&?z14K`3;+U3}e@pgt{}kL7iZbPJ^)r^POxq- zjO|@r;SUDhBYz_ROrn5Ey34(rEa?+qQwQH?w|~;1J6rmkeud4%7GV-Q0Zju-dIA7T z!|&Z@1Hs;ob+t$SNk@QnnRnR0TmV|#o%m;ODgdidkj^5=AT3a*pOSp2I@m(>kEn??K<@DbSPavRm(21 z-(sslx=^z(*IP5A6qrsg-@DH;!&(ZoiT;?d#_OlBQ+oKeiZ;$ zg~J}OfzW+fi~U&-YmXdKxZu-mg;;Hf1Ss6Brr|^68dC>YnNKxE2iZprq|4r*vb^RS zt+jX2?APzskPBHAo_)9x2BTD{I}ai8#rs+kG(Gd3b4}n`VDqq0+LSF24l^DC?{0x+ z$e{=eIP=Aw?3*(`I2ZoryA*Yu!&J`Kn!724gDaWkE-lCl_TTEm zSPlCvXr4DEg}mis=}-;ba(F`EL!{5A(SSKQN(Qb65kJ--W);c!utdSbELoQ^PJPzl uK*VZbbsY+|x~jB~<1`9|T91VDI@I%hcnuAoPw(M3sB@?FPvxDkzy5!iN|oLK literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/material-symbols-refresh.png b/src/main/resources/icons/material-symbols-refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..9b968a306ed19e4e6ef58a4d0fbb43c74d65d5d5 GIT binary patch literal 17192 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 Date: Mon, 18 Aug 2025 02:16:16 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(token,AxisInnovatorsBox)=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=8A=A5=E9=94=99NullPointerException=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0token=E6=8C=81=E4=B9=85=E5=8C=96=E5=8A=A0?= =?UTF-8?q?=E8=A7=A3=E5=AF=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- language/saved_language.properties | 2 +- .../innovators/box/AxisInnovatorsBox.java | 26 ++++++++-- .../box/tools/Crypto/AESCryptoUtil.java | 50 +++++++++++++++++++ .../box/tools/Crypto/Base64CryptoUtil.java | 15 ++++++ .../innovators/box/tools/Crypto/HashUtil.java | 12 +++++ .../box/tools/Crypto/WindowsDPAPIUtil.java | 15 ++++++ 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/axis/innovators/box/tools/Crypto/AESCryptoUtil.java create mode 100644 src/main/java/com/axis/innovators/box/tools/Crypto/Base64CryptoUtil.java create mode 100644 src/main/java/com/axis/innovators/box/tools/Crypto/HashUtil.java create mode 100644 src/main/java/com/axis/innovators/box/tools/Crypto/WindowsDPAPIUtil.java diff --git a/language/saved_language.properties b/language/saved_language.properties index b822022..ef6cfb5 100644 --- a/language/saved_language.properties +++ b/language/saved_language.properties @@ -1,3 +1,3 @@ #Current Loaded Language -#Sat Aug 16 18:11:03 CST 2025 +#Mon Aug 18 02:11:52 CST 2025 loadedLanguage=system\:zh_CN diff --git a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java index a628f50..d508447 100644 --- a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java +++ b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java @@ -12,6 +12,8 @@ import com.axis.innovators.box.register.RegistrationSettingsItem; import com.axis.innovators.box.register.RegistrationTool; import com.axis.innovators.box.register.RegistrationTopic; import com.axis.innovators.box.tools.*; +import com.axis.innovators.box.tools.Crypto.AESCryptoUtil; +import com.axis.innovators.box.tools.Crypto.Base64CryptoUtil; import com.axis.innovators.box.util.PythonResult; import com.axis.innovators.box.util.Tray; import com.axis.innovators.box.verification.LoginResult; @@ -94,14 +96,29 @@ public class AxisInnovatorsBox { (Exception) throwable : new Exception(throwable)); }); // 初始化,这里为了能够在登录窗口使用主题,特意将初始化放在构造函数中 - main.initLog4j2(); - main.setTopic(); + initLog4j2(); + setTopic(); // 加载登录信息,如果没有,弹出登录弹窗,后续可以删掉默认弹出 // TODO: login window should not be show when AxisInnovatorsBox initialize, // it should be show when user click login button. try { StateManager stateManager = new StateManager(); - String token = stateManager.getState("loginToken"); + String excryptedKey = "loginToken"; + try { + excryptedKey = Base64CryptoUtil.base64Encode(excryptedKey); + } catch (Exception e) { + logger.error("Failed to encrypt key", e); + } + String encryptedToken = stateManager.getState(excryptedKey); + String token = null; + if (encryptedToken != null && !encryptedToken.isEmpty()) { + try { + token = AESCryptoUtil.decrypt(encryptedToken); + } catch (Exception ex) { + logger.error("Token 解密失败", ex); + token = null; + } + } if (token == null || token.isEmpty()) { LoginResult loginResult = CasdoorLoginWindow.showLoginDialogAndGetLoginResult(); if (loginResult == null) { @@ -110,7 +127,8 @@ public class AxisInnovatorsBox { JOptionPane.INFORMATION_MESSAGE); } else if (loginResult.success()) { loginData = loginResult.loginData(); - stateManager.saveState("loginToken", loginResult.token()); + String encrypted = AESCryptoUtil.encrypt(loginResult.token()); + stateManager.saveState(excryptedKey, encrypted); logger.info( "Login result: token: " + loginResult.token() + ", user: " + loginResult.user()); JOptionPane.showMessageDialog(null, "登录成功", "登录", diff --git a/src/main/java/com/axis/innovators/box/tools/Crypto/AESCryptoUtil.java b/src/main/java/com/axis/innovators/box/tools/Crypto/AESCryptoUtil.java new file mode 100644 index 0000000..e01cd7a --- /dev/null +++ b/src/main/java/com/axis/innovators/box/tools/Crypto/AESCryptoUtil.java @@ -0,0 +1,50 @@ +package com.axis.innovators.box.tools.Crypto; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import java.nio.file.*; +import java.security.SecureRandom; +import java.util.Base64; + +public class AESCryptoUtil { + private static final String KEY_FILE = System.getProperty("user.home") + "/.lingqi/.axis_box_key"; + + // 获取密钥(Base64字符串,长度16字节) + public static byte[] getKeyBytes() throws Exception { + Path path = Paths.get(KEY_FILE); + if (Files.exists(path)) { + byte[] encrypted = Base64.getDecoder().decode(Files.readAllBytes(path)); + return WindowsDPAPIUtil.unprotect(encrypted); + } else { + // 首次生成密钥 + byte[] keyBytes = new byte[16]; + new SecureRandom().nextBytes(keyBytes); + byte[] encrypted = WindowsDPAPIUtil.protect(keyBytes); + Files.createDirectories(path.getParent()); + Files.write(path, Base64.getEncoder().encode(encrypted)); + return keyBytes; + } + } + + // 加密 + public static String encrypt(String data) throws Exception { + byte[] keyBytes = getKeyBytes(); + SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(encrypted); + } + + // 解密 + public static String decrypt(String encrypted) throws Exception { + byte[] keyBytes = getKeyBytes(); + SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + byte[] decoded = Base64.getDecoder().decode(encrypted); + byte[] decrypted = cipher.doFinal(decoded); + return new String(decrypted, "UTF-8"); + } +} \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/tools/Crypto/Base64CryptoUtil.java b/src/main/java/com/axis/innovators/box/tools/Crypto/Base64CryptoUtil.java new file mode 100644 index 0000000..f16370a --- /dev/null +++ b/src/main/java/com/axis/innovators/box/tools/Crypto/Base64CryptoUtil.java @@ -0,0 +1,15 @@ +package com.axis.innovators.box.tools.Crypto; + +import java.util.Base64; + +public class Base64CryptoUtil { + + public static String base64Encode(String input) { + return Base64.getEncoder().encodeToString(input.getBytes()); + } + + public static String base64Decode(String input) { + byte[] decodedBytes = Base64.getDecoder().decode(input); + return new String(decodedBytes); + } +} diff --git a/src/main/java/com/axis/innovators/box/tools/Crypto/HashUtil.java b/src/main/java/com/axis/innovators/box/tools/Crypto/HashUtil.java new file mode 100644 index 0000000..5b33181 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/tools/Crypto/HashUtil.java @@ -0,0 +1,12 @@ +package com.axis.innovators.box.tools.Crypto; + +import java.security.MessageDigest; +import java.util.Base64; + +public class HashUtil { + public static String sha256(String input) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(hash); // 输出为Base64字符串 + } +} diff --git a/src/main/java/com/axis/innovators/box/tools/Crypto/WindowsDPAPIUtil.java b/src/main/java/com/axis/innovators/box/tools/Crypto/WindowsDPAPIUtil.java new file mode 100644 index 0000000..83e4900 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/tools/Crypto/WindowsDPAPIUtil.java @@ -0,0 +1,15 @@ +package com.axis.innovators.box.tools.Crypto; + +import com.sun.jna.platform.win32.Crypt32Util; + +public class WindowsDPAPIUtil { + // 加密 + public static byte[] protect(byte[] data) { + return Crypt32Util.cryptProtectData(data); + } + + // 解密 + public static byte[] unprotect(byte[] encrypted) { + return Crypt32Util.cryptUnprotectData(encrypted); + } +} \ No newline at end of file