mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-12 05:01:41 +08:00
feat(security/crypto): 新增 API 加/解密功能
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!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。 --> <!-- 在 [] 中输入 x 来勾选) --> ## PR 类型 <!-- 您的 PR 引入了哪种类型的变更? --> <!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 --> - [x] 新 feature - [ ] Bug 修复 - [ ] 功能增强 - [ ] 文档变更 - [ ] 代码样式变更 - [ ] 重构 - [ ] 性能改进 - [ ] 单元测试 - [ ] CI/CD - [ ] 其他 ## PR 目的 <!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 --> 1、feat:✨ 新增 API 加/解密功能。 2、支持PUT/POST请求方法且JSON类型的请求解密。 3、支持响应加密。 ## 解决方案 <!-- 详细描述您是如何解决的问题 --> 1、新增加解密工具类 2、新增API加解密过滤器。 3、新增API加解密自动配置。 ## PR 测试 <!-- 如果可以,请为您的 PR 添加或更新单元测试。 --> <!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 --> 请求加密和解密:  响应加密和解密:  ## Changelog | 模块 | Changelog | Related issues | | -------------------------------- | ------------------------------------------------------------ | -------------- | | continew-starter-security-crypto | feat:✨ 新增 API 加/解密功能。<br/><br/>- 新增 API 加密注解和相关配置<br/>- 实现请求体解密和响应体加密的过滤器<br/>- 添加必要的工具类和属性配置 | | <!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 --> <!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 --> ## 其他信息 <!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 --> ## 提交前确认 - [x] PR 代码经过了完整测试,并且通过了代码规范检查 - [x] 已经完整填写 Changelog,并链接到了相关 issues - [x] PR 代码将要提交到 dev 分支 See merge request: continew/continew-starter!3
This commit is contained in:
@@ -31,6 +31,11 @@ public class OrderedConstants {
|
|||||||
*/
|
*/
|
||||||
public static final class Filter {
|
public static final class Filter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API加/密过滤器顺序
|
||||||
|
*/
|
||||||
|
public static final int API_CRYPTO_FILTER = Ordered.HIGHEST_PRECEDENCE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 链路追踪过滤器顺序
|
* 链路追踪过滤器顺序
|
||||||
*/
|
*/
|
||||||
|
@@ -55,10 +55,15 @@ public class PropertiesConstants {
|
|||||||
public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security";
|
public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全-加/解密配置
|
* 安全-数据加/解密配置
|
||||||
*/
|
*/
|
||||||
public static final String SECURITY_CRYPTO = SECURITY + StringConstants.DOT + "crypto";
|
public static final String SECURITY_CRYPTO = SECURITY + StringConstants.DOT + "crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全-API加/解密配置
|
||||||
|
*/
|
||||||
|
public static final String SECURITY_API_CRYPTO = SECURITY + StringConstants.DOT + "api-crypto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全-敏感词配置
|
* 安全-敏感词配置
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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.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;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.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<ApiCryptoFilter> apiCryptoFilterRegistration(ApiCryptoProperties properties) {
|
||||||
|
FilterRegistrationBean<ApiCryptoFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||||
|
registration.setFilter(new ApiCryptoFilter(properties));
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setName("apiCryptoFilter");
|
||||||
|
registration.setOrder(OrderedConstants.Filter.API_CRYPTO_FILTER);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void postConstruct() {
|
||||||
|
log.debug("[ContiNew Starter] - Auto Configuration 'Security-API-Crypto' completed initialization.");
|
||||||
|
}
|
||||||
|
}
|
@@ -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.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;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* 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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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.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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -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.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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -16,8 +16,13 @@
|
|||||||
|
|
||||||
package top.continew.starter.security.crypto.util;
|
package top.continew.starter.security.crypto.util;
|
||||||
|
|
||||||
|
import cn.hutool.core.codec.Base64;
|
||||||
import cn.hutool.core.text.CharSequenceUtil;
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.ReflectUtil;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
|
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 top.continew.starter.security.crypto.enums.Algorithm;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -217,4 +223,98 @@ public class EncryptHelper {
|
|||||||
cryptoContext.setPublicKey(defaultProperties.getPublicKey());
|
cryptoContext.setPublicKey(defaultProperties.getPublicKey());
|
||||||
return cryptoContext;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1,2 @@
|
|||||||
top.continew.starter.security.crypto.autoconfigure.CryptoAutoConfiguration
|
top.continew.starter.security.crypto.autoconfigure.CryptoAutoConfiguration
|
||||||
|
top.continew.starter.security.crypto.autoconfigure.ApiCryptoAutoConfiguration
|
@@ -0,0 +1,6 @@
|
|||||||
|
--- ### 安全配置:API加/解密配置
|
||||||
|
continew-starter.security:
|
||||||
|
api-crypto:
|
||||||
|
enabled: true
|
||||||
|
# 请求头中 AES 密钥 键名
|
||||||
|
secretKeyHeader: X-Api-Crypto
|
Reference in New Issue
Block a user