diff --git a/demo1-V2/README.md b/demo1-V2/README.md new file mode 100644 index 0000000..cc0bc59 --- /dev/null +++ b/demo1-V2/README.md @@ -0,0 +1,32 @@ +从0到1实现一个web服务器 +--- +### 实现https访问 +1. 生成证书 + 1. 方式1:使用openssl生成证书 + ```shell + openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out keystore.p12 -name myhttps -CAfile chain.pem -caname root + ``` + 2. 方式2:使用第三方工具证书(key和crt) + ```shell + # key 和 crt 转换成 PKCS12(.p12) + openssl pkcs12 -export -in ssl.crt -inkey ssl.key -out keystore_3rd.p12 -name myhttps -CAfile ca.crt -caname root + Enter Export Password: (123456) + Verifying - Enter Export Password: (123456) + ``` +2. 代码参照: [top.yexuejc.demo.core.WebServer](src/main/java/top/yexuejc/demo/core/WebServer.java).startHttps +### 扩展一个ctrl能正常处理请求 +1. 定义[@RestController](src/main/java/top/yexuejc/demo/annotation/RestController.java)和[@GetMapping](src/main/java/top/yexuejc/demo/annotation/GetMapping.java)注解 +2. 定义扫描器和模拟bean容器: [ControllerSupplier](src/main/java/top/yexuejc/demo/core/ControllerSupplier.java) +3. 接入处理逻辑: [RequestHandler](src/main/java/top/yexuejc/demo/core/RequestHandler.java) line 49 +```java +Response response = ControllerSupplier.invoke(request); +``` +4. 程序入口装配: [WebServerApplication](src/main/java/top/yexuejc/demo/WebServerApplication.java) line 16 +```java +ControllerSupplier.builder(WebServerApplication.class); +``` +### 启动参数配置 +参照 [WebServerApplication](src/main/java/top/yexuejc/demo/WebServerApplication.java).argsProcess + +### 配置文件读取 +参照 [AppConfig](src/main/java/top/yexuejc/demo/AppConfig.java) \ No newline at end of file diff --git a/demo1-V2/pom.xml b/demo1-V2/pom.xml new file mode 100644 index 0000000..a79fe01 --- /dev/null +++ b/demo1-V2/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + top.yexuejc + demo1-v2 + 1.0.0 + demo1-v2 + 从0到1实现一个web服务器 + + + 21 + 21 + 21 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + --enable-preview + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/AppConfig.java b/demo1-V2/src/main/java/top/yexuejc/demo/AppConfig.java new file mode 100644 index 0000000..a0a5439 --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/AppConfig.java @@ -0,0 +1,68 @@ +package top.yexuejc.demo; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import top.yexuejc.demo.core.WebServer; + +/** + * @author maxiaofeng + * @date 2025/7/11 13:56 + */ +public class AppConfig { + private static Properties properties = new Properties(); + private static boolean loaded = false; + + public static Properties getProperties() { + if (!loaded) { + loaded = true; + try { + loadDefaultConfig(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return properties; + } + + /** + * 配置加载,同key不覆蓋 + */ + public static void loadAfter(Properties p) { + p.putAll(properties); + properties = p; + } + + public static void put(String key, String value) { + properties.put(key, value); + } + + public static synchronized void loadDefaultConfig() throws Exception { + loadConfig("classpath:application.properties"); + } + + public static Properties loadConfig(String path) throws Exception { + if (path.startsWith("classpath:")) { + // classpath:application.properties + try (InputStream inputStream = WebServer.class.getClassLoader().getResourceAsStream(path.substring(10))) { + Properties properties = new Properties(); + properties.load(inputStream); + AppConfig.loadAfter(properties); + } catch (IOException e) { + throw new Exception(e); + } + } else { + // /home/app/application.properties + try (InputStream inputStream = new FileInputStream(path)) { + Properties properties = new Properties(); + properties.load(inputStream); + AppConfig.loadAfter(properties); + } catch (IOException e) { + throw new Exception(e); + } + } + return AppConfig.properties; + } +} diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/WebServerApplication.java b/demo1-V2/src/main/java/top/yexuejc/demo/WebServerApplication.java new file mode 100644 index 0000000..2273970 --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/WebServerApplication.java @@ -0,0 +1,58 @@ +package top.yexuejc.demo; + +import top.yexuejc.demo.core.ControllerSupplier; +import top.yexuejc.demo.core.WebServer; + +/** + * web服务器入口 + * + * @author maxiaofeng + * @date 2025/6/19 11:11 + */ +public class WebServerApplication { + public static void main(String[] args) { + System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [%4$s] %2$s - %5$s%6$s%n"); + argsProcess(args); + ControllerSupplier.builder(WebServerApplication.class); + new WebServer().start(); + } + + /** + * 处理参数 + * @param args + */ + private static void argsProcess(String[] args) { + if (args != null) { + boolean argsCheck = true; + for (String arg : args) { + String[] split = arg.split("="); + if (split.length == 2) { + String value = split[1]; + switch (split[0]) { + case "--http.port": + AppConfig.put("http.port", value); + break; + case "--https.port": + AppConfig.put("https.port", value); + break; + case "--property": + try { + AppConfig.loadConfig(value); + } catch (Exception e) { + System.out.println("参数错误:" + arg); + argsCheck = false; + } + break; + default: + System.out.println("参数错误:" + arg); + argsCheck = false; + break; + } + } + } + if (!argsCheck) { + System.exit(0); + } + } + } +} diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/annotation/GetMapping.java b/demo1-V2/src/main/java/top/yexuejc/demo/annotation/GetMapping.java new file mode 100644 index 0000000..86ec764 --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/annotation/GetMapping.java @@ -0,0 +1,18 @@ +package top.yexuejc.demo.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author maxiaofeng + * @date 2025/7/11 14:36 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GetMapping { + String[] value() default {}; +} diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/annotation/RestController.java b/demo1-V2/src/main/java/top/yexuejc/demo/annotation/RestController.java new file mode 100644 index 0000000..753e1c7 --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/annotation/RestController.java @@ -0,0 +1,17 @@ +package top.yexuejc.demo.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author maxiaofeng + * @date 2025/7/11 14:35 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RestController { +} diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/core/ControllerSupplier.java b/demo1-V2/src/main/java/top/yexuejc/demo/core/ControllerSupplier.java new file mode 100644 index 0000000..d908e7e --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/core/ControllerSupplier.java @@ -0,0 +1,168 @@ +package top.yexuejc.demo.core; + + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import top.yexuejc.demo.annotation.GetMapping; +import top.yexuejc.demo.annotation.RestController; + +/** + * controller类提供者 + * + * @author maxiaofeng + * @date 2025/7/11 14:40 + */ +public class ControllerSupplier { + private final String scanPackage; + private List> controllerClasses = new ArrayList<>(); + /** @GetMapping 路径映射 */ + private Map getMapping = new ConcurrentHashMap<>(); + + private static volatile ControllerSupplier instance; + + + private ControllerSupplier(String scanPackage) { + this.scanPackage = scanPackage; + } + + public static ControllerSupplier getInstance(String scanPackage) { + if (instance == null) { + synchronized (ControllerSupplier.class) { + if (instance == null) { + instance = new ControllerSupplier(scanPackage); + } + } + } + return instance; + } + + public static void builder(Class applicationClass) { + // 得到类的包名 + String packageName = applicationClass.getPackage().getName(); + ControllerSupplier controllerSupplier = ControllerSupplier.getInstance(packageName); + controllerSupplier.load(); + } + + /** + * 扫描包下的@RestController和@Controller注解的类 + */ + public void load() { + try { + // 获取包路径对应的 URL 资源 + String packagePath = scanPackage.replace(".", "/"); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration resources = classLoader.getResources(packagePath); + + List> classes = new ArrayList<>(); + + while (resources.hasMoreElements()) { + File file = new File(resources.nextElement().getFile()); + if (file.isDirectory()) { + // 遍历目录下所有 .class 文件并加载类 + List> foundClasses = getClassesFromDirectory(file, scanPackage); + classes.addAll(foundClasses); + } + } + + // 筛选出带有 @RestController 的类 + controllerClasses = classes.stream().filter(clazz -> clazz.isAnnotationPresent(RestController.class)).toList(); + + controllerClasses.forEach(clazz -> { + System.out.println("controller: " + clazz.getName()); + // 实例化类 + Object ctrl; + try { + ctrl = clazz.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + // 找到类下的@GetMapping 方法 + Arrays.stream(clazz.getMethods()).filter(method -> method.isAnnotationPresent(GetMapping.class)).forEach(method -> { + GetMapping getMappingAnnotation = method.getAnnotation(GetMapping.class); + String[] uris = getMappingAnnotation.value(); + for (String uri : uris) { + System.out.println("uri: " + uri); + getMapping.put(uri, new CtrlBean(ctrl, method)); + } + }); + }); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static class CtrlBean { + public final Object ctrl; + public final Method method; + + public CtrlBean(Object ctrl, Method method) { + this.ctrl = ctrl; + this.method = method; + } + } + + /** + * 从给定目录扫描并加载所有类 + */ + private List> getClassesFromDirectory(File directory, String packageName) throws ClassNotFoundException { + List> classes = new ArrayList<>(); + if (!directory.exists()) { + return classes; + } + + File[] files = directory.listFiles(); + if (files == null) { + return classes; + } + + for (File file : files) { + if (file.isDirectory()) { + classes.addAll(getClassesFromDirectory(file, STR."\{packageName}.\{file.getName()}")); + } else if (file.getName().endsWith(".class")) { + String className = STR."\{packageName}.\{file.getName().substring(0, file.getName().length() - 6)}"; + Class clazz = Class.forName(className); + classes.add(clazz); + } + } + + return classes; + } + + /** + * 处理请求 + * + * @param request + * @return + */ + public static Response invoke(Request request) { + CtrlBean ctrlBean = ControllerSupplier.getInstance(null).getMapping.get(request.getPath()); + if (ctrlBean != null) { + try { + // 判断method是否有参数 + Object result; + if (ctrlBean.method.getParameterCount() == 0) { + result = ctrlBean.method.invoke(ctrlBean.ctrl); + } else { + result = ctrlBean.method.invoke(ctrlBean.ctrl, request.getParams()); + } + return new Response(200, "OK", "text/html;charset=utf-8", result.toString().getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + e.printStackTrace(); + return new Response(500, "Internal Server Error", "text/html;charset=utf-8", "Internal Server Error".getBytes(StandardCharsets.UTF_8)); + } + } + return null; + } + +} + diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/core/Request.java b/demo1-V2/src/main/java/top/yexuejc/demo/core/Request.java new file mode 100644 index 0000000..a2f3c3d --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/core/Request.java @@ -0,0 +1,36 @@ +package top.yexuejc.demo.core; + +/** + * 请求对象类 + * @author maxiaofeng + * @date 2025/6/19 11:37 + */ +public class Request { + private final String method; + private final String path; + private final String body; + private final String params; + + public Request(String method, String path, String body, String params) { + this.method = method; + this.path = path; + this.body = body; + this.params = params; + } + + public String getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getBody() { + return body; + } + + public String getParams() { + return params; + } +} diff --git a/demo1-V2/src/main/java/top/yexuejc/demo/core/RequestHandler.java b/demo1-V2/src/main/java/top/yexuejc/demo/core/RequestHandler.java new file mode 100644 index 0000000..938cb0b --- /dev/null +++ b/demo1-V2/src/main/java/top/yexuejc/demo/core/RequestHandler.java @@ -0,0 +1,234 @@ +package top.yexuejc.demo.core; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; +import javax.net.ssl.SSLHandshakeException; + + +/** + * 请求处理 + * + * @author maxiaofeng + * @date 2025/6/19 11:29 + */ +public class RequestHandler implements Runnable { + Logger logger = Logger.getLogger(RequestHandler.class.getName()); + private final String OS_NAME = System.getProperty("os.name"); + private final List 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 @@ + + + +
+ + + + + + +
超级管理系统
+
+
+ + + +
+ +
+
+ + \ 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() { + } + +}