【同步】BOOT 和 CLOUD 的功能
							parent
							
								
									66824310c1
								
							
						
					
					
						commit
						b4df6f93cb
					
				|  | @ -17,6 +17,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 前面
 | ||||
|  |  | |||
|  | @ -25,16 +25,16 @@ public class CommonResult<T> implements Serializable { | |||
|      * @see ErrorCode#getCode() | ||||
|      */ | ||||
|     private Integer code; | ||||
|     /** | ||||
|      * 返回数据 | ||||
|      */ | ||||
|     private T data; | ||||
|     /** | ||||
|      * 错误提示,用户可阅读 | ||||
|      * | ||||
|      * @see ErrorCode#getMsg() () | ||||
|      */ | ||||
|     private String msg; | ||||
|     /** | ||||
|      * 返回数据 | ||||
|      */ | ||||
|     private T data; | ||||
| 
 | ||||
|     /** | ||||
|      * 将传入的 result 对象,转换成另外一个泛型结果的对象 | ||||
|  |  | |||
|  | @ -11,12 +11,12 @@ import java.util.List; | |||
| @Data | ||||
| public final class PageResult<T> implements Serializable { | ||||
| 
 | ||||
|     @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) | ||||
|     private List<T> list; | ||||
| 
 | ||||
|     @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) | ||||
|     private Long total; | ||||
| 
 | ||||
|     @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) | ||||
|     private List<T> list; | ||||
| 
 | ||||
|     public PageResult() { | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 <a href="https://help.aliyun.com/zh/ssl-certificate/what-are-a-public-key-and-a-private-key">什么是公钥和私钥?</a> | ||||
|      */ | ||||
|     @NotEmpty(message = "对称加密算法不能为空") | ||||
|     private String algorithm; | ||||
| 
 | ||||
|     /** | ||||
|      * 请求的解密密钥 | ||||
|      * | ||||
|      * 注意: | ||||
|      * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 | ||||
|      * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!) | ||||
|      */ | ||||
|     @NotEmpty(message = "请求的解密密钥不能为空") | ||||
|     private String requestKey; | ||||
| 
 | ||||
|     /** | ||||
|      * 响应的加密密钥 | ||||
|      * | ||||
|      * 注意: | ||||
|      * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 | ||||
|      * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!) | ||||
|      */ | ||||
|     @NotEmpty(message = "响应的加密密钥不能为空") | ||||
|     private String responseKey; | ||||
| 
 | ||||
| } | ||||
|  | @ -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> apiEncryptFilter(WebProperties webProperties, | ||||
|                                                                      ApiEncryptProperties apiEncryptProperties, | ||||
|                                                                      RequestMappingHandlerMapping requestMappingHandlerMapping, | ||||
|                                                                      GlobalExceptionHandler globalExceptionHandler) { | ||||
|         ApiEncryptFilter filter = new ApiEncryptFilter(webProperties, apiEncryptProperties, | ||||
|                 requestMappingHandlerMapping, globalExceptionHandler); | ||||
|         return createFilterBean(filter, WebFilterOrderEnum.API_ENCRYPT_FILTER); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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; | ||||
| 
 | ||||
| } | ||||
|  | @ -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) { | ||||
|             } | ||||
| 
 | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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); | ||||
|             } | ||||
| 
 | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,4 @@ | |||
