第一版教学,作业扩展
This commit is contained in:
parent
61a5493cc0
commit
5c87319677
32
demo1-V2/README.md
Normal file
32
demo1-V2/README.md
Normal file
@ -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)
|
35
demo1-V2/pom.xml
Normal file
35
demo1-V2/pom.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>top.yexuejc</groupId>
|
||||
<artifactId>demo1-v2</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>demo1-v2</name>
|
||||
<description>从0到1实现一个web服务器</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<!-- 开启预览功能:STR模板引擎-->
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
68
demo1-V2/src/main/java/top/yexuejc/demo/AppConfig.java
Normal file
68
demo1-V2/src/main/java/top/yexuejc/demo/AppConfig.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {};
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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<Class<?>> controllerClasses = new ArrayList<>();
|
||||
/** @GetMapping 路径映射 */
|
||||
private Map<String, CtrlBean> 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<URL> resources = classLoader.getResources(packagePath);
|
||||
|
||||
List<Class<?>> classes = new ArrayList<>();
|
||||
|
||||
while (resources.hasMoreElements()) {
|
||||
File file = new File(resources.nextElement().getFile());
|
||||
if (file.isDirectory()) {
|
||||
// 遍历目录下所有 .class 文件并加载类
|
||||
List<Class<?>> 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<Class<?>> getClassesFromDirectory(File directory, String packageName) throws ClassNotFoundException {
|
||||
List<Class<?>> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
36
demo1-V2/src/main/java/top/yexuejc/demo/core/Request.java
Normal file
36
demo1-V2/src/main/java/top/yexuejc/demo/core/Request.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
234
demo1-V2/src/main/java/top/yexuejc/demo/core/RequestHandler.java
Normal file
234
demo1-V2/src/main/java/top/yexuejc/demo/core/RequestHandler.java
Normal file
@ -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<String> 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();
|
||||
}
|
||||
}
|
45
demo1-V2/src/main/java/top/yexuejc/demo/core/Response.java
Normal file
45
demo1-V2/src/main/java/top/yexuejc/demo/core/Response.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
165
demo1-V2/src/main/java/top/yexuejc/demo/core/WebServer.java
Normal file
165
demo1-V2/src/main/java/top/yexuejc/demo/core/WebServer.java
Normal file
@ -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服务停止.");
|
||||
}
|
||||
}
|
22
demo1-V2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
Normal file
22
demo1-V2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
Normal file
@ -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}";
|
||||
}
|
||||
}
|
22
demo1-V2/src/main/resources/Intasect.crt
Normal file
22
demo1-V2/src/main/resources/Intasect.crt
Normal file
@ -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-----
|
5
demo1-V2/src/main/resources/application.properties
Normal file
5
demo1-V2/src/main/resources/application.properties
Normal file
@ -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
|
BIN
demo1-V2/src/main/resources/ssl/keystore_3rd.p12
Normal file
BIN
demo1-V2/src/main/resources/ssl/keystore_3rd.p12
Normal file
Binary file not shown.
22
demo1-V2/src/main/resources/ssl/ssl.crt
Normal file
22
demo1-V2/src/main/resources/ssl/ssl.crt
Normal file
@ -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-----
|
17
demo1-V2/src/main/resources/ssl/ssl.csr
Normal file
17
demo1-V2/src/main/resources/ssl/ssl.csr
Normal file
@ -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-----
|
28
demo1-V2/src/main/resources/ssl/ssl.key
Normal file
28
demo1-V2/src/main/resources/ssl/ssl.key
Normal file
@ -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-----
|
22
demo1-V2/src/main/resources/static/css/index.css
Normal file
22
demo1-V2/src/main/resources/static/css/index.css
Normal file
@ -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;
|
||||
}
|
13
demo1-V2/src/main/resources/web/index.html
Normal file
13
demo1-V2/src/main/resources/web/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Simple Web Server</title>
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to Simple Web Server</h1>
|
||||
<p>This page is served by our custom Java web server!</p>
|
||||
<p>你的请求参数是:${body}</p>
|
||||
</body>
|
||||
</html>
|
22
demo1/src/main/resources/ssl/ssl.crt
Normal file
22
demo1/src/main/resources/ssl/ssl.crt
Normal file
@ -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-----
|
17
demo1/src/main/resources/ssl/ssl.csr
Normal file
17
demo1/src/main/resources/ssl/ssl.csr
Normal file
@ -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-----
|
28
demo1/src/main/resources/ssl/ssl.key
Normal file
28
demo1/src/main/resources/ssl/ssl.key
Normal file
@ -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-----
|
98
demo2/pom.xml
Normal file
98
demo2/pom.xml
Normal file
@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>top.yexuejc</groupId>
|
||||
<artifactId>demo2</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>demo2</name>
|
||||
<description>demo2</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>nz.net.ultraq.thymeleaf</groupId>
|
||||
<artifactId>thymeleaf-layout-dialect</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<!-- 项目热部署 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional> <!-- 表示依赖不会传递 -->
|
||||
<scope>true</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
13
demo2/src/main/java/top/yexuejc/demo/Demo2Application.java
Normal file
13
demo2/src/main/java/top/yexuejc/demo/Demo2Application.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
22
demo2/src/main/java/top/yexuejc/demo/model/LoginVO.java
Normal file
22
demo2/src/main/java/top/yexuejc/demo/model/LoginVO.java
Normal file
@ -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;
|
||||
//}
|
||||
|
18
demo2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
Normal file
18
demo2/src/main/java/top/yexuejc/demo/web/IndexCtrl.java
Normal file
@ -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");
|
||||
}
|
||||
}
|
56
demo2/src/main/java/top/yexuejc/demo/web/LoginCtrl.java
Normal file
56
demo2/src/main/java/top/yexuejc/demo/web/LoginCtrl.java
Normal file
@ -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<String, Object> 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:/";
|
||||
}
|
||||
}
|
9
demo2/src/main/resources/application.properties
Normal file
9
demo2/src/main/resources/application.properties
Normal file
@ -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
|
127
demo2/src/main/resources/static/css/common/main.css
Normal file
127
demo2/src/main/resources/static/css/common/main.css
Normal file
@ -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;
|
||||
}
|
38
demo2/src/main/resources/static/css/login.css
Normal file
38
demo2/src/main/resources/static/css/login.css
Normal file
@ -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;
|
||||
}
|
27
demo2/src/main/resources/templates/common/header.html
Normal file
27
demo2/src/main/resources/templates/common/header.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div class="h-container">
|
||||
<svg class="h-icon-menu h-icon-menu-show" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.776 899.072h1000.96v59.904H11.776zM12.8 65.024H1013.76v59.904H12.8zM10.24 473.088h636.416v59.904H10.24zM765.44 728.064l244.224-229.888-244.224-229.888z"></path>
|
||||
</svg>
|
||||
<svg class="h-icon-menu h-icon-menu-hide" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.264 64.512h1001.472v59.904H11.264zM10.24 899.072h1001.472v59.904H10.24zM376.32 490.496h636.928v59.904H376.32zM258.56 295.424l-244.736 230.4 244.736 229.888z"></path>
|
||||
</svg>
|
||||
<div class="h-sys-name">超级管理系统</div>
|
||||
<div class="h-right-container">
|
||||
<div class="h-notice">
|
||||
<svg class="h-icon-menu" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 42.666667c63.637333 0 116.288 50.432 120.682667 114.005333 103.125333 46.058667 175.552 149.12 175.552 269.696v203.306667C867.477333 677.952 896 741.525333 896 820.458667c0 17.557333-15.36 32.896-32.917333 32.896H640l-0.085333 4.8A128 128 0 0 1 384 853.333333L160.917333 853.333333C143.36 853.333333 128 837.973333 128 820.437333c0-76.736 28.522667-142.506667 87.765333-190.741333v-203.306667c0-120.618667 72.426667-223.658667 175.552-269.717333C395.712 93.098667 448.362667 42.666667 512 42.666667z m64 810.688L448 853.333333l0.106667 3.754667A64 64 0 0 0 576 853.354667zM512 106.666667c-29.525333 0-54.741333 23.914667-56.832 54.421333l-2.645333 38.357333-35.114667 15.68c-83.072 37.077333-137.642667 119.104-137.642667 211.242667v233.749333l-23.594666 19.2c-35.136 28.608-55.744 65.173333-62.08 110.016h635.904l-1.066667-7.082666c-7.210667-41.962667-27.2-75.306667-61.098667-102.933334l-23.594666-19.2V426.368c0-92.16-54.570667-174.165333-137.642667-211.242667l-35.114667-15.68-2.645333-38.357333C566.741333 130.581333 541.525333 106.666667 512 106.666667z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-user-info">
|
||||
<div class="h-user-name">maxiaofeng</div>
|
||||
<div class="h-user-avatar">
|
||||
<img src="https://picsum.photos/id/237/200/200" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
10
demo2/src/main/resources/templates/common/menu.html
Normal file
10
demo2/src/main/resources/templates/common/menu.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
71
demo2/src/main/resources/templates/index.html
Normal file
71
demo2/src/main/resources/templates/index.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title layout:title-pattern="${contentTitle} - 我的应用">首页</title>
|
||||
<link rel="stylesheet" th:href="@{/css/common/main.css}" href="../static/css/common/main.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="ui-menu" layout:insert="~{common/menu}">
|
||||
|
||||
</div>
|
||||
<div class="ui-main-container">
|
||||
<header class="ui-header">
|
||||
<div layout:insert="~{common/header}">
|
||||
<svg class="h-icon-menu h-icon-menu-hide" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.264 64.512h1001.472v59.904H11.264zM10.24 899.072h1001.472v59.904H10.24zM376.32 490.496h636.928v59.904H376.32zM258.56 295.424l-244.736 230.4 244.736 229.888z"></path>
|
||||
</svg>
|
||||
<svg class="h-icon-menu h-icon-menu-show" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.776 899.072h1000.96v59.904H11.776zM12.8 65.024H1013.76v59.904H12.8zM10.24 473.088h636.416v59.904H10.24zM765.44 728.064l244.224-229.888-244.224-229.888z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui-content" layout:fragment="content">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>$(document).ready(function () {
|
||||
const $menu = $('.ui-menu');
|
||||
const $mainContainer = $('.ui-main-container');
|
||||
const $iconHide = $('.h-icon-menu-hide');
|
||||
const $iconShow = $('.h-icon-menu-show');
|
||||
|
||||
// 初始状态:菜单显示
|
||||
let isMenuVisible = true;
|
||||
|
||||
function updateLayout() {
|
||||
if (isMenuVisible) {
|
||||
$menu.css('width', '200px');
|
||||
$mainContainer.css({
|
||||
'margin-left': '200px',
|
||||
'width': 'calc(100% - 200px)'
|
||||
});
|
||||
} else {
|
||||
$menu.css('width', '0');
|
||||
$mainContainer.css({
|
||||
'margin-left': '0',
|
||||
'width': '100%'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 点击 hide 图标:隐藏菜单
|
||||
$iconHide.on('click', function () {
|
||||
isMenuVisible = false;
|
||||
updateLayout();
|
||||
$iconHide.hide();
|
||||
$iconShow.show();
|
||||
});
|
||||
|
||||
// 点击 show 图标:显示菜单
|
||||
$iconShow.on('click', function () {
|
||||
isMenuVisible = true;
|
||||
updateLayout();
|
||||
$iconShow.hide();
|
||||
$iconHide.show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
60
demo2/src/main/resources/templates/login.html
Normal file
60
demo2/src/main/resources/templates/login.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - 我的应用</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link th:href="@{/css/login.css}" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="card-title">用户登录</h2>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息 -->
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<!-- 注销消息 -->
|
||||
<div th:if="${message}" class="alert alert-success" role="alert">
|
||||
<span th:text="${message}"></span>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form th:action="@{/login/postFrom}" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">用户名</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="请输入用户名" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">密码</label>
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="请输入密码" required>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mb-3">
|
||||
<button type="submit" class="btn btn-primary">登录</button>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a th:href="@{/register}" class="text-decoration-none">注册新账户</a>
|
||||
<span class="mx-2">|</span>
|
||||
<a href="#" class="text-decoration-none">忘记密码?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user