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

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>client</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency>
<groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>

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);

View File

@@ -20,7 +20,7 @@
<?import javafx.scene.paint.Color?>
<?import javafx.scene.text.Font?>
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.manager.ds.company.controller.contact.CompanyContactWindowController">
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.ds.company.controller.contact.CompanyContactWindowController">
<center>
<TabPane fx:id="tabPane" tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0">
<tabs>
@@ -114,3 +114,4 @@
</HBox>
</bottom>
</BorderPane>

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>server</module>

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>server</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency>
<groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId>
<version>0.0.58-SNAPSHOT</version>
<version>0.0.80-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -75,6 +75,14 @@
<!-- <scope>provided</scope> -->
<!-- </dependency> -->
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>

View File

@@ -0,0 +1,159 @@
package com.ecep.contract.api.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.data.domain.Sort;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ecep.contract.ds.other.service.EmployeeAuthBindService;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Employee;
import com.ecep.contract.model.EmployeeAuthBind;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/api")
public class LoginApiController {
private static final Logger logger = LoggerFactory.getLogger(LoginApiController.class);
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private EmployeeService employeeService;
@Autowired
private EmployeeAuthBindService employeeAuthBindService;
/**
* JSON格式的登录API
* 使用Spring Security进行认证返回JSON格式的响应
*/
@PostMapping("/login")
public Map<String, Object> jsonLogin(@RequestBody LoginRequest loginRequest, HttpServletRequest request) {
// 获取HttpSession
HttpSession session = request.getSession();
// 添加日志记录,确认方法被调用
logger.debug("jsonLogin方法被调用请求类型: {}", loginRequest.getType());
logger.debug("请求用户名: {}", loginRequest.getUsername());
logger.debug("会话ID: {}", session != null ? session.getId() : "null");
Map<String, Object> result = new HashMap<>();
try {
Employee employee = null;
if ("client".equals(loginRequest.getType())) {
// 根据用户名查找Employee对象
employee = employeeService.findByAccount(loginRequest.getUsername());
if (employee == null) {
result.put("success", false);
result.put("error", "用户信息不存在");
return result;
}
List<EmployeeAuthBind> authBinds = employeeAuthBindService.findAllByEmployee(employee, Sort.unsorted());
if (authBinds.isEmpty()) {
result.put("success", false);
result.put("error", "用户未绑定认证信息");
return result;
}
EmployeeAuthBind matched = null;
Map<String, String> signMap = loginRequest.getSign();
if (signMap != null) {
for (EmployeeAuthBind authBind : authBinds) {
String clientIp = signMap.get(authBind.getMac());
if (clientIp != null && authBind.getIp().equals(clientIp)) {
matched = authBind;
break;
}
}
}
if (matched == null) {
result.put("success", false);
result.put("error", "认证信息错误");
return result;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), "default123");
// 使用AuthenticationManager进行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 设置认证结果到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
// 创建认证令牌
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
// 使用AuthenticationManager进行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 设置认证结果到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
employee = employeeService.findByAccount(loginRequest.getUsername());
}
if (employee != null) {
// 获取当前登录用户
User currentUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 返回登录成功的信息
result.put("success", true);
result.put("employeeId", employee.getId());
result.put("sessionId", session.getId());
result.put("username", employee.getAccount());
result.put("roles", currentUser.getAuthorities());
result.put("message", "登录成功");
} else {
// 用户不存在
result.put("success", false);
result.put("error", "用户信息不存在");
}
} catch (AuthenticationException e) {
// 认证失败
result.put("success", false);
result.put("error", "用户名或密码错误");
} catch (Exception e) {
// 其他错误
result.put("success", false);
result.put("error", "登录过程中发生错误: " + e.getMessage());
}
return result;
}
/**
* 登录请求的JSON数据模型
*/
@Data
public static class LoginRequest {
private String type;
private String username;
private String password;
private Map<String, String> sign = new HashMap<>();
}
}

View File

@@ -15,13 +15,15 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器捕获并处理所有Controller层抛出的异常将错误信息以JSON格式返回给前端
*/
// @RestControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@@ -36,10 +38,25 @@ public class GlobalExceptionHandler {
result.put("code", 500);
result.put("message", "系统内部错误:" + e.getMessage());
result.put("errorType", e.getClass().getName());
result.put("stackTrace", getStackTraceAsString(e));
result.put("success", false);
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 将异常堆栈转换为字符串
*/
private String getStackTraceAsString(Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
try {
throwable.printStackTrace(pw);
return sw.toString();
} finally {
pw.close();
}
}
/**
* 处理运行时异常
*/
@@ -50,6 +67,7 @@ public class GlobalExceptionHandler {
result.put("code", 500);
result.put("message", "运行时错误:" + e.getMessage());
result.put("errorType", e.getClass().getName());
result.put("stackTrace", getStackTraceAsString(e));
result.put("success", false);
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}

View File

@@ -51,8 +51,8 @@ public class SecurityConfig {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login.html", "/css/**", "/js/**", "/images/**", "/webjars/**", "/login",
"/error")
.permitAll() // 允许静态资源、登录页面错误页面访问
"/error", "/api/login")
.permitAll() // 允许静态资源、登录页面错误页面和JSON登录API访问
.anyRequest().authenticated() // 其他所有请求需要认证
)
.csrf(AbstractHttpConfigurer::disable) // 禁用CSRF保护适合开发环境

View File

@@ -1,6 +1,9 @@
package com.ecep.contract.ds.other.controller;
import java.util.Map;
import java.util.HashMap;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
@@ -8,6 +11,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -50,4 +54,35 @@ public class EmployeeController {
employeeService.delete(employee);
}
/**
* 获取当前登录用户的信息
* 包括employeeId和sessionId
*/
@RequestMapping("/currentUser")
public Map<String, Object> getCurrentUser(HttpSession session) {
Map<String, Object> result = new HashMap<>();
try {
// 获取当前登录用户
User currentUser = SecurityUtils.getCurrentUser();
if (currentUser != null) {
// 根据用户名查找Employee对象
Employee employee = employeeService.findByName(currentUser.getUsername());
if (employee != null) {
result.put("employeeId", employee.getId());
result.put("sessionId", session.getId());
result.put("success", true);
return result;
}
}
} catch (Exception e) {
// 处理异常
}
// 如果获取失败,返回错误信息
result.put("success", false);
result.put("error", "无法获取当前用户信息");
return result;
}
}

View File

@@ -4,21 +4,14 @@ import java.util.List;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import com.ecep.contract.ds.MyRepository;
import com.ecep.contract.model.Employee;
import com.ecep.contract.model.EmployeeAuthBind;
@Lazy
@Repository
public interface EmployeeAuthBindRepository extends
// JDBC interfaces
CrudRepository<EmployeeAuthBind, Integer>, PagingAndSortingRepository<EmployeeAuthBind, Integer>,
// JPA interfaces
JpaRepository<EmployeeAuthBind, Integer>, JpaSpecificationExecutor<EmployeeAuthBind> {
public interface EmployeeAuthBindRepository extends MyRepository<EmployeeAuthBind, Integer> {
List<EmployeeAuthBind> findAllByEmployee(Employee employee, Sort sort);
}