From 26effb6ee2a98cbedc0dd3ea1d15b453cdf0c0d8 Mon Sep 17 00:00:00 2001
From: lishuyanla <1206770390@qq.com>
Date: Mon, 11 Aug 2025 21:22:42 +0800
Subject: [PATCH] =?UTF-8?q?feat(security/crypto):=20=E6=96=B0=E5=A2=9E=20A?=
=?UTF-8?q?PI=20=E5=8A=A0/=E8=A7=A3=E5=AF=86=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: lishuyan<1206770390@qq.com>
# message auto-generated for no-merge-commit merge:
merge lishuyan/dev into dev
feat:✨ 新增 API 加/解密功能。
Created-by: lishuyanla
Commit-by: lishuyan
Merged-by: Charles_7c
Description: 1、feat:✨ 新增 API 加解密功能。
## PR 类型
- [x] 新 feature
- [ ] Bug 修复
- [ ] 功能增强
- [ ] 文档变更
- [ ] 代码样式变更
- [ ] 重构
- [ ] 性能改进
- [ ] 单元测试
- [ ] CI/CD
- [ ] 其他
## PR 目的
1、feat:✨ 新增 API 加/解密功能。
2、支持PUT/POST请求方法且JSON类型的请求解密。
3、支持响应加密。
## 解决方案
1、新增加解密工具类
2、新增API加解密过滤器。
3、新增API加解密自动配置。
## PR 测试
请求加密和解密:

响应加密和解密:

