diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
index 11a5ee0782..497a213a2f 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
@@ -15,6 +15,8 @@ public interface WebFilterOrderEnum {
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+ int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1;
+
// OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java
new file mode 100644
index 0000000000..135eb85bb0
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.framework.encrypt.config;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * HTTP API 加解密配置
+ *
+ * @author 芋道源码
+ */
+@ConfigurationProperties(prefix = "yudao.api-encrypt")
+@Validated
+@Data
+public class ApiEncryptProperties {
+
+ /**
+ * 是否开启
+ */
+ @NotNull(message = "是否开启不能为空")
+ private Boolean enable;
+
+ /**
+ * 请求头(响应头)名称
+ *
+ * 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密
+ * 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密
+ */
+ @NotEmpty(message = "请求头(响应头)名称不能为空")
+ private String header = "X-Api-Encrypt";
+
+ /**
+ * 对称加密算法,用于请求/响应的加解密
+ *
+ * 目前支持
+ * 【对称加密】:
+ * 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES}
+ * 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低)
+ * 【非对称加密】
+ * 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA}
+ * 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低)
+ *
+ * @see 什么是公钥和私钥?
+ */
+ @NotEmpty(message = "对称加密算法不能为空")
+ private String algorithm;
+
+ /**
+ * 请求的解密密钥
+ *
+ * 注意:
+ * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。
+ * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!)
+ */
+ @NotEmpty(message = "请求的解密密钥不能为空")
+ private String requestKey;
+
+ /**
+ * 响应的加密密钥
+ *
+ * 注意:
+ * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。
+ * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!)
+ */
+ @NotEmpty(message = "响应的加密密钥不能为空")
+ private String responseKey;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java
new file mode 100644
index 0000000000..03d0f1ac12
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.encrypt.config;
+
+import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
+import cn.iocoder.yudao.framework.encrypt.core.filter.ApiEncryptFilter;
+import cn.iocoder.yudao.framework.web.config.WebProperties;
+import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import static cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean;
+
+@AutoConfiguration
+@Slf4j
+@EnableConfigurationProperties(ApiEncryptProperties.class)
+@ConditionalOnProperty(prefix = "yudao.api-encrypt", name = "enable", havingValue = "true")
+public class YudaoApiEncryptAutoConfiguration {
+
+ @Bean
+ public FilterRegistrationBean apiEncryptFilter(WebProperties webProperties,
+ ApiEncryptProperties apiEncryptProperties,
+ RequestMappingHandlerMapping requestMappingHandlerMapping,
+ GlobalExceptionHandler globalExceptionHandler) {
+ ApiEncryptFilter filter = new ApiEncryptFilter(webProperties, apiEncryptProperties,
+ requestMappingHandlerMapping, globalExceptionHandler);
+ return createFilterBean(filter, WebFilterOrderEnum.API_ENCRYPT_FILTER);
+
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java
new file mode 100644
index 0000000000..7405111038
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.framework.encrypt.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * HTTP API 加解密注解
+ */
+@Documented
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiEncrypt {
+
+ /**
+ * 是否对请求参数进行解密,默认 true
+ */
+ boolean request() default true;
+
+ /**
+ * 是否对响应结果进行加密,默认 true
+ */
+ boolean response() default true;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java
new file mode 100644
index 0000000000..b9f015a7ec
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java
@@ -0,0 +1,86 @@
+package cn.iocoder.yudao.framework.encrypt.core.filter;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.asymmetric.AsymmetricDecryptor;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.symmetric.SymmetricDecryptor;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * 解密请求 {@link HttpServletRequestWrapper} 实现类
+ *
+ * @author 芋道源码
+ */
+public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper {
+
+ private final byte[] body;
+
+ public ApiDecryptRequestWrapper(HttpServletRequest request,
+ SymmetricDecryptor symmetricDecryptor,
+ AsymmetricDecryptor asymmetricDecryptor) throws IOException {
+ super(request);
+ // 读取 body,允许 HEX、BASE64 传输
+ String requestBody = StrUtil.utf8Str(
+ IoUtil.readBytes(request.getInputStream(), false));
+
+ // 解密 body
+ body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody)
+ : asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey);
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ return new BufferedReader(new InputStreamReader(this.getInputStream()));
+ }
+
+ @Override
+ public int getContentLength() {
+ return body.length;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ return body.length;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() {
+ ByteArrayInputStream stream = new ByteArrayInputStream(body);
+ return new ServletInputStream() {
+
+ @Override
+ public int read() {
+ return stream.read();
+ }
+
+ @Override
+ public int available() {
+ return body.length;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+ }
+
+ };
+ }
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java
new file mode 100644
index 0000000000..e6d03ba32b
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java
@@ -0,0 +1,152 @@
+package cn.iocoder.yudao.framework.encrypt.core.filter;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.asymmetric.AsymmetricDecryptor;
+import cn.hutool.crypto.asymmetric.AsymmetricEncryptor;
+import cn.hutool.crypto.symmetric.SymmetricDecryptor;
+import cn.hutool.crypto.symmetric.SymmetricEncryptor;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties;
+import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiEncrypt;
+import cn.iocoder.yudao.framework.web.config.WebProperties;
+import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
+import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerExecutionChain;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.io.IOException;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
+
+/**
+ * API 加密过滤器,处理 {@link ApiEncrypt} 注解。
+ *
+ * 1. 解密请求参数
+ * 2. 加密响应结果
+ *
+ * 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢?
+ * 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!!
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class ApiEncryptFilter extends ApiRequestFilter {
+
+ private final ApiEncryptProperties apiEncryptProperties;
+
+ private final RequestMappingHandlerMapping requestMappingHandlerMapping;
+
+ private final GlobalExceptionHandler globalExceptionHandler;
+
+ private final SymmetricDecryptor requestSymmetricDecryptor;
+ private final AsymmetricDecryptor requestAsymmetricDecryptor;
+
+ private final SymmetricEncryptor responseSymmetricEncryptor;
+ private final AsymmetricEncryptor responseAsymmetricEncryptor;
+
+ public ApiEncryptFilter(WebProperties webProperties,
+ ApiEncryptProperties apiEncryptProperties,
+ RequestMappingHandlerMapping requestMappingHandlerMapping,
+ GlobalExceptionHandler globalExceptionHandler) {
+ super(webProperties);
+ this.apiEncryptProperties = apiEncryptProperties;
+ this.requestMappingHandlerMapping = requestMappingHandlerMapping;
+ this.globalExceptionHandler = globalExceptionHandler;
+ if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) {
+ this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey()));
+ this.requestAsymmetricDecryptor = null;
+ this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey()));
+ this.responseAsymmetricEncryptor = null;
+ } else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) {
+ this.requestSymmetricDecryptor = null;
+ this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null);
+ this.responseSymmetricEncryptor = null;
+ this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey());
+ } else {
+ // 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。
+ throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm());
+ }
+ }
+
+ @Override
+ @SuppressWarnings("NullableProblems")
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ // 获取 @ApiEncrypt 注解
+ ApiEncrypt apiEncrypt = getApiEncrypt(request);
+ boolean requestEnable = apiEncrypt != null && apiEncrypt.request();
+ boolean responseEnable = apiEncrypt != null && apiEncrypt.response();
+ String encryptHeader = request.getHeader(apiEncryptProperties.getHeader());
+ if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ // 1. 解密请求
+ if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()),
+ HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) {
+ try {
+ if (StrUtil.isNotBlank(encryptHeader)) {
+ request = new ApiDecryptRequestWrapper(request,
+ requestSymmetricDecryptor, requestAsymmetricDecryptor);
+ } else if (requestEnable) {
+ throw invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头");
+ }
+ } catch (Exception ex) {
+ CommonResult> result = globalExceptionHandler.allExceptionHandler(request, ex);
+ ServletUtils.writeJSON(response, result);
+ return;
+ }
+ }
+
+ // 2. 执行过滤器链
+ if (responseEnable) {
+ // 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!!
+ response = new ApiEncryptResponseWrapper(response);
+ }
+ chain.doFilter(request, response);
+
+ // 3. 加密响应(真正执行)
+ if (responseEnable) {
+ ((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties,
+ responseSymmetricEncryptor, responseAsymmetricEncryptor);
+ }
+ }
+
+ /**
+ * 获取 @ApiEncrypt 注解
+ *
+ * @param request 请求
+ */
+ private ApiEncrypt getApiEncrypt(HttpServletRequest request) {
+ try {
+ HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request);
+ if (mappingHandler == null) {
+ return null;
+ }
+ Object handler = mappingHandler.getHandler();
+ if (handler instanceof HandlerMethod handlerMethod) {
+ ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class);
+ if (annotation == null) {
+ annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class);
+ }
+ return annotation;
+ }
+ } catch (Exception e) {
+ log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]",
+ request.getRequestURI(), request.getMethod(), e);
+ }
+ return null;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java
new file mode 100644
index 0000000000..fed38917b9
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java
@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.framework.encrypt.core.filter;
+
+import cn.hutool.crypto.asymmetric.AsymmetricEncryptor;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.symmetric.SymmetricEncryptor;
+import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+/**
+ * 加密响应 {@link HttpServletResponseWrapper} 实现类
+ *
+ * @author 芋道源码
+ */
+public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper {
+
+ private final ByteArrayOutputStream byteArrayOutputStream;
+ private final ServletOutputStream servletOutputStream;
+ private final PrintWriter printWriter;
+
+ public ApiEncryptResponseWrapper(HttpServletResponse response) {
+ super(response);
+ this.byteArrayOutputStream = new ByteArrayOutputStream();
+ this.servletOutputStream = this.getOutputStream();
+ this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
+ }
+
+ public void encrypt(ApiEncryptProperties properties,
+ SymmetricEncryptor symmetricEncryptor,
+ AsymmetricEncryptor asymmetricEncryptor) throws IOException {
+ // 1.1 清空 body
+ HttpServletResponse response = (HttpServletResponse) this.getResponse();
+ response.resetBuffer();
+ // 1.2 获取 body
+ this.flushBuffer();
+ byte[] body = byteArrayOutputStream.toByteArray();
+
+ // 2. 加密 body
+ String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body)
+ : asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey);
+ response.getWriter().write(encryptedBody);
+
+ // 3. 添加加密 header 标识
+ this.addHeader(properties.getHeader(), "true");
+ // 特殊:特殊:https://juejin.cn/post/6867327674675625992
+ this.addHeader("Access-Control-Expose-Headers", properties.getHeader());
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return printWriter;
+ }
+
+ @Override
+ public void flushBuffer() throws IOException {
+ if (servletOutputStream != null) {
+ servletOutputStream.flush();
+ }
+ if (printWriter != null) {
+ printWriter.flush();
+ }
+ }
+
+ @Override
+ public void reset() {
+ byteArrayOutputStream.reset();
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ return new ServletOutputStream() {
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ }
+
+ @Override
+ public void write(int b) {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ @SuppressWarnings("NullableProblems")
+ public void write(byte[] b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ @SuppressWarnings("NullableProblems")
+ public void write(byte[] b, int off, int len) {
+ byteArrayOutputStream.write(b, off, len);
+ }
+
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java
new file mode 100644
index 0000000000..ca08197125
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * HTTP API 加密组件:支持 Request 和 Response 的加密、解密
+ */
+package cn.iocoder.yudao.framework.encrypt;
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java
index 8e80fa591f..b5f38d96fd 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java
@@ -1,14 +1,13 @@
package cn.iocoder.yudao.framework.web.core.filter;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
-
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
+
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
-import java.io.IOException;
import java.io.InputStreamReader;
/**
@@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
}
@Override
- public BufferedReader getReader() throws IOException {
+ public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
- public ServletInputStream getInputStream() throws IOException {
+ public int getContentLength() {
+ return body.length;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ return body.length;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
// 返回 ServletInputStream
return new ServletInputStream() {
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 9cdcd09c4e..07a7955c34 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -3,4 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration
cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration
cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration
cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration
-cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
\ No newline at end of file
+cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
+cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java
new file mode 100644
index 0000000000..12d406e5f5
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java
@@ -0,0 +1,86 @@
+package cn.iocoder.yudao.framework.encrypt;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
+import org.junit.jupiter.api.Test;
+
+import java.util.Objects;
+
+/**
+ * 各种 API 加解密的测试类:不是单测,而是方便大家生成密钥、加密、解密等操作。
+ *
+ * @author 芋道源码
+ */
+@SuppressWarnings("ConstantValue")
+public class ApiEncryptTest {
+
+ @Test
+ public void testGenerateAsymmetric() {
+ String asymmetricAlgorithm = AsymmetricAlgorithm.RSA.getValue();
+// String asymmetricAlgorithm = "SM2";
+// String asymmetricAlgorithm = SM4.ALGORITHM_NAME;
+// String asymmetricAlgorithm = SymmetricAlgorithm.AES.getValue();
+ String requestClientKey = null;
+ String requestServerKey = null;
+ String responseClientKey = null;
+ String responseServerKey = null;
+ if (Objects.equals(asymmetricAlgorithm, AsymmetricAlgorithm.RSA.getValue())) {
+ // 请求的密钥
+ RSA requestRsa = SecureUtil.rsa();
+ requestClientKey = requestRsa.getPublicKeyBase64();
+ requestServerKey = requestRsa.getPrivateKeyBase64();
+ // 响应的密钥
+ RSA responseRsa = new RSA();
+ responseClientKey = responseRsa.getPrivateKeyBase64();
+ responseServerKey = responseRsa.getPublicKeyBase64();
+ } else if (Objects.equals(asymmetricAlgorithm, SymmetricAlgorithm.AES.getValue())) {
+ // AES 密钥可选 32、24、16 位
+ // 请求的密钥(前后端密钥一致)
+ requestClientKey = RandomUtil.randomNumbers(32);
+ requestServerKey = requestClientKey;
+ // 响应的密钥(前后端密钥一致)
+ responseClientKey = RandomUtil.randomNumbers(32);
+ responseServerKey = responseClientKey;
+ }
+
+ // 打印结果
+ System.out.println("requestClientKey = " + requestClientKey);
+ System.out.println("requestServerKey = " + requestServerKey);
+ System.out.println("responseClientKey = " + responseClientKey);
+ System.out.println("responseServerKey = " + responseServerKey);
+ }
+
+ @Test
+ public void testEncrypt_aes() {
+ String key = "52549111389893486934626385991395";
+ String body = "{\n" +
+ " \"username\": \"admin\",\n" +
+ " \"password\": \"admin123\",\n" +
+ " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" +
+ " \"code\": \"1024\"\n" +
+ "}";
+ String encrypt = SecureUtil.aes(StrUtil.utf8Bytes(key))
+ .encryptBase64(body);
+ System.out.println("encrypt = " + encrypt);
+ }
+
+ @Test
+ public void testEncrypt_rsa() {
+ String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB";
+ String body = "{\n" +
+ " \"username\": \"admin\",\n" +
+ " \"password\": \"admin123\",\n" +
+ " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" +
+ " \"code\": \"1024\"\n" +
+ "}";
+ String encrypt = SecureUtil.rsa(null, key)
+ .encryptBase64(body, KeyType.PublicKey);
+ System.out.println("encrypt = " + encrypt);
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http
index f42dfcd030..52a724bf35 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http
@@ -11,6 +11,24 @@ tag: Yunai.local
"code": "1024"
}
+### 请求 /login 接口【加密 AES】 => 成功
+POST {{baseUrl}}/system/auth/login
+Content-Type: application/json
+tenant-id: {{adminTenantId}}
+tag: Yunai.local
+X-API-ENCRYPT: true
+
+WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRtad+JrqChAul/sR/SdOsUKqjBhvvQx1JVhzxr6s8uUP67aKTSZ6Psv7O32ELxXrzSaQvG5CInzz3w6sLtbNNLd1kXe6Q=
+
+### 请求 /login 接口【加密 RSA】 => 成功
+POST {{baseUrl}}/system/auth/login
+Content-Type: application/json
+tenant-id: {{adminTenantId}}
+tag: Yunai.local
+X-API-ENCRYPT: true
+
+e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVAEd27wwrXBmupOOA/bhpuzzDwcRuJRD+z+YgiNoEXFDRHERxPYlPqAe9zAHtihD0ceub1AjybQsEsROew4C3Q602XYW0=
+
### 请求 /login 接口 => 成功(无验证码)
POST {{baseUrl}}/system/auth/login
Content-Type: application/json
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index d82d3974a5..1decd79cda 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -245,6 +245,13 @@ yudao:
security:
permit-all_urls:
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
+ api-encrypt:
+ enable: true # 是否开启 API 加密
+ algorithm: AES # 加密算法,支持 AES、RSA 等
+ request-key: 52549111389893486934626385991395 # 【AES 案例】请求加密的秘钥,,必须 16、24、32 位
+ response-key: 96103715984234343991809655248883 # 【AES 案例】响应加密的秘钥,AES 案例,必须 16、24、32 位
+# request-key: MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKWzasimcZ1icsWDPVdTXcZs1DkOWjI+m9bTQU8aOqflnomkr6QO1WWeSHBHzuJGsTlV/ZY2pFfq/NstKC94hBjx7yioYJvzb2bKN/Uy4j5nM3iCF//u0RiFkkY8j0Bt/EWoFTOb6RHf8cHIAjbYYtP3pYzbpCIwryfe0g//KIuzAgMBAAECgYADDjZrYcpZjR2xr7RbXmGtzYbyUGXwZEAqa3XaWBD51J2iSyOkAlQEDjGmxGQ3vvb4qDHHadWI+3/TKNeDXJUO+xTVJrnismK5BsHyC6dfxlIK/5BAuknryTca/3UoA1yomS9ZlF3Q0wcecaDoEnSmZEaTrp9T3itPAz4KnGjv5QJBAN5mNcfu6iJ5ktNvEdzqcxkKwbXb9Nq1SLnmTvt+d5TPX7eQ9fCwtOfVu5iBLhhZzb5PJ7pSN3Zt6rl5/jPOGv0CQQC+vETX9oe1wbxZSv6/RBGy0Xow6GndbJwvd89PcAJ2h+OJXWtg/rRHB3t9EQm7iis0XbZTapj19E4U6l8EibhvAkEA1CvYpRwmHKu1SqdM+GBnW/2qHlBwwXJvpoK02TOm674HR/4w0+YRQJfkd7LOAgcyxJuJgDTNmtt0MmzS+iNoFQJAMVSUIZ77XoDq69U/qcw7H5qaFcgmiUQr6QL9tTftCyb+LGri+MUnby96OtCLSdvkbLjIDS8GvKYhA7vSM2RDNQJBAKGyVVnFFIrbK3yuwW71yvxQEGoGxlgvZSezZ4vGgqTxrr9HvAtvWLwR6rpe6ybR/x8uUtoW7NRBWgpiIFwjvY4= # 【RSA 案例】请求解密的私钥
+# response-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDh/CHyBcS/zEfVyINVA7+c9Xxl0CPdxPMK1OIjxaLy/7BLfbkoEpI8onQtjuzfpuxCraDem9bu3BMF0pMH95HytI3Vi0kGjaV+WLIalwgc2w37oA2sbsmKzQOP7SDLO5s2QJNAD7kVwd+Q5rqaLu2MO0xVv+0IUJhn83hClC0L5wIDAQAB # 【RSA 案例】响应加密的公钥
websocket:
enable: true # websocket的开关
path: /infra/ws # 路径