feat: 实现基于JSON的登录API和安全认证

refactor: 重构登录逻辑和会话管理

fix: 修复会话ID类型和WebSocket连接问题

build: 更新项目版本号和添加Servlet API依赖

style: 清理无用导入和注释代码
This commit is contained in:
2025-09-08 17:46:48 +08:00
parent 3b90db0450
commit 23e1f98ae5
17 changed files with 477 additions and 223 deletions

View File

@@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import com.ecep.contract.controller.BaseController;
import com.ecep.contract.controller.HomeWindowController;
import com.ecep.contract.controller.OkHttpLoginController;
import com.ecep.contract.task.TaskMonitorCenter;
import com.ecep.contract.util.TextMessageHolder;
@@ -25,7 +26,7 @@ import com.ecep.contract.vm.CurrentEmployee;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
@@ -36,6 +37,10 @@ import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import lombok.Getter;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
/**
* JavaFx 应用程序
@@ -63,9 +68,12 @@ public class Desktop extends Application {
private ScheduledExecutorService scheduledExecutorService = null;
private final TaskMonitorCenter taskMonitorCenter = new TaskMonitorCenter();
private final SimpleIntegerProperty sessionId = new SimpleIntegerProperty(0);
private final SimpleStringProperty sessionId = new SimpleStringProperty("");
@Getter
private final CurrentEmployee activeEmployee = new CurrentEmployee();
@Getter
private OkHttpClient httpClient;
public void setActiveEmployeeId(int activeEmployeeId) {
activeEmployee.getId().set(activeEmployeeId);
@@ -75,11 +83,11 @@ public class Desktop extends Application {
return activeEmployee.getId().get();
}
public int getSessionId() {
public String getSessionId() {
return sessionId.get();
}
public void setSessionId(int sessionId) {
public void setSessionId(String sessionId) {
this.sessionId.set(sessionId);
}
@@ -136,7 +144,6 @@ public class Desktop extends Application {
// 更新窗口标题
Node titleNode = root.lookup("#title");
if (titleNode != null) {
primaryStage.setTitle(((Text) titleNode).getText());
}
@@ -187,16 +194,37 @@ public class Desktop extends Application {
});
try {
initHttpClient();
OkHttpLoginController controller = new OkHttpLoginController();
controller.setHttpClient(this.httpClient);
controller.setHolder(holder);
controller.setPrimaryStage(primaryStage);
controller.setProperties(properties);
while (true) {
controller.tryLogin();
if (getActiveEmployeeId() > 0) {
break;
// while (true) {
controller.tryLogin().whenComplete((v, e) -> {
if (e != null) {
holder.error("登录失败:" + e.getMessage());
} else {
holder.info("登录成功");
try {
while (!SpringApp.isRunning()) {
System.out.println("等待启动");
Thread.sleep(1000);
}
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
// 必须要等待启动成功后才能关闭主场景,否则进程结束程序退出
HomeWindowController.show().thenRun(() -> Platform.runLater(primaryStage::close));
}
}
});
// if (getActiveEmployeeId() > 0) {
// break;
// }
// }
} catch (Exception e) {
holder.error("登录失败:" + e.getMessage());
logger.error(e.getMessage(), e);
@@ -210,6 +238,26 @@ public class Desktop extends Application {
}
private void initHttpClient() {
this.httpClient = new OkHttpClient().newBuilder().cookieJar(new CookieJar() {
private final List<Cookie> cookies = new java.util.ArrayList<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
// 保存服务器返回的Cookie如JSESSIONID
this.cookies.addAll(cookies);
System.out.println("保存Cookie: " + cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
// 请求时自动携带Cookie
return cookies;
}
}).build();
}
@Override
public void stop() throws Exception {
if (logger.isDebugEnabled()) {

View File

@@ -34,6 +34,7 @@ import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.FxmlUtils;
import com.ecep.contract.vm.CurrentEmployee;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.scene.Node;
@@ -46,6 +47,12 @@ import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
@Lazy
@Scope("prototype")
@@ -70,6 +77,9 @@ public class HomeWindowController extends BaseController {
public Label taskMonitorLabel;
public Label employeeStatusLabel;
private WebSocket webSocket;
private String webSocketUrl = "ws://localhost:8080/ws";
public void initialize() {
openCompanyManagerWindow.setOnAction(event -> showInOwner(CompanyManagerWindowController.class));
openProjectManagerWindow.setOnAction(event -> showInOwner(ProjectManagerWindowController.class));
@@ -95,6 +105,7 @@ public class HomeWindowController extends BaseController {
employeeStatusLabel.textProperty().bind(Desktop.instance.getActiveEmployee().getName());
Desktop.instance.getTaskMonitorCenter().bindStatusLabel(taskMonitorLabel);
Desktop.instance.getActiveEmployee().initialize();
initWebSocket();
}
@EventListener
@@ -209,4 +220,64 @@ public class HomeWindowController extends BaseController {
public void onShowTaskMonitorWindowAction(ActionEvent event) {
showInOwner(TaskMonitorViewController.class);
}
private void initWebSocket() {
OkHttpClient httpClient = Desktop.instance.getHttpClient();
try {
// 构建WebSocket请求包含认证信息
Request request = new Request.Builder()
.url(webSocketUrl)
.build();
webSocket = httpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
Platform.runLater(() -> {
setStatus("WebSocket连接已建立");
// 登录成功后的处理
System.out.println("WebSocket连接已建立");
});
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 处理收到的文本消息
logger.debug("收到WebSocket消息: " + text);
// 这里可以根据需要处理从服务器接收的数据
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
// 处理收到的二进制消息
logger.debug("收到二进制WebSocket消息长度: " + bytes.size());
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
logger.debug("WebSocket连接正在关闭: 代码=" + code + ", 原因=" + reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
logger.debug("WebSocket连接已关闭: 代码=" + code + ", 原因=" + reason);
// 可以在这里处理重连逻辑
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
logger.error("WebSocket连接失败: " + t.getMessage());
Platform.runLater(() -> {
setStatus("WebSocket连接失败: " + t.getMessage());
});
}
});
} catch (Exception e) {
logger.error("建立WebSocket连接失败: " + e.getMessage());
Platform.runLater(() -> {
setStatus("建立WebSocket连接失败: " + e.getMessage());
});
}
}
}

View File

@@ -524,7 +524,7 @@ public class LoginWidowController implements MessageHolder {
info("请稍后...");
}
Desktop.instance.setActiveEmployeeId(employeeInfo.employeeId);
Desktop.instance.setSessionId(employeeInfo.sessionId);
// Desktop.instance.setSessionId(employeeInfo.sessionId);
tryShowHomeWindow();
}

View File

@@ -18,11 +18,13 @@ import org.springframework.util.StringUtils;
import com.ecep.contract.Desktop;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
@@ -34,16 +36,16 @@ import javafx.stage.Stage;
import lombok.Setter;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class OkHttpLoginController implements MessageHolder {
private static final Logger logger = LoggerFactory.getLogger(OkHttpLoginController.class);
@@ -55,14 +57,30 @@ public class OkHttpLoginController implements MessageHolder {
private Stage primaryStage;
@Setter
private Properties properties;
@Setter
private OkHttpClient httpClient;
private WebSocket webSocket;
private String serverUrl;
private String webSocketUrl;
public OkHttpLoginController() {
this.httpClient = new OkHttpClient();
this.httpClient = new OkHttpClient().newBuilder().cookieJar(new CookieJar() {
private final List<Cookie> cookies = new java.util.ArrayList<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
// 保存服务器返回的Cookie如JSESSIONID
this.cookies.addAll(cookies);
System.out.println("保存Cookie: " + cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
// 请求时自动携带Cookie
return cookies;
}
}).build();
}
@Override
@@ -79,20 +97,32 @@ public class OkHttpLoginController implements MessageHolder {
this.webSocketUrl = "ws://" + host + ":" + port + "/ws";
}
public void tryLogin() {
public CompletableFuture<Void> tryLogin() {
initServerUrls();
// 检查配置文件中是否保存用户名和密码
String userName = getUserName();
String password = getPassword();
CompletableFuture<Void> loginFuture = new CompletableFuture<>();
if (StringUtils.hasText(userName) && StringUtils.hasText(password)) {
login(userName, password);
login(userName, password).whenComplete((v, e) -> {
if (e != null) {
loginFuture.completeExceptionally(e);
} else {
loginFuture.complete(v);
}
});
} else {
Platform.runLater(() -> {
showLoginDialog();
if (!loginFuture.isDone()) {
loginFuture.complete(null);
}
});
}
return loginFuture;
}
private String getUserName() {
@@ -193,8 +223,19 @@ public class OkHttpLoginController implements MessageHolder {
}
// 执行登录
login(username, password);
stage.close();
login(username, password).whenComplete((v, e) -> {
if (e != null) {
Platform.runLater(() -> {
errorLabel.setText(e.getMessage());
errorLabel.setVisible(true);
// showError("登录失败", e.getMessage());
});
return;
}
Platform.runLater(() -> {
stage.close();
});
});
});
// 创建场景并设置到窗口
@@ -202,160 +243,84 @@ public class OkHttpLoginController implements MessageHolder {
stage.setScene(scene);
stage.setResizable(false);
stage.showAndWait();
System.out.println("登录窗口关闭");
}
private void login(String username, String password) {
private CompletableFuture<Void> login(String username, String password) {
// 添加详细日志记录服务器URL和请求准备情况
info("正在连接服务器: " + serverUrl);
logger.debug("login方法被调用用户名: " + username);
CompletableFuture<Void> future = new CompletableFuture<>();
try {
// 构建表单格式的登录请求
RequestBody body = new FormBody.Builder()
.add("username", username)
.add("password", password)
.build();
ObjectMapper objectMapper = SpringApp.getBean(ObjectMapper.class);
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("username", username);
objectNode.put("password", password);
objectNode.put("type", "client");
// 将MacIP列表转换为Map<String, String>格式MAC地址->IP地址
List<MacIP> macIpList = getMacAndIP().join();
ObjectNode signNode = objectMapper.createObjectNode();
for (MacIP macIp : macIpList) {
signNode.put(macIp.mac, macIp.ip);
}
objectNode.set("sign", signNode);
// 构建JSON格式的登录请求
RequestBody body = RequestBody.create(objectNode.toString(), JSON);
// 构建并记录完整的请求URL
String loginUrl = serverUrl + "/api/login";
logger.debug("构建登录请求URL: " + loginUrl);
Request request = new Request.Builder()
.url(serverUrl + "/login")
.url(loginUrl)
.post(body)
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Platform.runLater(() -> {
error("登录失败: 无法连接到服务器 - " + e.getMessage());
showError("登录失败", "无法连接到服务器,请检查网络连接或服务器配置。");
});
}
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
future.completeExceptionally(
new IOException("登录失败: 无法连接到服务器,请检查网络连接或服务器配置 - " + e.getMessage(), e));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
if (!response.isSuccessful()) {
Platform.runLater(() -> {
error("登录失败: 服务器返回错误码 - " + response.code());
showError("登录失败", "用户名或密码错误,或服务器暂时不可用。");
});
future.completeExceptionally(
new IOException("登录失败: 服务器返回错误码 - " + response.toString()));
return;
}
ResponseBody body = response.body();
System.out.println("contentType = " + body.contentType());
JsonNode jsonNode = objectMapper.readTree(body.string());
try {
// 解析登录响应
String responseBody = response.body().string();
logger.debug("登录响应: " + responseBody);
// 这里需要根据实际的响应格式解析数据
// 假设响应包含employeeId和sessionId
int employeeId = extractEmployeeId(responseBody);
int sessionId = extractSessionId(responseBody);
if (employeeId > 0 && sessionId > 0) {
Platform.runLater(() -> {
info("登录成功正在建立WebSocket连接...");
// 登录成功后建立WebSocket连接
establishWebSocketConnection(employeeId, sessionId);
});
} else {
Platform.runLater(() -> {
error("登录失败: 无效的响应数据");
showError("登录失败", "服务器返回无效的响应数据。");
});
}
} catch (Exception e) {
Platform.runLater(() -> {
error("登录失败: 解析响应失败 - " + e.getMessage());
showError("登录失败", "解析服务器响应时发生错误。");
});
boolean success = jsonNode.get("success").asBoolean(false);
if (!success) {
future.completeExceptionally(
new IOException("登录失败: 服务器返回错误 - " + jsonNode.get("error").asText()));
return;
}
System.out.println("登录成功: " + jsonNode.toString());
// 登录成功后调用新的API端点获取用户信息
Desktop.instance.setActiveEmployeeId(jsonNode.get("employeeId").asInt());
Desktop.instance.setSessionId(jsonNode.get("sessionId").asText());
future.complete(null);
} finally {
// 确保主响应体被关闭
response.close();
}
});
} catch (Exception e) {
Platform.runLater(() -> {
error("登录过程中发生错误: " + e.getMessage());
showError("登录错误", e.getMessage());
});
}
}
private void establishWebSocketConnection(int employeeId, int sessionId) {
try {
// 构建WebSocket请求包含认证信息
Request request = new Request.Builder()
.url(webSocketUrl + "?employeeId=" + employeeId + "&sessionId=" + sessionId)
.build();
webSocket = httpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
Platform.runLater(() -> {
info("WebSocket连接已建立");
// 登录成功后的处理
logined(employeeId, sessionId);
});
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 处理收到的文本消息
logger.debug("收到WebSocket消息: " + text);
// 这里可以根据需要处理从服务器接收的数据
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
// 处理收到的二进制消息
logger.debug("收到二进制WebSocket消息长度: " + bytes.size());
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
logger.debug("WebSocket连接正在关闭: 代码=" + code + ", 原因=" + reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
logger.debug("WebSocket连接已关闭: 代码=" + code + ", 原因=" + reason);
// 可以在这里处理重连逻辑
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
logger.error("WebSocket连接失败: " + t.getMessage());
Platform.runLater(() -> {
error("WebSocket连接失败: " + t.getMessage());
showError("连接错误", "与服务器的WebSocket连接失败。");
});
}
});
} catch (Exception e) {
logger.error("建立WebSocket连接失败: " + e.getMessage());
Platform.runLater(() -> {
error("建立WebSocket连接失败: " + e.getMessage());
showError("连接错误", "无法建立与服务器的WebSocket连接。");
});
future.completeExceptionally(new IOException("登录过程中发生错误: " + e.getMessage(), e));
}
}
private void logined(int employeeId, int sessionId) {
// 设置当前登录员工信息
Desktop.instance.setActiveEmployeeId(employeeId);
Desktop.instance.setSessionId(sessionId);
// 显示主窗口
tryShowHomeWindow();
}
void tryShowHomeWindow() {
try {
while (!SpringApp.isRunning()) {
System.out.println("等待启动");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 必须要等待启动成功后才能关闭主场景,否则进程结束程序退出
HomeWindowController.show().thenRun(() -> Platform.runLater(primaryStage::close));
return future;
}
CompletableFuture<List<MacIP>> getMacAndIP() {
@@ -402,46 +367,6 @@ public class OkHttpLoginController implements MessageHolder {
});
}
// 辅助方法从响应中提取employeeId
private int extractEmployeeId(String responseBody) {
// 这里应该根据实际的响应格式进行解析
// 示例:假设响应格式是 {"employeeId": 123, "sessionId": 456}
try {
int start = responseBody.indexOf("employeeId") + 12;
int end = responseBody.indexOf(",", start);
if (end == -1) {
end = responseBody.indexOf("}", start);
}
return Integer.parseInt(responseBody.substring(start, end).trim());
} catch (Exception e) {
return -1;
}
}
// 辅助方法从响应中提取sessionId
private int extractSessionId(String responseBody) {
// 这里应该根据实际的响应格式进行解析
try {
int start = responseBody.indexOf("sessionId") + 11;
int end = responseBody.indexOf(",", start);
if (end == -1) {
end = responseBody.indexOf("}", start);
}
return Integer.parseInt(responseBody.substring(start, end).trim());
} catch (Exception e) {
return -1;
}
}
private void showError(String title, String message) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
});
}
// WebSocket消息发送方法
public void sendMessage(String message) {

View File

@@ -11,10 +11,6 @@ import org.springframework.stereotype.Component;
import com.ecep.contract.constant.CompanyCustomerConstant;
import com.ecep.contract.constant.CompanyVendorConstant;
import com.ecep.contract.constant.ContractConstant;
import com.ecep.contract.service.CompanyCustomerFileService;
import com.ecep.contract.service.CompanyCustomerService;
import com.ecep.contract.service.ContractService;
import com.ecep.contract.service.YongYouU8Service;
import com.ecep.contract.util.StringConfig;
import jakarta.annotation.PreDestroy;

View File

@@ -10,7 +10,6 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.ecep.contract.util.ProxyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
@@ -35,6 +34,7 @@ import com.ecep.contract.service.CompanyVendorService;
import com.ecep.contract.service.ContractService;
import com.ecep.contract.task.Tasker;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.util.ProxyUtils;
import lombok.Setter;

View File

@@ -157,7 +157,7 @@ public class CurrentEmployee extends EmployeeViewModel {
*/
executorService.scheduleWithFixedDelay(() -> {
try {
SpringApp.getBean(EmployeeService.class).updateActive(Desktop.instance.getSessionId());
// SpringApp.getBean(EmployeeService.class).updateActive(Desktop.instance.getSessionId());
} catch (Exception e) {
if (logger.isErrorEnabled()) {
logger.error("updateActive:{}", e.getMessage(), e);