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 777586d..6f8a5a8 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ configurations { proguardLib } + // JDK 版本检查 def requiredJavaVersion = 20 def currentJavaVersion = JavaVersion.current().majorVersion.toInteger() 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/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 d204f06..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; @@ -44,6 +46,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 +86,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,12 +95,30 @@ public class AxisInnovatorsBox { organizingCrashReports(throwable instanceof Exception ? (Exception) throwable : new Exception(throwable)); }); - + // 初始化,这里为了能够在登录窗口使用主题,特意将初始化放在构造函数中 + initLog4j2(); + 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"); + 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) { @@ -106,23 +127,31 @@ public class AxisInnovatorsBox { 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()); + String encrypted = AESCryptoUtil.encrypt(loginResult.token()); + stateManager.saveState(excryptedKey, encrypted); + 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 +1007,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 +1020,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/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 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/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/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/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..3fbaab2 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/window/CasdoorLoginWindow.java @@ -0,0 +1,299 @@ +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() { + 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); + dialog.setLocationRelativeTo(null); // 居中显示 + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); + + if (browser != null) { + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + + 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); + + 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(); + } + if (browser != null) { + 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/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/src/main/resources/icons/material-symbols-back.png b/src/main/resources/icons/material-symbols-back.png new file mode 100644 index 0000000..9503541 Binary files /dev/null and b/src/main/resources/icons/material-symbols-back.png differ 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 0000000..e30aa63 Binary files /dev/null and b/src/main/resources/icons/material-symbols-forward.png differ 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 0000000..b075c92 Binary files /dev/null and b/src/main/resources/icons/material-symbols-home.png differ 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 0000000..9b968a3 Binary files /dev/null and b/src/main/resources/icons/material-symbols-refresh.png differ diff --git a/state/toolbox.properties b/state/toolbox.properties new file mode 100644 index 0000000..e69de29