STATIC_RESOURCES = List.of("css", "js", "img", "fonts", "favicon.ico");
+
+ private final Socket clientSocket;
+ private final String staticRoot;
+ private final String webRoot;
+
+ public RequestHandler(Socket socket, String staticRoot, String webRoot) {
+ this.clientSocket = socket;
+ this.staticRoot = staticRoot;
+ this.webRoot = webRoot;
+ }
+
+ @Override
+ public void run() {
+ try (InputStream input = clientSocket.getInputStream(); OutputStream output = clientSocket.getOutputStream()) {
+
+ // 解析请求
+ Request request = parseRequest(input);
+
+ Response response = ControllerSupplier.invoke(request);
+ if (response == null) {
+ // 处理请求并生成响应(静态画面)
+ response = handleRequestPage(request);
+ }
+ // 画面渲染(动态画面)
+ pageLive(request, response);
+
+ // 发送响应
+ sendResponse(output, response);
+ } catch (SSLHandshakeException e) {
+ logger.warning("证书不受信任!请导入证书或使用受信任的证书。");
+ } catch (Exception e) {
+ logger.warning("Error handling request: " + e.getMessage());
+ e.printStackTrace();
+ } finally {
+ try {
+ clientSocket.close();
+ } catch (IOException e) {
+ logger.warning("Error closing client socket: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void pageLive(Request request, Response response) {
+ if (!request.getPath().endsWith("html") && !request.getPath().equals("/")) {
+ return;
+ }
+ byte[] originalBytes = response.getContent();
+ // 模拟模板引擎
+ // 1. 获取原始的字符串
+ String originalString = new String(originalBytes, StandardCharsets.UTF_8);
+
+ // 2. 替换字符串
+ String params = request.getParams();
+ if (params == null) {
+ params = "";
+ } else {
+ String[] split = params.split("&");
+ for (String s : split) {
+ String[] split1 = s.split("=");
+ if (split1.length == 2) {
+ originalString = originalString.replace(STR."${\{split1[0]}}", split1[1]);
+ }
+ }
+ }
+ // 3. 将替换后的 String 转回 byte[]
+ response.setContent(originalString.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private Request parseRequest(InputStream input) throws IOException, URISyntaxException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+ String requestLine = reader.readLine();
+ if (requestLine == null) {
+ throw new IOException("Empty request");
+ }
+ // GET /index.html?a=1 HTTP/1.1
+ // Host: 127.0.0.1:8080
+ // User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
+ // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;
+ // v=b3;q=0.7
+
+ // POST /getUser HTTP/1.1
+ // Content-Type: application/json
+ // User-Agent: PostmanRuntime/7.44.0
+ // Accept: */*
+ // Host: 127.0.0.1:8080
+ // Accept-Encoding: gzip, deflate, br
+ // Connection: keep-alive
+ // Content-Length: 10
+
+ logger.info("=====================================================");
+ logger.info(requestLine);
+ String[] parts = requestLine.split(" ");
+ if (parts.length != 3) {
+ throw new IOException("Invalid request line: " + requestLine);
+ }
+
+ String method = parts[0]; // GET
+ String path = parts[1]; // /index.html
+
+ URI url = new URI(path);
+ path = url.getPath();
+ String params = url.getQuery();
+
+
+ // 读取请求头
+ String headerLine;
+ int contentLength = 0; // 输入流中字节长度
+ while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) {
+ logger.info(headerLine);
+ if (method.equals("POST") && headerLine.startsWith("Content-Length:")) {
+ contentLength = Integer.parseInt(headerLine.split(":")[1].trim());
+ }
+ }
+
+ // 读取POST请求体
+ StringBuilder requestBody = new StringBuilder();
+ if (method.equals("POST") && contentLength > 0) {
+ char[] buffer = new char[contentLength];
+ reader.read(buffer, 0, contentLength);
+ requestBody.append(buffer);
+ logger.info("Body: " + requestBody);
+ }
+ logger.info("Params: " + params);
+
+ logger.info("=====================================================");
+ return new Request(method, path, requestBody.toString(), params);
+ }
+
+ private Response handleRequestPage(Request request) {
+ String path = request.getPath();
+
+ // 默认主页
+ if (path.equals("/")) {
+ path = "/index.html";
+ }
+ path = getAbsolutePath(path);
+
+ // 获取classPath中的文件路径
+ String classPath = Objects.requireNonNull(getClass().getClassLoader().getResource("")).getPath();
+ if (OS_NAME.startsWith("Windows")) {
+ classPath = classPath.substring(1);
+ }
+ Path filePath = Paths.get(classPath, path);
+
+ // 安全检查,防止目录遍历攻击
+ if (!filePath.startsWith(Paths.get(classPath, webRoot)) && !filePath.startsWith(Paths.get(classPath, staticRoot))) {
+ return new Response(403, "Forbidden", "text/plain;charset=utf-8", "Access denied".getBytes());
+ }
+
+ // 检查文件是否存在且是普通文件
+ if (Files.exists(filePath) && Files.isRegularFile(filePath)) {
+ try {
+ byte[] content = Files.readString(filePath, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
+ String contentType = determineContentType(filePath);
+ return new Response(200, "OK", contentType, content);
+ } catch (IOException e) {
+ return new Response(500, "Internal Server Error", "text/plain;charset=utf-8", "Error reading file".getBytes());
+ }
+ } else {
+ return new Response(404, "Not Found", "text/plain;charset=utf-8", "Page not found".getBytes());
+ }
+ }
+
+ private String getAbsolutePath(String path) {
+ if (path.startsWith("/") && STATIC_RESOURCES.stream().anyMatch(path::endsWith)) {
+ return staticRoot + path;
+ }
+ return webRoot + path;
+ }
+
+ private String determineContentType(Path filePath) {
+ String fileName = filePath.getFileName().toString();
+ if (fileName.endsWith(".html")) {
+ return "text/html;charset=utf-8";
+ } else if (fileName.endsWith(".css")) {
+ return "text/css;charset=utf-8";
+ } else if (fileName.endsWith(".js")) {
+ return "application/javascript;charset=utf-8";
+ } else if (fileName.endsWith(".png")) {
+ return "image/png";
+ } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
+ return "image/jpeg";
+ } else {
+ return "application/octet-stream";
+ }
+ }
+
+ private void sendResponse(OutputStream output, Response response) throws IOException {
+ String statusLine = STR."""
+HTTP/1.1 \{response.getStatusCode()} \{response.getStatusMessage()}\r
+""";
+ String headers = STR."""
+Content-Type: \{response.getContentType()}\r
+Content-Length: \{response.getContentLength()}\r
+\r
+""";
+
+ output.write(statusLine.getBytes());
+ output.write(headers.getBytes());
+ output.write(response.getContent());
+ output.flush();
+ }
+}
diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/core/Response.java b/demo1-V2/src/main/java/top/yexuejc/demo/core/Response.java
new file mode 100644
index 0000000..8b2e3f4
--- /dev/null
+++ b/demo1-V2/src/main/java/top/yexuejc/demo/core/Response.java
@@ -0,0 +1,45 @@
+package top.yexuejc.demo.core;
+
+/**
+ * 响应对象类
+ * @author maxiaofeng
+ * @date 2025/6/19 11:37
+ */
+public class Response {
+ private final int statusCode;
+ private final String statusMessage;
+ private final String contentType;
+ private byte[] content;
+
+ public Response(int statusCode, String statusMessage, String contentType, byte[] content) {
+ this.statusCode = statusCode;
+ this.statusMessage = statusMessage;
+ this.contentType = contentType;
+ this.content = content;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public String getStatusMessage() {
+ return statusMessage;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public byte[] getContent() {
+ return content;
+ }
+
+ public int getContentLength() {
+ return content.length;
+ }
+
+ public Response setContent(byte[] content) {
+ this.content = content;
+ return this;
+ }
+}
diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/core/WebServer.java b/demo1-V2/src/main/java/top/yexuejc/demo/core/WebServer.java
new file mode 100644
index 0000000..2b6f709
--- /dev/null
+++ b/demo1-V2/src/main/java/top/yexuejc/demo/core/WebServer.java
@@ -0,0 +1,165 @@
+package top.yexuejc.demo.core;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.util.Properties;
+import java.util.logging.Logger;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+
+import top.yexuejc.demo.AppConfig;
+
+/**
+ * web服务核心
+ *
+ * @author maxiaofeng
+ * @date 2025/6/19 11:13
+ */
+public class WebServer {
+ Logger logger = Logger.getLogger(WebServer.class.getName());
+ /** 端口 */
+ private int httpPort = 80;
+ private int httpsPort = 443;
+ /**
+ * 证书格式:
+ * 1. JKS
+ * 2. PKCS12
+ */
+ private String httpsSslKeyStore = "PKCS12";
+ private String httpsSslKeyStoreFile;
+ private String httpsSslKeyStorePassword;
+ /** 页面资源文件 */
+ private String webRoot = "web";
+ /** 静态资源文件 */
+ private String staticRoot = "static";
+
+ private ServerSocket serverSocket;
+ private SSLServerSocket httpsServerSocket;
+ private boolean isRunning;
+
+ public WebServer() {
+ Properties config = AppConfig.getProperties();
+ String s = config.getProperty("http.port");
+ httpPort = s == null ? 80 : Integer.parseInt(s);
+ s = config.getProperty("https.port");
+ httpsPort = s == null ? 443 : Integer.parseInt(s);
+ s = config.getProperty("https.ssl.type");
+ httpsSslKeyStore = s == null ? httpsSslKeyStore : s;
+ s = config.getProperty("https.ssl.key-store");
+ httpsSslKeyStoreFile = s == null ? httpsSslKeyStoreFile : s;
+ s = config.getProperty("https.ssl.key-store-password");
+ httpsSslKeyStorePassword = s == null ? httpsSslKeyStorePassword : s;
+ }
+
+ public WebServer(int httpPort, int httpsPort, String staticRoot, String webRoot) {
+ this.httpPort = httpPort;
+ this.httpsPort = httpsPort;
+ this.staticRoot = staticRoot;
+ this.webRoot = webRoot;
+ }
+
+ /**
+ * 启动服务
+ */
+ public void start() {
+ new Thread(this::startHttp).start();
+ if (httpsSslKeyStoreFile != null && !httpsSslKeyStoreFile.isEmpty()) {
+ new Thread(this::startHttps).start();
+ }
+ }
+
+ public void startHttp() {
+ try {
+ serverSocket = new ServerSocket(httpPort);
+ isRunning = true;
+ logger.info("启动Web服务 : http://127.0.0.1:" + httpPort);
+
+ // 阻塞式监听服务是否正常
+ while (isRunning) {
+ try {
+ Socket clientSocket = serverSocket.accept();
+ // 新建一个线程处理请求
+ new RequestHandler(clientSocket, staticRoot, webRoot).run();
+ } catch (IOException e) {
+ if (isRunning) {
+ logger.severe("创建连接异常。");
+ e.printStackTrace();
+ }
+ }
+ }
+ } catch (Throwable r) {
+ logger.severe("启动Web服务异常。");
+ r.printStackTrace();
+ } finally {
+ stop();
+ }
+ }
+
+ public void startHttps() {
+ try {
+ // 1. 加载 PKCS12 密钥库
+ KeyStore keyStore = KeyStore.getInstance(httpsSslKeyStore);
+ InputStream inputStream;
+ if (httpsSslKeyStoreFile.startsWith("classpath:")) {
+ inputStream = this.getClass().getClassLoader().getResourceAsStream(httpsSslKeyStoreFile.substring(10));
+ } else {
+ inputStream = new FileInputStream(httpsSslKeyStoreFile);
+ }
+ keyStore.load(inputStream, httpsSslKeyStorePassword.toCharArray());
+ // 2. 初始化 KeyManagerFactory
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, httpsSslKeyStorePassword.toCharArray());
+
+ // 3. 初始化 SSLContext(TLS 1.2 或 1.3)
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.3"); // 或 "TLS"
+ sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
+
+ // 4. 创建 SSLServerSocket
+ SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
+ SSLServerSocket httpsServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(httpsPort);
+ httpsServerSocket.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});
+
+ logger.info("启动Web服务 : https://127.0.0.1:" + httpsPort);
+ isRunning = true;
+ while (isRunning) {
+ // 新建一个线程处理请求
+ new RequestHandler(httpsServerSocket.accept(), staticRoot, webRoot).run();
+ }
+ } catch (Exception e) {
+ if (isRunning) {
+ logger.severe("创建连接异常。");
+ e.printStackTrace();
+ }
+ } finally {
+ stop();
+ }
+ }
+
+ /**
+ * 停止服务
+ */
+ public void stop() {
+ isRunning = false;
+ try {
+ if (serverSocket != null) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ logger.info("停止Web服务异常: " + e.getMessage());
+ }
+ try {
+ if (httpsServerSocket != null) {
+ httpsServerSocket.close();
+ }
+ } catch (IOException e) {
+ logger.info("停止Web服务异常: " + e.getMessage());
+ }
+ System.out.println("Web服务停止.");
+ }
+}
diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java b/demo1-V2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
new file mode 100644
index 0000000..43f975d
--- /dev/null
+++ b/demo1-V2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
@@ -0,0 +1,22 @@
+package top.yexuejc.demo.web;
+
+import top.yexuejc.demo.annotation.GetMapping;
+import top.yexuejc.demo.annotation.RestController;
+
+/**
+ * @author maxiaofeng
+ * @date 2025/7/11 14:33
+ */
+@RestController
+public class IndexCtrl {
+
+ @GetMapping("/test")
+ public String index() {
+ return "hello world";
+ }
+
+ @GetMapping("/test2")
+ public String index(String params) {
+ return STR."你好 : \{params}";
+ }
+}
diff --git a/demo1-V2/src/main/resources/Intasect.crt b/demo1-V2/src/main/resources/Intasect.crt
new file mode 100644
index 0000000..2883c28
--- /dev/null
+++ b/demo1-V2/src/main/resources/Intasect.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDojCCAoqgAwIBAgIEEBU9hTANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJD
+TjERMA8GA1UEAwwISW50YXNlY3QxEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcM
+B0NoZW5nRHUxETAPBgNVBAoMCEludGFzZWN0MREwDwYDVQQLDAhJbnRhc2VjdDAe
+Fw0yNTA3MTEwMTM5MDhaFw0yNzA3MTEwMTM5MDhaMGoxCzAJBgNVBAYTAkNOMREw
+DwYDVQQDDAhJbnRhc2VjdDEQMA4GA1UECAwHU2lDaHVhbjEQMA4GA1UEBwwHQ2hl
+bmdEdTERMA8GA1UECgwISW50YXNlY3QxETAPBgNVBAsMCEludGFzZWN0MIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Ce46Pj6hIeCNv/g+c4WaKJ5jURo
+fk0mLXhTaLa/QdsK9OVGUYWPeYlQtljZZ7y5o3DNHnYJ7bSeAQUE/NY786Owz7jN
+JER9ppqTg/2lpklgKXKEC+4UsSWuPCmBTQoXaU5SGX7l3esrR1n+vIUocOOyHlWv
+KzHCwiTJxEYbShC+kdgvB5ozWSTp5qF8j5sACSKNmdckSaIfiW14b3QUsNBt7uMv
+cS4VsrzxPBEcD2qWCkSrw6Q4Tvy7yhU21taisUO9diA6MfMZX6988U4lmW97R31m
+2iQwxVr1m6tQnkvJTHwR76xqKxW8lDKVBt42e42KEcFrTPNseDGWfh/WFwIDAQAB
+o1AwTjAdBgNVHQ4EFgQU526ItlTmddeCAhwL8Riz6Cv3Ze4wHwYDVR0jBBgwFoAU
+526ItlTmddeCAhwL8Riz6Cv3Ze4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsF
+AAOCAQEAaYr52M5ZNUB0MQFHqGQ5FvIDaBZJY9ntYC+iasW84EQcPzZy+XCeMcqd
+sv9RA1/UrNnmPM62AgM0TNUNHg/edEeVBf/IrqGdPNPEYKiB4lELWuzyVvjEkyEK
+0azf+GzOYEYsbqU98Jh3tTb2d5fqmN59otxqe7b9aA5BogL/n9AmAOw+1DgJ4l4g
+Qj5S+tA5jR4DpQthvrrzy0/xc37gIvzS7em7y6C2K6lDE0CCs9OuBZoAniaa+6/I
+86Xi+XtJFrZ1bFfyOwDp7TSuNALeEsJKggfASMbOG0NUGcgl5q/kMcp5v8e6RNUw
+oLBEvTic573dAWwtuCjOTW0QI2vG7w==
+-----END CERTIFICATE-----
diff --git a/demo1-V2/src/main/resources/application.properties b/demo1-V2/src/main/resources/application.properties
new file mode 100644
index 0000000..972d03c
--- /dev/null
+++ b/demo1-V2/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+http.port=80
+https.port=443
+https.ssl.type=PKCS12
+https.ssl.key-store=classpath:ssl/keystore_3rd.p12
+https.ssl.key-store-password=123456
\ No newline at end of file
diff --git a/demo1-V2/src/main/resources/ssl/keystore_3rd.p12 b/demo1-V2/src/main/resources/ssl/keystore_3rd.p12
new file mode 100644
index 0000000..d37993c
Binary files /dev/null and b/demo1-V2/src/main/resources/ssl/keystore_3rd.p12 differ
diff --git a/demo1-V2/src/main/resources/ssl/ssl.crt b/demo1-V2/src/main/resources/ssl/ssl.crt
new file mode 100644
index 0000000..2883c28
--- /dev/null
+++ b/demo1-V2/src/main/resources/ssl/ssl.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDojCCAoqgAwIBAgIEEBU9hTANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJD
+TjERMA8GA1UEAwwISW50YXNlY3QxEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcM
+B0NoZW5nRHUxETAPBgNVBAoMCEludGFzZWN0MREwDwYDVQQLDAhJbnRhc2VjdDAe
+Fw0yNTA3MTEwMTM5MDhaFw0yNzA3MTEwMTM5MDhaMGoxCzAJBgNVBAYTAkNOMREw
+DwYDVQQDDAhJbnRhc2VjdDEQMA4GA1UECAwHU2lDaHVhbjEQMA4GA1UEBwwHQ2hl
+bmdEdTERMA8GA1UECgwISW50YXNlY3QxETAPBgNVBAsMCEludGFzZWN0MIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Ce46Pj6hIeCNv/g+c4WaKJ5jURo
+fk0mLXhTaLa/QdsK9OVGUYWPeYlQtljZZ7y5o3DNHnYJ7bSeAQUE/NY786Owz7jN
+JER9ppqTg/2lpklgKXKEC+4UsSWuPCmBTQoXaU5SGX7l3esrR1n+vIUocOOyHlWv
+KzHCwiTJxEYbShC+kdgvB5ozWSTp5qF8j5sACSKNmdckSaIfiW14b3QUsNBt7uMv
+cS4VsrzxPBEcD2qWCkSrw6Q4Tvy7yhU21taisUO9diA6MfMZX6988U4lmW97R31m
+2iQwxVr1m6tQnkvJTHwR76xqKxW8lDKVBt42e42KEcFrTPNseDGWfh/WFwIDAQAB
+o1AwTjAdBgNVHQ4EFgQU526ItlTmddeCAhwL8Riz6Cv3Ze4wHwYDVR0jBBgwFoAU
+526ItlTmddeCAhwL8Riz6Cv3Ze4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsF
+AAOCAQEAaYr52M5ZNUB0MQFHqGQ5FvIDaBZJY9ntYC+iasW84EQcPzZy+XCeMcqd
+sv9RA1/UrNnmPM62AgM0TNUNHg/edEeVBf/IrqGdPNPEYKiB4lELWuzyVvjEkyEK
+0azf+GzOYEYsbqU98Jh3tTb2d5fqmN59otxqe7b9aA5BogL/n9AmAOw+1DgJ4l4g
+Qj5S+tA5jR4DpQthvrrzy0/xc37gIvzS7em7y6C2K6lDE0CCs9OuBZoAniaa+6/I
+86Xi+XtJFrZ1bFfyOwDp7TSuNALeEsJKggfASMbOG0NUGcgl5q/kMcp5v8e6RNUw
+oLBEvTic573dAWwtuCjOTW0QI2vG7w==
+-----END CERTIFICATE-----
diff --git a/demo1-V2/src/main/resources/ssl/ssl.csr b/demo1-V2/src/main/resources/ssl/ssl.csr
new file mode 100644
index 0000000..616f9ef
--- /dev/null
+++ b/demo1-V2/src/main/resources/ssl/ssl.csr
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICrzCCAZcCAQAwajELMAkGA1UEBhMCQ04xETAPBgNVBAMMCEludGFzZWN0MRAw
+DgYDVQQIDAdTaUNodWFuMRAwDgYDVQQHDAdDaGVuZ0R1MREwDwYDVQQKDAhJbnRh
+c2VjdDERMA8GA1UECwwISW50YXNlY3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDcJ7jo+PqEh4I2/+D5zhZoonmNRGh+TSYteFNotr9B2wr05UZRhY95
+iVC2WNlnvLmjcM0edgnttJ4BBQT81jvzo7DPuM0kRH2mmpOD/aWmSWApcoQL7hSx
+Ja48KYFNChdpTlIZfuXd6ytHWf68hShw47IeVa8rMcLCJMnERhtKEL6R2C8HmjNZ
+JOnmoXyPmwAJIo2Z1yRJoh+JbXhvdBSw0G3u4y9xLhWyvPE8ERwPapYKRKvDpDhO
+/LvKFTbW1qKxQ712IDox8xlfr3zxTiWZb3tHfWbaJDDFWvWbq1CeS8lMfBHvrGor
+FbyUMpUG3jZ7jYoRwWtM82x4MZZ+H9YXAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
+AQEAhHafpZDX/Ivl+27k+/4/npdAW8VSbUS++txJvB2u+r4WdwLZei5LSQDKzdfY
+BAfcyhHxNTUV90OB68jqYoi2RiivtaNzQVGaFJ/XrkfZqQq7UNramHbdisV/m5hg
+oFYoei52SDIXLv1BzZhBMFr2TtLTZMt5xXfaQjInInjcZJs8ngtwQL/DyPlrPNzS
+TOZrl4GHMIIyUejqG2TibKiugkFoZ2DwHyZggIgeFqfU7PNc2KQJ/BtYs4N0IpvF
+k0Yy+2Mi6lWYm9Nn7pHgo4/+8ZWrD/z5Ffsmgsrwv6S4iB/z3oVqJdEYnryHqy78
+WytygTsyK1SBLBDEdHgXbazfQw==
+-----END CERTIFICATE REQUEST-----
diff --git a/demo1-V2/src/main/resources/ssl/ssl.key b/demo1-V2/src/main/resources/ssl/ssl.key
new file mode 100644
index 0000000..fd7e2b6
--- /dev/null
+++ b/demo1-V2/src/main/resources/ssl/ssl.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcJ7jo+PqEh4I2
+/+D5zhZoonmNRGh+TSYteFNotr9B2wr05UZRhY95iVC2WNlnvLmjcM0edgnttJ4B
+BQT81jvzo7DPuM0kRH2mmpOD/aWmSWApcoQL7hSxJa48KYFNChdpTlIZfuXd6ytH
+Wf68hShw47IeVa8rMcLCJMnERhtKEL6R2C8HmjNZJOnmoXyPmwAJIo2Z1yRJoh+J
+bXhvdBSw0G3u4y9xLhWyvPE8ERwPapYKRKvDpDhO/LvKFTbW1qKxQ712IDox8xlf
+r3zxTiWZb3tHfWbaJDDFWvWbq1CeS8lMfBHvrGorFbyUMpUG3jZ7jYoRwWtM82x4
+MZZ+H9YXAgMBAAECggEATkD0UiNF8Nu15lTXpBOkFXdDG3qoZdSIcHsnsr3ah88T
+Ou9QKmP+FqY/gUFdrakAl17eGii86LhdvWEKX9DKqJSToZI/oNeTjie9rZn4Sn4k
+ZzckRpVO15TcNNhP9JFUtwK23gckL9iKnqcXi+0M7euRgYTVadYbMyUebty4kH9w
+YhGVDdoyBqsi4MpVU1EiszRfMrvtWYywpjrCwdcwsliGUXIOdz3AWabCpmn2N0Lc
+KHgdqPuDG57wAS7Wr/4UdfghIf0ywAhpL0A2F514dc0cDWsC5C33SSO77S3Gx4Wl
+Xz9lqkKtWh+30DNKShkb+YG030Y/p0Msxn9PlMzZMQKBgQDxe78cNqrWCMgj35yD
+9ZQBUgTDqQZGTKHVlAhdnLS/5yjy0AIQ+d1pT/OAxqPedw7TPNbkjJbhXHweG0Eh
+K/bQe+KU++N++c7oQ2Rv0DOMJ2Trd5U+TaVcrkwIy2D3Twg6kIk+Yru8P5QXfvE7
+MBxY4rJOL5h51nTsYwwKRau/bwKBgQDpY8AAnOAGR1lnnhm6nSStaU9ec45vTtbg
+2yXFxN5FjObk2cudEpgZOKboMrxec4VXLcgWnOw3Yk8UWJWwElIjxZwo1r45J/79
+c+LP8Rh9FCE+5An1aaNrGGu4Mms1N4Wd0utJ3fkL7+mVhddlXNMYqKbamABnX9jB
+q+Nx9FX/2QKBgCggUe9UPir2ppsfaxiaVA+sG1KP4ZUI4tNkl8dGZNqGhM1kNxOv
+EVWQjXvWhiBPVE1RjLvJiMDF53HxQW9LqOWX0FzFRlYxGGqL2EKkLAyb9y8RXeFO
+ca3m4IeNk/1ESq/AmK2fJmbvgaIt29Pj+LHkaZCIZCPKuP8Wrkd+sD1NAoGASZwa
+bJcN2S0bt6CXwNHbRY5XaBTOMbEN+LFlwnCLIiiEkl1W6N16d0n06ntGCgwpXAum
+detcXUN2aZZe7793hKzIyeCg8mn49HteZ/NEo/57Vdiag3qj/h0frGLKiWhPji19
+5DhMWkV6yJwECYYzVi2rInqadgA23y6Vd9V2YlECgYEA3v0BT6TpfUM5BnEIX9nA
+je5buofurZVpXlSb20QOhWXOD0XHhC7ZsP7xDQf5vuMTdTpFhq/hfCwUBOyI7mNR
+iwRTVyGCWqSafcHSbenXEfJs4GtU5Cw9KZTWp13M5PLMwhevhwpUR+yZw7EDY7oV
+PAgNmnu4pkAhHUSUxhlXcnw=
+-----END PRIVATE KEY-----
diff --git a/demo1-V2/src/main/resources/static/css/index.css b/demo1-V2/src/main/resources/static/css/index.css
new file mode 100644
index 0000000..97f73ed
--- /dev/null
+++ b/demo1-V2/src/main/resources/static/css/index.css
@@ -0,0 +1,22 @@
+body {
+ font-family: Arial, sans-serif;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+ color: #333;
+}
+
+h1 {
+ color: #0671dc;
+ /* 让文字左右滚动,并且改变颜色*/
+ text-shadow: 0 0 5px #333;
+ animation: scroll-text 5s linear infinite;
+ animation-fill-mode: forwards;
+ animation-delay: 2s;
+ animation-direction: alternate;
+ animation-iteration-count: infinite;
+ animation-timing-function: ease-in-out;
+ animation-play-state: running;
+ animation-name: scroll-text;
+ animation-duration: 5s;
+}
diff --git a/demo1-V2/src/main/resources/web/index.html b/demo1-V2/src/main/resources/web/index.html
new file mode 100644
index 0000000..9d89ff0
--- /dev/null
+++ b/demo1-V2/src/main/resources/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Simple Web Server
+
+
+
+Welcome to Simple Web Server
+This page is served by our custom Java web server!
+你的请求参数是:${body}
+
+
diff --git a/demo1/src/main/resources/ssl/ssl.crt b/demo1/src/main/resources/ssl/ssl.crt
new file mode 100644
index 0000000..2883c28
--- /dev/null
+++ b/demo1/src/main/resources/ssl/ssl.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDojCCAoqgAwIBAgIEEBU9hTANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJD
+TjERMA8GA1UEAwwISW50YXNlY3QxEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcM
+B0NoZW5nRHUxETAPBgNVBAoMCEludGFzZWN0MREwDwYDVQQLDAhJbnRhc2VjdDAe
+Fw0yNTA3MTEwMTM5MDhaFw0yNzA3MTEwMTM5MDhaMGoxCzAJBgNVBAYTAkNOMREw
+DwYDVQQDDAhJbnRhc2VjdDEQMA4GA1UECAwHU2lDaHVhbjEQMA4GA1UEBwwHQ2hl
+bmdEdTERMA8GA1UECgwISW50YXNlY3QxETAPBgNVBAsMCEludGFzZWN0MIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Ce46Pj6hIeCNv/g+c4WaKJ5jURo
+fk0mLXhTaLa/QdsK9OVGUYWPeYlQtljZZ7y5o3DNHnYJ7bSeAQUE/NY786Owz7jN
+JER9ppqTg/2lpklgKXKEC+4UsSWuPCmBTQoXaU5SGX7l3esrR1n+vIUocOOyHlWv
+KzHCwiTJxEYbShC+kdgvB5ozWSTp5qF8j5sACSKNmdckSaIfiW14b3QUsNBt7uMv
+cS4VsrzxPBEcD2qWCkSrw6Q4Tvy7yhU21taisUO9diA6MfMZX6988U4lmW97R31m
+2iQwxVr1m6tQnkvJTHwR76xqKxW8lDKVBt42e42KEcFrTPNseDGWfh/WFwIDAQAB
+o1AwTjAdBgNVHQ4EFgQU526ItlTmddeCAhwL8Riz6Cv3Ze4wHwYDVR0jBBgwFoAU
+526ItlTmddeCAhwL8Riz6Cv3Ze4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsF
+AAOCAQEAaYr52M5ZNUB0MQFHqGQ5FvIDaBZJY9ntYC+iasW84EQcPzZy+XCeMcqd
+sv9RA1/UrNnmPM62AgM0TNUNHg/edEeVBf/IrqGdPNPEYKiB4lELWuzyVvjEkyEK
+0azf+GzOYEYsbqU98Jh3tTb2d5fqmN59otxqe7b9aA5BogL/n9AmAOw+1DgJ4l4g
+Qj5S+tA5jR4DpQthvrrzy0/xc37gIvzS7em7y6C2K6lDE0CCs9OuBZoAniaa+6/I
+86Xi+XtJFrZ1bFfyOwDp7TSuNALeEsJKggfASMbOG0NUGcgl5q/kMcp5v8e6RNUw
+oLBEvTic573dAWwtuCjOTW0QI2vG7w==
+-----END CERTIFICATE-----
diff --git a/demo1/src/main/resources/ssl/ssl.csr b/demo1/src/main/resources/ssl/ssl.csr
new file mode 100644
index 0000000..616f9ef
--- /dev/null
+++ b/demo1/src/main/resources/ssl/ssl.csr
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICrzCCAZcCAQAwajELMAkGA1UEBhMCQ04xETAPBgNVBAMMCEludGFzZWN0MRAw
+DgYDVQQIDAdTaUNodWFuMRAwDgYDVQQHDAdDaGVuZ0R1MREwDwYDVQQKDAhJbnRh
+c2VjdDERMA8GA1UECwwISW50YXNlY3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDcJ7jo+PqEh4I2/+D5zhZoonmNRGh+TSYteFNotr9B2wr05UZRhY95
+iVC2WNlnvLmjcM0edgnttJ4BBQT81jvzo7DPuM0kRH2mmpOD/aWmSWApcoQL7hSx
+Ja48KYFNChdpTlIZfuXd6ytHWf68hShw47IeVa8rMcLCJMnERhtKEL6R2C8HmjNZ
+JOnmoXyPmwAJIo2Z1yRJoh+JbXhvdBSw0G3u4y9xLhWyvPE8ERwPapYKRKvDpDhO
+/LvKFTbW1qKxQ712IDox8xlfr3zxTiWZb3tHfWbaJDDFWvWbq1CeS8lMfBHvrGor
+FbyUMpUG3jZ7jYoRwWtM82x4MZZ+H9YXAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
+AQEAhHafpZDX/Ivl+27k+/4/npdAW8VSbUS++txJvB2u+r4WdwLZei5LSQDKzdfY
+BAfcyhHxNTUV90OB68jqYoi2RiivtaNzQVGaFJ/XrkfZqQq7UNramHbdisV/m5hg
+oFYoei52SDIXLv1BzZhBMFr2TtLTZMt5xXfaQjInInjcZJs8ngtwQL/DyPlrPNzS
+TOZrl4GHMIIyUejqG2TibKiugkFoZ2DwHyZggIgeFqfU7PNc2KQJ/BtYs4N0IpvF
+k0Yy+2Mi6lWYm9Nn7pHgo4/+8ZWrD/z5Ffsmgsrwv6S4iB/z3oVqJdEYnryHqy78
+WytygTsyK1SBLBDEdHgXbazfQw==
+-----END CERTIFICATE REQUEST-----
diff --git a/demo1/src/main/resources/ssl/ssl.key b/demo1/src/main/resources/ssl/ssl.key
new file mode 100644
index 0000000..fd7e2b6
--- /dev/null
+++ b/demo1/src/main/resources/ssl/ssl.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcJ7jo+PqEh4I2
+/+D5zhZoonmNRGh+TSYteFNotr9B2wr05UZRhY95iVC2WNlnvLmjcM0edgnttJ4B
+BQT81jvzo7DPuM0kRH2mmpOD/aWmSWApcoQL7hSxJa48KYFNChdpTlIZfuXd6ytH
+Wf68hShw47IeVa8rMcLCJMnERhtKEL6R2C8HmjNZJOnmoXyPmwAJIo2Z1yRJoh+J
+bXhvdBSw0G3u4y9xLhWyvPE8ERwPapYKRKvDpDhO/LvKFTbW1qKxQ712IDox8xlf
+r3zxTiWZb3tHfWbaJDDFWvWbq1CeS8lMfBHvrGorFbyUMpUG3jZ7jYoRwWtM82x4
+MZZ+H9YXAgMBAAECggEATkD0UiNF8Nu15lTXpBOkFXdDG3qoZdSIcHsnsr3ah88T
+Ou9QKmP+FqY/gUFdrakAl17eGii86LhdvWEKX9DKqJSToZI/oNeTjie9rZn4Sn4k
+ZzckRpVO15TcNNhP9JFUtwK23gckL9iKnqcXi+0M7euRgYTVadYbMyUebty4kH9w
+YhGVDdoyBqsi4MpVU1EiszRfMrvtWYywpjrCwdcwsliGUXIOdz3AWabCpmn2N0Lc
+KHgdqPuDG57wAS7Wr/4UdfghIf0ywAhpL0A2F514dc0cDWsC5C33SSO77S3Gx4Wl
+Xz9lqkKtWh+30DNKShkb+YG030Y/p0Msxn9PlMzZMQKBgQDxe78cNqrWCMgj35yD
+9ZQBUgTDqQZGTKHVlAhdnLS/5yjy0AIQ+d1pT/OAxqPedw7TPNbkjJbhXHweG0Eh
+K/bQe+KU++N++c7oQ2Rv0DOMJ2Trd5U+TaVcrkwIy2D3Twg6kIk+Yru8P5QXfvE7
+MBxY4rJOL5h51nTsYwwKRau/bwKBgQDpY8AAnOAGR1lnnhm6nSStaU9ec45vTtbg
+2yXFxN5FjObk2cudEpgZOKboMrxec4VXLcgWnOw3Yk8UWJWwElIjxZwo1r45J/79
+c+LP8Rh9FCE+5An1aaNrGGu4Mms1N4Wd0utJ3fkL7+mVhddlXNMYqKbamABnX9jB
+q+Nx9FX/2QKBgCggUe9UPir2ppsfaxiaVA+sG1KP4ZUI4tNkl8dGZNqGhM1kNxOv
+EVWQjXvWhiBPVE1RjLvJiMDF53HxQW9LqOWX0FzFRlYxGGqL2EKkLAyb9y8RXeFO
+ca3m4IeNk/1ESq/AmK2fJmbvgaIt29Pj+LHkaZCIZCPKuP8Wrkd+sD1NAoGASZwa
+bJcN2S0bt6CXwNHbRY5XaBTOMbEN+LFlwnCLIiiEkl1W6N16d0n06ntGCgwpXAum
+detcXUN2aZZe7793hKzIyeCg8mn49HteZ/NEo/57Vdiag3qj/h0frGLKiWhPji19
+5DhMWkV6yJwECYYzVi2rInqadgA23y6Vd9V2YlECgYEA3v0BT6TpfUM5BnEIX9nA
+je5buofurZVpXlSb20QOhWXOD0XHhC7ZsP7xDQf5vuMTdTpFhq/hfCwUBOyI7mNR
+iwRTVyGCWqSafcHSbenXEfJs4GtU5Cw9KZTWp13M5PLMwhevhwpUR+yZw7EDY7oV
+PAgNmnu4pkAhHUSUxhlXcnw=
+-----END PRIVATE KEY-----
diff --git a/demo2/pom.xml b/demo2/pom.xml
new file mode 100644
index 0000000..e1312d4
--- /dev/null
+++ b/demo2/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.3
+
+
+ top.yexuejc
+ demo2
+ 0.0.1-SNAPSHOT
+ demo2
+ demo2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+ 21
+ 21
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ nz.net.ultraq.thymeleaf
+ thymeleaf-layout-dialect
+
+
+
+ org.thymeleaf.extras
+ thymeleaf-extras-springsecurity6
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ true
+ true
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ true
+
+
+
+
+
+
diff --git a/demo2/src/main/java/top/yexuejc/demo/Demo2Application.java b/demo2/src/main/java/top/yexuejc/demo/Demo2Application.java
new file mode 100644
index 0000000..648935f
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/Demo2Application.java
@@ -0,0 +1,13 @@
+package top.yexuejc.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Demo2Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Demo2Application.class, args);
+ }
+
+}
diff --git a/demo2/src/main/java/top/yexuejc/demo/config/GlobalExceptionHandler.java b/demo2/src/main/java/top/yexuejc/demo/config/GlobalExceptionHandler.java
new file mode 100644
index 0000000..ad9e8bc
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/config/GlobalExceptionHandler.java
@@ -0,0 +1,43 @@
+package top.yexuejc.demo.config;
+
+import java.util.List;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * @author maxiaofeng
+ * @date 2025/7/3 18:27
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public Object handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
+
+ log.error("参数校验异常: ", ex);
+
+ BindingResult result = ex.getBindingResult();
+ List errorMessages = result.getAllErrors().stream().map(ObjectError::getDefaultMessage).toList();
+
+ if (request.getContentType() != null && request.getContentType().contains("application/json")) {
+ // 返回 JSON 错误信息
+ return new ResponseEntity<>(errorMessages, HttpStatus.BAD_REQUEST);
+ } else {
+ // 返回错误页面或重定向到原页面
+ ModelAndView modelAndView = new ModelAndView("login"); // 示例页面名
+ modelAndView.addObject("error", errorMessages);
+ return modelAndView;
+ }
+ }
+}
diff --git a/demo2/src/main/java/top/yexuejc/demo/config/SecurityConfig.java b/demo2/src/main/java/top/yexuejc/demo/config/SecurityConfig.java
new file mode 100644
index 0000000..5205c90
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/config/SecurityConfig.java
@@ -0,0 +1,39 @@
+package top.yexuejc.demo.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/", "/home", "/login/**", "/register", "/css/**", "/js/**", "/images/**").permitAll()
+ .anyRequest().authenticated()
+ )
+ .formLogin(form -> form
+ .loginPage("/login")
+ .defaultSuccessUrl("/home")
+ .failureUrl("/login?error=true")
+ .permitAll()
+ )
+ .logout(logout -> logout
+ .logoutSuccessUrl("/login?logout=true")
+ .permitAll()
+ );
+ return http.build();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/demo2/src/main/java/top/yexuejc/demo/config/ThymeleafConfig.java b/demo2/src/main/java/top/yexuejc/demo/config/ThymeleafConfig.java
new file mode 100644
index 0000000..2f6eea0
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/config/ThymeleafConfig.java
@@ -0,0 +1,22 @@
+package top.yexuejc.demo.config;
+
+import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.thymeleaf.spring6.SpringTemplateEngine;
+import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
+
+/**
+ * @author maxiaofeng
+ * @date 2025/7/4 18:28
+ */
+@Configuration
+public class ThymeleafConfig {
+ @Bean
+ public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
+ SpringTemplateEngine engine = new SpringTemplateEngine();
+ engine.addDialect(new LayoutDialect());
+ engine.setTemplateResolver(templateResolver);
+ return engine;
+ }
+}
diff --git a/demo2/src/main/java/top/yexuejc/demo/model/LoginVO.java b/demo2/src/main/java/top/yexuejc/demo/model/LoginVO.java
new file mode 100644
index 0000000..48c859d
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/model/LoginVO.java
@@ -0,0 +1,22 @@
+package top.yexuejc.demo.model;
+
+import java.io.Serializable;
+
+import jakarta.validation.constraints.NotBlank;
+
+/**
+ * @author maxiaofeng
+ * @date 2025/7/3 16:39
+ */
+public record LoginVO(@NotBlank(message = "账号不能为空") String username,
+ @NotBlank(message = "密码不能为空") String password) implements Serializable {
+
+}
+
+//@Data
+//public class LoginVO implements Serializable {
+// @NotBlank
+// private String username;
+// private String password;
+//}
+
diff --git a/demo2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java b/demo2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
new file mode 100644
index 0000000..d398a11
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
@@ -0,0 +1,18 @@
+package top.yexuejc.demo.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * @author maxiaofeng
+ * @date 2025/7/3 18:43
+ */
+@Controller
+public class IndexCtrl {
+
+ @RequestMapping("/")
+ public ModelAndView index() {
+ return new ModelAndView("index");
+ }
+}
diff --git a/demo2/src/main/java/top/yexuejc/demo/web/LoginCtrl.java b/demo2/src/main/java/top/yexuejc/demo/web/LoginCtrl.java
new file mode 100644
index 0000000..9cf83ec
--- /dev/null
+++ b/demo2/src/main/java/top/yexuejc/demo/web/LoginCtrl.java
@@ -0,0 +1,56 @@
+package top.yexuejc.demo.web;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import jakarta.validation.Valid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+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.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.servlet.ModelAndView;
+import top.yexuejc.demo.model.LoginVO;
+
+/**
+ * @author maxiaofeng
+ * @date 2025/7/3 16:37
+ */
+@Controller
+@RequestMapping("/login")
+public class LoginCtrl {
+
+ private static final Logger log = LoggerFactory.getLogger(LoginCtrl.class);
+
+ @GetMapping("")
+ public ModelAndView page(ModelAndView mv, @RequestParam Map params) {
+ mv.addAllObjects(params);
+ mv.setViewName("login");
+ return mv;
+ }
+
+ @GetMapping("/get")
+ public String loginGet(@Valid LoginVO loginVO) throws JsonProcessingException {
+ log.info(new JsonMapper().writeValueAsString(loginVO));
+ return "redirect:/";
+ }
+
+ @PostMapping(value = "/postJson", consumes = MediaType.APPLICATION_JSON_VALUE)
+ public String loginPostJson(@RequestBody @Validated LoginVO loginVO) throws JsonProcessingException {
+ log.info(new JsonMapper().writeValueAsString(loginVO));
+ return "redirect:/";
+ }
+
+ @PostMapping(value = "/postFrom", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+ public String loginPostFrom(@Validated LoginVO loginVO) throws JsonProcessingException {
+ log.info(new JsonMapper().writeValueAsString(loginVO));
+ return "redirect:/";
+ }
+}
diff --git a/demo2/src/main/resources/application.properties b/demo2/src/main/resources/application.properties
new file mode 100644
index 0000000..a9dcb8e
--- /dev/null
+++ b/demo2/src/main/resources/application.properties
@@ -0,0 +1,9 @@
+spring.application.name=demo2
+
+
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+spring.thymeleaf.mode=HTML
+spring.thymeleaf.cache=false
+spring.thymeleaf.encoding=UTF-8
+spring.thymeleaf.servlet.content-type=text/html
\ No newline at end of file
diff --git a/demo2/src/main/resources/static/css/common/main.css b/demo2/src/main/resources/static/css/common/main.css
new file mode 100644
index 0000000..877e0cd
--- /dev/null
+++ b/demo2/src/main/resources/static/css/common/main.css
@@ -0,0 +1,127 @@
+/**
+ *
+ * @author maxiaofeng
+ * @date 2025/7/4 17:33
+ */
+/* 移动设备样式 */
+@media screen and (max-width: 760px) {
+ .ui-menu {
+ display: none;
+ }
+
+ .ui-main-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .ui-header {
+ height: 50px;
+ background-color: #14ccff50;
+ }
+
+ .ui-content {
+ flex: 1;
+ background: #d2fe8650;
+ }
+}
+
+/* 桌面设备样式 */
+@media screen and (min-width: 760px) {
+ .ui-menu {
+ display: block;
+ width: 200px;
+ height: 100vh;
+ background-color: #834f4f50;
+ position: fixed; /* 固定位置 */
+ left: 0;
+ top: 0;
+ transition: width 0.3s ease-in-out;
+ overflow: hidden;
+ }
+
+ .ui-main-container {
+ margin-left: 200px; /* 给侧边栏留出空间 */
+ display: flex;
+ flex-direction: column;
+ height: 100vh; /* 占满整个视口高度 */
+ transition: width 0.3s ease-in-out;
+ }
+
+ .ui-content {
+ flex: 1;
+ background: #d2fe8650;
+ padding: 10px;
+ }
+
+ .ui-header {
+ height: 50px;
+ background-color: #14ccff50;
+ }
+
+ .h-container {
+ display: flex;
+ flex-direction: row; /* 横向排列 */
+ align-items: center; /* 垂直居中对齐 */
+ justify-content: space-between; /* 左右撑开 */
+ width: 100%; /* 确保占满宽度 */
+ height: 50px;
+ }
+
+ .h-icon-menu {
+ height: 20px;
+ margin: 0 10px;
+ }
+ .h-icon-menu-hide {
+ display: block;
+ }
+
+ .h-icon-menu-show {
+ display: none;
+ }
+
+ .h-sys-name {
+ font-size: 20px;
+ }
+
+ .h-right-container {
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+ height: 100%;
+ }
+
+ .h-notice {
+ display: flex;
+ align-items: center;
+ }
+
+ .h-user-info {
+ display: flex;
+ align-items: center;
+ margin: 0 10px;
+ height: 100%;
+ }
+
+ .h-user-avatar {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ }
+
+ .h-user-avatar img {
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ }
+
+ .h-user-name {
+ margin: 0 10px;
+ }
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/demo2/src/main/resources/static/css/login.css b/demo2/src/main/resources/static/css/login.css
new file mode 100644
index 0000000..442bd4f
--- /dev/null
+++ b/demo2/src/main/resources/static/css/login.css
@@ -0,0 +1,38 @@
+body {
+ background-color: #f8f9fa;
+}
+
+.card {
+ border-radius: 10px;
+ border: none;
+}
+
+.card-title {
+ color: #495057;
+ font-weight: 600;
+}
+
+.btn-primary {
+ background-color: #0d6efd;
+ border: none;
+ padding: 10px;
+}
+
+.btn-primary:hover {
+ background-color: #0b5ed7;
+}
+
+.form-control {
+ padding: 12px;
+ border-radius: 5px;
+}
+
+.form-control:focus {
+ border-color: #86b7fe;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.alert {
+ border-radius: 5px;
+ padding: 10px 15px;
+}
diff --git a/demo2/src/main/resources/templates/common/header.html b/demo2/src/main/resources/templates/common/header.html
new file mode 100644
index 0000000..37d7c68
--- /dev/null
+++ b/demo2/src/main/resources/templates/common/header.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
超级管理系统
+
+
+
+
+
+
maxiaofeng
+
+

+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo2/src/main/resources/templates/common/menu.html b/demo2/src/main/resources/templates/common/menu.html
new file mode 100644
index 0000000..566549b
--- /dev/null
+++ b/demo2/src/main/resources/templates/common/menu.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Title
+
+
+
+
+
\ No newline at end of file
diff --git a/demo2/src/main/resources/templates/index.html b/demo2/src/main/resources/templates/index.html
new file mode 100644
index 0000000..7f1056a
--- /dev/null
+++ b/demo2/src/main/resources/templates/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+ 首页
+
+
+
+
+
+
+
+
+
+
diff --git a/demo2/src/main/resources/templates/login.html b/demo2/src/main/resources/templates/login.html
new file mode 100644
index 0000000..abbaff8
--- /dev/null
+++ b/demo2/src/main/resources/templates/login.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ 登录 - 我的应用
+
+
+
+
+
+
+
+
+
+
+
用户登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo2/src/test/java/top/yexuejc/demo/Demo2ApplicationTests.java b/demo2/src/test/java/top/yexuejc/demo/Demo2ApplicationTests.java
new file mode 100644
index 0000000..2fa5f5f
--- /dev/null
+++ b/demo2/src/test/java/top/yexuejc/demo/Demo2ApplicationTests.java
@@ -0,0 +1,13 @@
+package top.yexuejc.demo;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class Demo2ApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}