refactor(encrypt): 拆分字段加密、API 加密模块

This commit is contained in:
2025-08-20 21:44:40 +08:00
parent e5002b8bfc
commit e9bf92ea1f
46 changed files with 525 additions and 343 deletions

View File

@@ -0,0 +1,25 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter-encrypt</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-encrypt-api</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 加密模块 - API 加密</description>
<dependencies>
<!-- 加密模块 - 核心模块 -->
<dependency>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter-encrypt-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.encrypt.api.annotation;
import java.lang.annotation.*;
/**
* API 加密注解
*
* @author lishuyan
* @since 2.14.0
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
/**
* 是否加密响应
*/
boolean response() default true;
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.encrypt.api.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 top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.encrypt.api.filter.ApiEncryptFilter;
/**
* API 加密自动配置
*
* @author lishuyan
* @author Charles7c
* @since 2.14.0
*/
@AutoConfiguration
@EnableConfigurationProperties(ApiEncryptProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.ENCRYPT_API, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class ApiEncryptAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(ApiEncryptAutoConfiguration.class);
/**
* API 加密过滤器
*/
@Bean
public FilterRegistrationBean<ApiEncryptFilter> apiEncryptFilter(ApiEncryptProperties properties) {
FilterRegistrationBean<ApiEncryptFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ApiEncryptFilter(properties));
registrationBean.setOrder(OrderedConstants.Filter.API_ENCRYPT_FILTER);
registrationBean.addUrlPatterns(StringConstants.PATH_PATTERN_CURRENT_DIR);
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return registrationBean;
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Encrypt-API' completed initialization.");
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.encrypt.api.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* API 加密配置属性
*
* @author lishuyan
* @since 2.14.0
*/
@ConfigurationProperties(PropertiesConstants.ENCRYPT_API)
public class ApiEncryptProperties {
/**
* 是否启用
*/
private Boolean enabled;
/**
* 请求头中 AES 密钥 键名
*/
private String secretKeyHeader = "X-Api-Encrypt";
/**
* 响应加密公钥
*/
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;
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.encrypt.api.filter;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import top.continew.starter.core.util.SpringWebUtils;
import top.continew.starter.encrypt.api.annotation.ApiEncrypt;
import top.continew.starter.encrypt.api.autoconfigure.ApiEncryptProperties;
import java.io.IOException;
import java.util.Optional;
/**
* API 加密过滤器
*
* @author lishuyan
* @author Charles7c
* @since 2.14.0
*/
public class ApiEncryptFilter implements Filter {
private final ApiEncryptProperties properties;
public ApiEncryptFilter(ApiEncryptProperties 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;
// 是否加密响应
boolean isResponseEncrypt = this.isResponseEncrypt(request);
// 密钥标头
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 (isResponseEncrypt) {
responseBodyEncryptWrapper = new ResponseBodyEncryptWrapper(response);
responseWrapper = responseBodyEncryptWrapper;
}
// 继续执行
chain.doFilter(ObjectUtil.defaultIfNull(requestWrapper, request), ObjectUtil
.defaultIfNull(responseWrapper, response));
// 响应加密,执行完成后,响应密文
if (isResponseEncrypt) {
servletResponse.reset();
// 获取密文
String encryptContent = responseBodyEncryptWrapper.getEncryptContent(response, properties
.getPublicKey(), secretKeyHeader);
// 写出密文
servletResponse.getWriter().write(encryptContent);
}
}
/**
* 是否加密响应
*
* @param request 请求对象
* @return 是否加密响应
*/
private boolean isResponseEncrypt(HttpServletRequest request) {
// 获取 API 加密注解
ApiEncrypt apiEncrypt = Optional.ofNullable(SpringWebUtils.getHandlerMethod(request))
.map(h -> h.getMethodAnnotation(ApiEncrypt.class))
.orElse(null);
return apiEncrypt != null && apiEncrypt.response();
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.encrypt.api.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.encrypt.util.EncryptUtils;
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 = EncryptUtils.decryptByRsa(secretKeyByRsa, privateKey);
// 通过 Base64 解码,获取 AES 密钥
String aesSecretKey = EncryptUtils.decodeByBase64(secretKeyByBase64);
request.setCharacterEncoding(CharsetUtil.UTF_8);
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 通过 AES 密钥,解密 请求体
return EncryptUtils.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) {
}
};
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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.encrypt.api.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.encrypt.util.EncryptUtils;
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 = EncryptUtils.encodeByBase64(aesSecretKey);
// RSA 加密
String secretKeyByRsa = EncryptUtils.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 EncryptUtils.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);
}
};
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.encrypt.api.autoconfigure.ApiEncryptAutoConfiguration