## Changelog
| 模块 | Changelog | Related issues |
| -------------------------------- | ------------------------------------------------------------ | -------------- |
| continew-starter-security-crypto | feat:✨ 新增 API 加/解密功能。
- 新增 API 加密注解和相关配置
- 实现请求体解密和响应体加密的过滤器
- 添加必要的工具类和属性配置 | |
## 其他信息
## 提交前确认
- [x] PR 代码经过了完整测试,并且通过了代码规范检查
- [x] 已经完整填写 Changelog,并链接到了相关 issues
- [x] PR 代码将要提交到 dev 分支
See merge request: continew/continew-starter!3
---
.../core/constant/OrderedConstants.java | 5 +
.../core/constant/PropertiesConstants.java | 7 +-
.../crypto/annotation/ApiEncrypt.java | 37 +++++
.../ApiCryptoAutoConfiguration.java | 69 +++++++++
.../autoconfigure/ApiCryptoProperties.java | 82 ++++++++++
.../crypto/filter/ApiCryptoFilter.java | 121 +++++++++++++++
.../filter/RequestBodyDecryptWrapper.java | 126 ++++++++++++++++
.../filter/ResponseBodyEncryptWrapper.java | 140 ++++++++++++++++++
.../security/crypto/util/EncryptHelper.java | 100 +++++++++++++
...ot.autoconfigure.AutoConfiguration.imports | 3 +-
.../src/main/resources/default-api-crypto.yml | 6 +
11 files changed, 694 insertions(+), 2 deletions(-)
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/annotation/ApiEncrypt.java
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/autoconfigure/ApiCryptoAutoConfiguration.java
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/autoconfigure/ApiCryptoProperties.java
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ApiCryptoFilter.java
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/RequestBodyDecryptWrapper.java
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ResponseBodyEncryptWrapper.java
create mode 100644 continew-starter-security/continew-starter-security-crypto/src/main/resources/default-api-crypto.yml
diff --git a/continew-starter-core/src/main/java/top/continew/starter/core/constant/OrderedConstants.java b/continew-starter-core/src/main/java/top/continew/starter/core/constant/OrderedConstants.java
index 1ea1f239..e81410d2 100644
--- a/continew-starter-core/src/main/java/top/continew/starter/core/constant/OrderedConstants.java
+++ b/continew-starter-core/src/main/java/top/continew/starter/core/constant/OrderedConstants.java
@@ -31,6 +31,11 @@ public class OrderedConstants {
*/
public static final class Filter {
+ /**
+ * API加/密过滤器顺序
+ */
+ public static final int API_CRYPTO_FILTER = Ordered.HIGHEST_PRECEDENCE;
+
/**
* 链路追踪过滤器顺序
*/
diff --git a/continew-starter-core/src/main/java/top/continew/starter/core/constant/PropertiesConstants.java b/continew-starter-core/src/main/java/top/continew/starter/core/constant/PropertiesConstants.java
index d1333e5a..dc495ece 100644
--- a/continew-starter-core/src/main/java/top/continew/starter/core/constant/PropertiesConstants.java
+++ b/continew-starter-core/src/main/java/top/continew/starter/core/constant/PropertiesConstants.java
@@ -55,10 +55,15 @@ public class PropertiesConstants {
public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security";
/**
- * 安全-加/解密配置
+ * 安全-数据加/解密配置
*/
public static final String SECURITY_CRYPTO = SECURITY + StringConstants.DOT + "crypto";
+ /**
+ * 安全-API加/解密配置
+ */
+ public static final String SECURITY_API_CRYPTO = SECURITY + StringConstants.DOT + "api-crypto";
+
/**
* 安全-敏感词配置
*/
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/annotation/ApiEncrypt.java b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/annotation/ApiEncrypt.java
new file mode 100644
index 00000000..7d5b1e78
--- /dev/null
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/annotation/ApiEncrypt.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * http://www.gnu.org/licenses/lgpl.html + *
+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.starter.security.crypto.annotation; + +import java.lang.annotation.*; + +/** + * API加密注解 + * + * @author lishuyan + * @since 2.14.0 + */ +@Documented +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiEncrypt { + + /** + * 默认API响应加密 + */ + boolean response() default true; + +} diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/autoconfigure/ApiCryptoAutoConfiguration.java b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/autoconfigure/ApiCryptoAutoConfiguration.java new file mode 100644 index 00000000..91061cf6 --- /dev/null +++ b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/autoconfigure/ApiCryptoAutoConfiguration.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * http://www.gnu.org/licenses/lgpl.html + *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.security.crypto.autoconfigure;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.servlet.DispatcherType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.context.annotation.PropertySource;
+import top.continew.starter.core.constant.OrderedConstants;
+import top.continew.starter.core.constant.PropertiesConstants;
+import top.continew.starter.core.util.GeneralPropertySourceFactory;
+import top.continew.starter.security.crypto.filter.ApiCryptoFilter;
+
+/**
+ * API加/解密自动配置
+ *
+ * @author lishuyan
+ * @since 2.14.0
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(ApiCryptoProperties.class)
+@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_API_CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
+@PropertySource(value = "classpath:default-api-crypto.yml", factory = GeneralPropertySourceFactory.class)
+public class ApiCryptoAutoConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(ApiCryptoAutoConfiguration.class);
+
+ /**
+ * API加/解密过滤器
+ *
+ * @param properties 配置
+ * @return 过滤器
+ */
+ @Bean
+ public FilterRegistrationBean
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.security.crypto.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import top.continew.starter.core.constant.PropertiesConstants;
+
+/**
+ * API加/解密属性配置
+ *
+ * @author lishuyan
+ * @since 2.14.0
+ */
+@ConfigurationProperties(PropertiesConstants.SECURITY_API_CRYPTO)
+public class ApiCryptoProperties {
+
+ /**
+ * 是否启用
+ */
+ private Boolean enabled;
+
+ /**
+ * 请求头中 AES 密钥 键名
+ */
+ private String secretKeyHeader = "X-Api-Crypto";
+
+ /**
+ * 响应加密公钥
+ */
+ private String publicKey;
+
+ /**
+ * 请求解密私钥
+ */
+ private String privateKey;
+
+ public Boolean getEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getSecretKeyHeader() {
+ return secretKeyHeader;
+ }
+
+ public void setSecretKeyHeader(String secretKeyHeader) {
+ this.secretKeyHeader = secretKeyHeader;
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+
+ public void setPublicKey(String publicKey) {
+ this.publicKey = publicKey;
+ }
+
+ public String getPrivateKey() {
+ return privateKey;
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.privateKey = privateKey;
+ }
+}
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ApiCryptoFilter.java b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ApiCryptoFilter.java
new file mode 100644
index 00000000..d6a89fd5
--- /dev/null
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ApiCryptoFilter.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.security.crypto.filter;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+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 top.continew.starter.security.crypto.annotation.ApiEncrypt;
+import top.continew.starter.security.crypto.autoconfigure.ApiCryptoProperties;
+
+import java.io.IOException;
+
+/**
+ * API加/解密过滤器
+ *
+ * @author lishuyan
+ * @since 2.14.0
+ */
+public class ApiCryptoFilter implements Filter {
+
+ /**
+ * API加/密配置
+ */
+ private final ApiCryptoProperties properties;
+
+ public ApiCryptoFilter(ApiCryptoProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest,
+ ServletResponse servletResponse,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest request = (HttpServletRequest)servletRequest;
+ HttpServletResponse response = (HttpServletResponse)servletResponse;
+ // 获取API加密注解
+ ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(request);
+ // 响应加密标识
+ boolean responseEncryptFlag = ObjectUtil.isNotNull(apiEncrypt) && apiEncrypt.response();
+ // 密钥标头
+ String secretKeyHeader = properties.getSecretKeyHeader();
+ ServletRequest requestWrapper = null;
+ ServletResponse responseWrapper = null;
+ ResponseBodyEncryptWrapper responseBodyEncryptWrapper = null;
+ // 是否为 PUT 或者 POST 请求
+ if (HttpMethod.PUT.matches(request.getMethod()) || HttpMethod.POST.matches(request.getMethod())) {
+ // 获取密钥值
+ String secretKeyValue = request.getHeader(secretKeyHeader);
+ if (CharSequenceUtil.isNotBlank(secretKeyValue)) {
+ // 请求解密
+ requestWrapper = new RequestBodyDecryptWrapper(request, properties.getPrivateKey(), secretKeyHeader);
+ }
+ }
+ // 响应加密,响应包装器替换响应体加密包装器
+ if (responseEncryptFlag) {
+ responseBodyEncryptWrapper = new ResponseBodyEncryptWrapper(response);
+ responseWrapper = responseBodyEncryptWrapper;
+ }
+ // 继续执行
+ chain.doFilter(ObjectUtil.defaultIfNull(requestWrapper, request), ObjectUtil
+ .defaultIfNull(responseWrapper, response));
+ // 响应加密,执行完成后,响应密文
+ if (responseEncryptFlag) {
+ servletResponse.reset();
+ // 获取密文
+ String encryptContent = responseBodyEncryptWrapper.getEncryptContent(response, properties
+ .getPublicKey(), secretKeyHeader);
+ // 写出密文
+ servletResponse.getWriter().write(encryptContent);
+ }
+ }
+
+ /**
+ * 获取 ApiEncrypt 注解
+ *
+ * @param request HTTP请求
+ * @return ApiEncrypt注解,如果未找到则返回null
+ */
+ private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest request) {
+ try {
+ RequestMappingHandlerMapping handlerMapping = SpringUtil
+ .getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
+ HandlerExecutionChain mappingHandler = handlerMapping.getHandler(request);
+ // 检查是否存在处理链
+ if (ObjectUtil.isNull(mappingHandler)) {
+ return null;
+ }
+ // 获取处理器
+ Object handler = mappingHandler.getHandler();
+ // 检查是否为HandlerMethod类型
+ if (!(handler instanceof HandlerMethod handlerMethod)) {
+ return null;
+ }
+ // 获取方法上的ApiEncrypt注解
+ return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/RequestBodyDecryptWrapper.java b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/RequestBodyDecryptWrapper.java
new file mode 100644
index 00000000..8e1210f2
--- /dev/null
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/RequestBodyDecryptWrapper.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.security.crypto.filter;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.CharsetUtil;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import org.springframework.http.MediaType;
+import top.continew.starter.security.crypto.util.EncryptHelper;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 请求体解密包装类
+ *
+ * @author lishuyan
+ * @since 2.14.0
+ */
+public class RequestBodyDecryptWrapper extends HttpServletRequestWrapper {
+
+ private final byte[] body;
+
+ public RequestBodyDecryptWrapper(HttpServletRequest request,
+ String privateKey,
+ String secretKeyHeader) throws IOException {
+ super(request);
+ this.body = getDecryptContent(request, privateKey, secretKeyHeader);
+ }
+
+ /**
+ * 获取解密后的请求体
+ *
+ * @param request 请求对象
+ * @param privateKey RSA私钥
+ * @param secretKeyHeader 密钥头
+ * @return 解密后的请求体
+ * @throws IOException /
+ */
+ public byte[] getDecryptContent(HttpServletRequest request,
+ String privateKey,
+ String secretKeyHeader) throws IOException {
+ // 通过 请求头 获取 AES 密钥,密钥内容经过 RSA 加密
+ String secretKeyByRsa = request.getHeader(secretKeyHeader);
+ // 通过 RSA 解密,获取 AES 密钥,密钥内容经过 Base64 编码
+ String secretKeyByBase64 = EncryptHelper.decryptByRsa(secretKeyByRsa, privateKey);
+ // 通过 Base64 解码,获取 AES 密钥
+ String aesSecretKey = EncryptHelper.decodeByBase64(secretKeyByBase64);
+ request.setCharacterEncoding(CharsetUtil.UTF_8);
+ byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
+ String requestBody = new String(readBytes, StandardCharsets.UTF_8);
+ // 通过 AES 密钥,解密 请求体
+ return EncryptHelper.decryptByAes(requestBody, aesSecretKey).getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+ @Override
+ public int getContentLength() {
+ return body.length;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ return body.length;
+ }
+
+ @Override
+ public String getContentType() {
+ return MediaType.APPLICATION_JSON_VALUE;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() {
+ final 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) {
+
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ResponseBodyEncryptWrapper.java b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ResponseBodyEncryptWrapper.java
new file mode 100644
index 00000000..2e4755ff
--- /dev/null
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/filter/ResponseBodyEncryptWrapper.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.security.crypto.filter;
+
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.RandomUtil;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
+import org.springframework.http.HttpHeaders;
+import top.continew.starter.core.constant.StringConstants;
+import top.continew.starter.security.crypto.util.EncryptHelper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+/**
+ * 响应体加密包装类
+ *
+ * @author lishuyan
+ * @since 2.14.0
+ */
+public class ResponseBodyEncryptWrapper extends HttpServletResponseWrapper {
+
+ private final ByteArrayOutputStream byteArrayOutputStream;
+ private final ServletOutputStream servletOutputStream;
+ private final PrintWriter printWriter;
+
+ public ResponseBodyEncryptWrapper(HttpServletResponse response) throws IOException {
+ super(response);
+ this.byteArrayOutputStream = new ByteArrayOutputStream();
+ this.servletOutputStream = this.getOutputStream();
+ this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
+ }
+
+ @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();
+ }
+
+ public byte[] getResponseData() throws IOException {
+ flushBuffer();
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ public String getContent() throws IOException {
+ flushBuffer();
+ return byteArrayOutputStream.toString();
+ }
+
+ /**
+ * 获取加密内容
+ *
+ * @param response 响应对象
+ * @param publicKey RSA公钥
+ * @param secretKeyHeader 密钥头
+ * @return 加密内容
+ */
+ public String getEncryptContent(HttpServletResponse response,
+ String publicKey,
+ String secretKeyHeader) throws IOException {
+ // 生成 AES 密钥
+ String aesSecretKey = RandomUtil.randomString(32);
+ // Base64 编码
+ String secretKeyByBase64 = EncryptHelper.encodeByBase64(aesSecretKey);
+ // RSA 加密
+ String secretKeyByRsa = EncryptHelper.encryptByRsa(secretKeyByBase64, publicKey);
+ // 设置响应头
+ response.addHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, secretKeyHeader);
+ response.setHeader(secretKeyHeader, secretKeyByRsa);
+ response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, StringConstants.ASTERISK);
+ response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, StringConstants.ASTERISK);
+ response.setCharacterEncoding(CharsetUtil.UTF_8);
+ // 通过 AES 密钥,对原始内容进行加密
+ return EncryptHelper.encryptByAes(this.getContent(), aesSecretKey);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ return new ServletOutputStream() {
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ byteArrayOutputStream.write(b, off, len);
+ }
+ };
+ }
+
+}
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/util/EncryptHelper.java b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/util/EncryptHelper.java
index 546ba89a..d891726d 100644
--- a/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/util/EncryptHelper.java
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/java/top/continew/starter/security/crypto/util/EncryptHelper.java
@@ -16,8 +16,13 @@
package top.continew.starter.security.crypto.util;
+import cn.hutool.core.codec.Base64;
import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
@@ -27,6 +32,7 @@ import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -217,4 +223,98 @@ public class EncryptHelper {
cryptoContext.setPublicKey(defaultProperties.getPublicKey());
return cryptoContext;
}
+
+ /**
+ * Base64编码
+ *
+ * @param data 待编码数据
+ * @return 编码后字符串
+ * @since 2.14.0
+ */
+ public static String encodeByBase64(String data) {
+ return Base64.encode(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Base64解码
+ *
+ * @param data 待解码数据
+ * @return 解码后字符串
+ * @since 2.14.0
+ */
+ public static String decodeByBase64(String data) {
+ return Base64.decodeStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES加密
+ *
+ * @param data 待加密数据
+ * @param password 秘钥字符串
+ * @return 加密后字符串, 采用Base64编码
+ * @since 2.14.0
+ */
+ public static String encryptByAes(String data, String password) {
+ if (CharSequenceUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES需要传入秘钥信息");
+ }
+ // AES算法的秘钥要求是16位、24位、32位
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES解密
+ *
+ * @param data 待解密数据
+ * @param password 秘钥字符串
+ * @return 解密后字符串
+ * @since 2.14.0
+ */
+ public static String decryptByAes(String data, String password) {
+ if (CharSequenceUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES需要传入秘钥信息");
+ }
+ // AES算法的秘钥要求是16位、24位、32位
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * RSA公钥加密
+ *
+ * @param data 待加密数据
+ * @param publicKey 公钥
+ * @return 加密后字符串, 采用Base64编码
+ * @since 2.14.0
+ */
+ public static String encryptByRsa(String data, String publicKey) {
+ if (CharSequenceUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("RSA需要传入公钥进行加密");
+ }
+ RSA rsa = SecureUtil.rsa(null, publicKey);
+ return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * RSA私钥解密
+ *
+ * @param data 待解密数据
+ * @param privateKey 私钥
+ * @return 解密后字符串
+ * @since 2.14.0
+ */
+ public static String decryptByRsa(String data, String privateKey) {
+ if (CharSequenceUtil.isBlank(privateKey)) {
+ throw new IllegalArgumentException("RSA需要传入私钥进行解密");
+ }
+ RSA rsa = SecureUtil.rsa(privateKey, null);
+ return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+ }
}
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/continew-starter-security/continew-starter-security-crypto/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 8ec4466c..f8fe1748 100644
--- a/continew-starter-security/continew-starter-security-crypto/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1 +1,2 @@
-top.continew.starter.security.crypto.autoconfigure.CryptoAutoConfiguration
\ No newline at end of file
+top.continew.starter.security.crypto.autoconfigure.CryptoAutoConfiguration
+top.continew.starter.security.crypto.autoconfigure.ApiCryptoAutoConfiguration
\ No newline at end of file
diff --git a/continew-starter-security/continew-starter-security-crypto/src/main/resources/default-api-crypto.yml b/continew-starter-security/continew-starter-security-crypto/src/main/resources/default-api-crypto.yml
new file mode 100644
index 00000000..31ecc79d
--- /dev/null
+++ b/continew-starter-security/continew-starter-security-crypto/src/main/resources/default-api-crypto.yml
@@ -0,0 +1,6 @@
+--- ### 安全配置:API加/解密配置
+continew-starter.security:
+ api-crypto:
+ enabled: true
+ # 请求头中 AES 密钥 键名
+ secretKeyHeader: X-Api-Crypto
\ No newline at end of file