| /** | ||||
|  * HTTP API 加密组件:支持 Request 和 Response 的加密、解密 | ||||
|  */ | ||||
| package cn.iocoder.yudao.framework.encrypt; | ||||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -4,4 +4,5 @@ cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration | |||
| cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration | ||||
| cn.iocoder.yudao.framework.apilog.config.YudaoApiLogRpcAutoConfiguration | ||||
| cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration | ||||
| cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration | ||||
| cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration | ||||
| cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration | ||||
|  | @ -14,7 +14,7 @@ public interface LogRecordConstants { | |||
|     String CRM_CLUE_CREATE_SUB_TYPE = "创建线索"; | ||||
|     String CRM_CLUE_CREATE_SUCCESS = "创建了线索{{#clue.name}}"; | ||||
|     String CRM_CLUE_UPDATE_SUB_TYPE = "更新线索"; | ||||
|     String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}"; | ||||
|     String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReqVO}}"; | ||||
|     String CRM_CLUE_DELETE_SUB_TYPE = "删除线索"; | ||||
|     String CRM_CLUE_DELETE_SUCCESS = "删除了线索【{{#clueName}}】"; | ||||
|     String CRM_CLUE_TRANSFER_SUB_TYPE = "转移线索"; | ||||
|  |  | |||
|  | @ -275,6 +275,10 @@ public class MenuServiceImpl implements MenuService { | |||
|         if (menu == null) { | ||||
|             return; | ||||
|         } | ||||
|         // 如果 id 为空,说明不用比较是否为相同 id 的菜单
 | ||||
|         if (id == null) { | ||||
|             return; | ||||
|         } | ||||
|         if (!menu.getId().equals(id)) { | ||||
|             throw exception(MENU_COMPONENT_NAME_DUPLICATE); | ||||
|         } | ||||
|  |  | |||
|  | @ -158,6 +158,13 @@ yudao: | |||
|     enable: false | ||||
|     exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 | ||||
|       - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 | ||||
|   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 案例】响应加密的公钥 | ||||
|   swagger: | ||||
|     title: 管理后台 | ||||
|     description: 提供管理员管理的所有功能 | ||||
|  |  | |||
|  | @ -26,7 +26,6 @@ import jakarta.validation.Validation; | |||
| import jakarta.validation.Validator; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.springframework.boot.test.mock.mockito.MockBean; | ||||
| import org.springframework.context.annotation.Import; | ||||
| import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||||
| 
 | ||||
|  |  | |||
|  | @ -105,12 +105,40 @@ public class DeptServiceImplTest extends BaseDbUnitTest { | |||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testValidateDeptExists_notFound() { | ||||
|     public void testDeleteDeptList_success() { | ||||
|         // mock 数据
 | ||||
|         DeptDO deptDO1 = randomPojo(DeptDO.class); | ||||
|         deptMapper.insert(deptDO1); | ||||
|         DeptDO deptDO2 = randomPojo(DeptDO.class); | ||||
|         deptMapper.insert(deptDO2); | ||||
|         // 准备参数
 | ||||
|         Long id = randomLongId(); | ||||
|         List<Long> ids = Arrays.asList(deptDO1.getId(), deptDO2.getId()); | ||||
| 
 | ||||
|         // 调用
 | ||||
|         deptService.deleteDeptList(ids); | ||||
|         // 校验数据不存在了
 | ||||
|         assertNull(deptMapper.selectById(deptDO1.getId())); | ||||
|         assertNull(deptMapper.selectById(deptDO2.getId())); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testDeleteDeptList_exitsChildren() { | ||||
|         // mock 数据
 | ||||
|         DeptDO parentDept = randomPojo(DeptDO.class); | ||||
|         deptMapper.insert(parentDept); | ||||
|         DeptDO childrenDeptDO = randomPojo(DeptDO.class, o -> { | ||||
|             o.setParentId(parentDept.getId()); | ||||
|             o.setStatus(randomCommonStatus()); | ||||
|         }); | ||||
|         deptMapper.insert(childrenDeptDO); | ||||
|         DeptDO anotherDept = randomPojo(DeptDO.class); | ||||
|         deptMapper.insert(anotherDept); | ||||
| 
 | ||||
|         // 准备参数(包含有子部门的 parentDept)
 | ||||
|         List<Long> ids = Arrays.asList(parentDept.getId(), anotherDept.getId()); | ||||
| 
 | ||||
|         // 调用, 并断言异常
 | ||||
|         assertServiceException(() -> deptService.validateDeptExists(id), DEPT_NOT_FOUND); | ||||
|         assertServiceException(() -> deptService.deleteDeptList(ids), DEPT_EXITS_CHILDREN); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  |  | |||
|  | @ -33,11 +33,11 @@ | |||
|         </dependency> | ||||
| 
 | ||||
|         <!-- 会员中心。默认注释,保证编译速度 --> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-member-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-member-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <!-- 数据报表。默认注释,保证编译速度 --> | ||||
| <!--        <dependency>--> | ||||
|  | @ -46,17 +46,17 @@ | |||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
|         <!-- 工作流。默认注释,保证编译速度 --> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-bpm-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-bpm-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
|         <!-- 支付服务。默认注释,保证编译速度 --> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-pay-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-pay-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <!-- 微信公众号模块。默认注释,保证编译速度 --> | ||||
| <!--        <dependency>--> | ||||
|  | @ -66,26 +66,26 @@ | |||
| <!--        </dependency>--> | ||||
| 
 | ||||
|         <!-- 商城相关模块。默认注释,保证编译速度--> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-product-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-promotion-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-trade-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>cn.iocoder.cloud</groupId>--> | ||||
| <!--            <artifactId>yudao-module-statistics-server</artifactId>--> | ||||
| <!--            <version>${revision}</version>--> | ||||
| <!--        </dependency>--> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-product-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-promotion-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-trade-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.cloud</groupId> | ||||
|             <artifactId>yudao-module-statistics-server</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <!-- CRM 相关模块。默认注释,保证编译速度 --> | ||||
| <!--        <dependency>--> | ||||
|  |  | |||
|  | @ -269,6 +269,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 # 路径 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 YunaiV
						YunaiV