Commit 2cad35f3610a99566876c5bc35c94de8041af9ec
1 parent
be39a263
feat:【framework 框架】增加 api 加解密能力
Showing
13 changed files
with
606 additions
and
5 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
| ... | ... | @@ -15,6 +15,8 @@ public interface WebFilterOrderEnum { |
| 15 | 15 | |
| 16 | 16 | int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; |
| 17 | 17 | |
| 18 | + int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1; | |
| 19 | + | |
| 18 | 20 | // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 |
| 19 | 21 | |
| 20 | 22 | int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.config; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.NotEmpty; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | +import org.springframework.boot.context.properties.ConfigurationProperties; | |
| 7 | +import org.springframework.validation.annotation.Validated; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * HTTP API 加解密配置 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@ConfigurationProperties(prefix = "yudao.api-encrypt") | |
| 15 | +@Validated | |
| 16 | +@Data | |
| 17 | +public class ApiEncryptProperties { | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * 是否开启 | |
| 21 | + */ | |
| 22 | + @NotNull(message = "是否开启不能为空") | |
| 23 | + private Boolean enable; | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 请求头(响应头)名称 | |
| 27 | + * | |
| 28 | + * 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密 | |
| 29 | + * 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密 | |
| 30 | + */ | |
| 31 | + @NotEmpty(message = "请求头(响应头)名称不能为空") | |
| 32 | + private String header = "X-Api-Encrypt"; | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 对称加密算法,用于请求/响应的加解密 | |
| 36 | + * | |
| 37 | + * 目前支持 | |
| 38 | + * 【对称加密】: | |
| 39 | + * 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES} | |
| 40 | + * 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低) | |
| 41 | + * 【非对称加密】 | |
| 42 | + * 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA} | |
| 43 | + * 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低) | |
| 44 | + * | |
| 45 | + * @see <a href="https://help.aliyun.com/zh/ssl-certificate/what-are-a-public-key-and-a-private-key">什么是公钥和私钥?</a> | |
| 46 | + */ | |
| 47 | + @NotEmpty(message = "对称加密算法不能为空") | |
| 48 | + private String algorithm; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 请求的解密密钥 | |
| 52 | + * | |
| 53 | + * 注意: | |
| 54 | + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 | |
| 55 | + * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!) | |
| 56 | + */ | |
| 57 | + @NotEmpty(message = "请求的解密密钥不能为空") | |
| 58 | + private String requestKey; | |
| 59 | + | |
| 60 | + /** | |
| 61 | + * 响应的加密密钥 | |
| 62 | + * | |
| 63 | + * 注意: | |
| 64 | + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 | |
| 65 | + * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!) | |
| 66 | + */ | |
| 67 | + @NotEmpty(message = "响应的加密密钥不能为空") | |
| 68 | + private String responseKey; | |
| 69 | + | |
| 70 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.config; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; | |
| 4 | +import cn.iocoder.yudao.framework.encrypt.core.filter.ApiEncryptFilter; | |
| 5 | +import cn.iocoder.yudao.framework.web.config.WebProperties; | |
| 6 | +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | +import org.springframework.boot.autoconfigure.AutoConfiguration; | |
| 9 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
| 10 | +import org.springframework.boot.context.properties.EnableConfigurationProperties; | |
| 11 | +import org.springframework.boot.web.servlet.FilterRegistrationBean; | |
| 12 | +import org.springframework.context.annotation.Bean; | |
| 13 | +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean; | |
| 16 | + | |
| 17 | +@AutoConfiguration | |
| 18 | +@Slf4j | |
| 19 | +@EnableConfigurationProperties(ApiEncryptProperties.class) | |
| 20 | +@ConditionalOnProperty(prefix = "yudao.api-encrypt", name = "enable", havingValue = "true") | |
| 21 | +public class YudaoApiEncryptAutoConfiguration { | |
| 22 | + | |
| 23 | + @Bean | |
| 24 | + public FilterRegistrationBean<ApiEncryptFilter> apiEncryptFilter(WebProperties webProperties, | |
| 25 | + ApiEncryptProperties apiEncryptProperties, | |
| 26 | + RequestMappingHandlerMapping requestMappingHandlerMapping, | |
| 27 | + GlobalExceptionHandler globalExceptionHandler) { | |
| 28 | + ApiEncryptFilter filter = new ApiEncryptFilter(webProperties, apiEncryptProperties, | |
| 29 | + requestMappingHandlerMapping, globalExceptionHandler); | |
| 30 | + return createFilterBean(filter, WebFilterOrderEnum.API_ENCRYPT_FILTER); | |
| 31 | + | |
| 32 | + } | |
| 33 | + | |
| 34 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.annotation; | |
| 2 | + | |
| 3 | +import java.lang.annotation.*; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * HTTP API 加解密注解 | |
| 7 | + */ | |
| 8 | +@Documented | |
| 9 | +@Target({ElementType.TYPE, ElementType.METHOD}) | |
| 10 | +@Retention(RetentionPolicy.RUNTIME) | |
| 11 | +public @interface ApiEncrypt { | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * 是否对请求参数进行解密,默认 true | |
| 15 | + */ | |
| 16 | + boolean request() default true; | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * 是否对响应结果进行加密,默认 true | |
| 20 | + */ | |
| 21 | + boolean response() default true; | |
| 22 | + | |
| 23 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.filter; | |
| 2 | + | |
| 3 | +import cn.hutool.core.io.IoUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; | |
| 6 | +import cn.hutool.crypto.asymmetric.KeyType; | |
| 7 | +import cn.hutool.crypto.symmetric.SymmetricDecryptor; | |
| 8 | +import jakarta.servlet.ReadListener; | |
| 9 | +import jakarta.servlet.ServletInputStream; | |
| 10 | +import jakarta.servlet.http.HttpServletRequest; | |
| 11 | +import jakarta.servlet.http.HttpServletRequestWrapper; | |
| 12 | + | |
| 13 | +import java.io.BufferedReader; | |
| 14 | +import java.io.ByteArrayInputStream; | |
| 15 | +import java.io.IOException; | |
| 16 | +import java.io.InputStreamReader; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * 解密请求 {@link HttpServletRequestWrapper} 实现类 | |
| 20 | + * | |
| 21 | + * @author 芋道源码 | |
| 22 | + */ | |
| 23 | +public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { | |
| 24 | + | |
| 25 | + private final byte[] body; | |
| 26 | + | |
| 27 | + public ApiDecryptRequestWrapper(HttpServletRequest request, | |
| 28 | + SymmetricDecryptor symmetricDecryptor, | |
| 29 | + AsymmetricDecryptor asymmetricDecryptor) throws IOException { | |
| 30 | + super(request); | |
| 31 | + // 读取 body,允许 HEX、BASE64 传输 | |
| 32 | + String requestBody = StrUtil.utf8Str( | |
| 33 | + IoUtil.readBytes(request.getInputStream(), false)); | |
| 34 | + | |
| 35 | + // 解密 body | |
| 36 | + body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody) | |
| 37 | + : asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey); | |
| 38 | + } | |
| 39 | + | |
| 40 | + @Override | |
| 41 | + public BufferedReader getReader() { | |
| 42 | + return new BufferedReader(new InputStreamReader(this.getInputStream())); | |
| 43 | + } | |
| 44 | + | |
| 45 | + @Override | |
| 46 | + public int getContentLength() { | |
| 47 | + return body.length; | |
| 48 | + } | |
| 49 | + | |
| 50 | + @Override | |
| 51 | + public long getContentLengthLong() { | |
| 52 | + return body.length; | |
| 53 | + } | |
| 54 | + | |
| 55 | + @Override | |
| 56 | + public ServletInputStream getInputStream() { | |
| 57 | + ByteArrayInputStream stream = new ByteArrayInputStream(body); | |
| 58 | + return new ServletInputStream() { | |
| 59 | + | |
| 60 | + @Override | |
| 61 | + public int read() { | |
| 62 | + return stream.read(); | |
| 63 | + } | |
| 64 | + | |
| 65 | + @Override | |
| 66 | + public int available() { | |
| 67 | + return body.length; | |
| 68 | + } | |
| 69 | + | |
| 70 | + @Override | |
| 71 | + public boolean isFinished() { | |
| 72 | + return false; | |
| 73 | + } | |
| 74 | + | |
| 75 | + @Override | |
| 76 | + public boolean isReady() { | |
| 77 | + return false; | |
| 78 | + } | |
| 79 | + | |
| 80 | + @Override | |
| 81 | + public void setReadListener(ReadListener readListener) { | |
| 82 | + } | |
| 83 | + | |
| 84 | + }; | |
| 85 | + } | |
| 86 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.filter; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.hutool.crypto.SecureUtil; | |
| 5 | +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; | |
| 6 | +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; | |
| 7 | +import cn.hutool.crypto.symmetric.SymmetricDecryptor; | |
| 8 | +import cn.hutool.crypto.symmetric.SymmetricEncryptor; | |
| 9 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 10 | +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | |
| 11 | +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | |
| 12 | +import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties; | |
| 13 | +import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiEncrypt; | |
| 14 | +import cn.iocoder.yudao.framework.web.config.WebProperties; | |
| 15 | +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; | |
| 16 | +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; | |
| 17 | +import jakarta.servlet.FilterChain; | |
| 18 | +import jakarta.servlet.ServletException; | |
| 19 | +import jakarta.servlet.http.HttpServletRequest; | |
| 20 | +import jakarta.servlet.http.HttpServletResponse; | |
| 21 | +import lombok.extern.slf4j.Slf4j; | |
| 22 | +import org.springframework.http.HttpMethod; | |
| 23 | +import org.springframework.web.method.HandlerMethod; | |
| 24 | +import org.springframework.web.servlet.HandlerExecutionChain; | |
| 25 | +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; | |
| 26 | + | |
| 27 | +import java.io.IOException; | |
| 28 | + | |
| 29 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 30 | + | |
| 31 | +/** | |
| 32 | + * API 加密过滤器,处理 {@link ApiEncrypt} 注解。 | |
| 33 | + * | |
| 34 | + * 1. 解密请求参数 | |
| 35 | + * 2. 加密响应结果 | |
| 36 | + * | |
| 37 | + * 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢? | |
| 38 | + * 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!! | |
| 39 | + * | |
| 40 | + * @author 芋道源码 | |
| 41 | + */ | |
| 42 | +@Slf4j | |
| 43 | +public class ApiEncryptFilter extends ApiRequestFilter { | |
| 44 | + | |
| 45 | + private final ApiEncryptProperties apiEncryptProperties; | |
| 46 | + | |
| 47 | + private final RequestMappingHandlerMapping requestMappingHandlerMapping; | |
| 48 | + | |
| 49 | + private final GlobalExceptionHandler globalExceptionHandler; | |
| 50 | + | |
| 51 | + private final SymmetricDecryptor requestSymmetricDecryptor; | |
| 52 | + private final AsymmetricDecryptor requestAsymmetricDecryptor; | |
| 53 | + | |
| 54 | + private final SymmetricEncryptor responseSymmetricEncryptor; | |
| 55 | + private final AsymmetricEncryptor responseAsymmetricEncryptor; | |
| 56 | + | |
| 57 | + public ApiEncryptFilter(WebProperties webProperties, | |
| 58 | + ApiEncryptProperties apiEncryptProperties, | |
| 59 | + RequestMappingHandlerMapping requestMappingHandlerMapping, | |
| 60 | + GlobalExceptionHandler globalExceptionHandler) { | |
| 61 | + super(webProperties); | |
| 62 | + this.apiEncryptProperties = apiEncryptProperties; | |
| 63 | + this.requestMappingHandlerMapping = requestMappingHandlerMapping; | |
| 64 | + this.globalExceptionHandler = globalExceptionHandler; | |
| 65 | + if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) { | |
| 66 | + this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey())); | |
| 67 | + this.requestAsymmetricDecryptor = null; | |
| 68 | + this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey())); | |
| 69 | + this.responseAsymmetricEncryptor = null; | |
| 70 | + } else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) { | |
| 71 | + this.requestSymmetricDecryptor = null; | |
| 72 | + this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null); | |
| 73 | + this.responseSymmetricEncryptor = null; | |
| 74 | + this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey()); | |
| 75 | + } else { | |
| 76 | + // 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。 | |
| 77 | + throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm()); | |
| 78 | + } | |
| 79 | + } | |
| 80 | + | |
| 81 | + @Override | |
| 82 | + @SuppressWarnings("NullableProblems") | |
| 83 | + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | |
| 84 | + throws ServletException, IOException { | |
| 85 | + // 获取 @ApiEncrypt 注解 | |
| 86 | + ApiEncrypt apiEncrypt = getApiEncrypt(request); | |
| 87 | + boolean requestEnable = apiEncrypt != null && apiEncrypt.request(); | |
| 88 | + boolean responseEnable = apiEncrypt != null && apiEncrypt.response(); | |
| 89 | + String encryptHeader = request.getHeader(apiEncryptProperties.getHeader()); | |
| 90 | + if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) { | |
| 91 | + chain.doFilter(request, response); | |
| 92 | + return; | |
| 93 | + } | |
| 94 | + | |
| 95 | + // 1. 解密请求 | |
| 96 | + if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()), | |
| 97 | + HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) { | |
| 98 | + try { | |
| 99 | + if (StrUtil.isNotBlank(encryptHeader)) { | |
| 100 | + request = new ApiDecryptRequestWrapper(request, | |
| 101 | + requestSymmetricDecryptor, requestAsymmetricDecryptor); | |
| 102 | + } else if (requestEnable) { | |
| 103 | + throw invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头"); | |
| 104 | + } | |
| 105 | + } catch (Exception ex) { | |
| 106 | + CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); | |
| 107 | + ServletUtils.writeJSON(response, result); | |
| 108 | + return; | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + // 2. 执行过滤器链 | |
| 113 | + if (responseEnable) { | |
| 114 | + // 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!! | |
| 115 | + response = new ApiEncryptResponseWrapper(response); | |
| 116 | + } | |
| 117 | + chain.doFilter(request, response); | |
| 118 | + | |
| 119 | + // 3. 加密响应(真正执行) | |
| 120 | + if (responseEnable) { | |
| 121 | + ((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties, | |
| 122 | + responseSymmetricEncryptor, responseAsymmetricEncryptor); | |
| 123 | + } | |
| 124 | + } | |
| 125 | + | |
| 126 | + /** | |
| 127 | + * 获取 @ApiEncrypt 注解 | |
| 128 | + * | |
| 129 | + * @param request 请求 | |
| 130 | + */ | |
| 131 | + private ApiEncrypt getApiEncrypt(HttpServletRequest request) { | |
| 132 | + try { | |
| 133 | + HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request); | |
| 134 | + if (mappingHandler == null) { | |
| 135 | + return null; | |
| 136 | + } | |
| 137 | + Object handler = mappingHandler.getHandler(); | |
| 138 | + if (handler instanceof HandlerMethod handlerMethod) { | |
| 139 | + ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class); | |
| 140 | + if (annotation == null) { | |
| 141 | + annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class); | |
| 142 | + } | |
| 143 | + return annotation; | |
| 144 | + } | |
| 145 | + } catch (Exception e) { | |
| 146 | + log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]", | |
| 147 | + request.getRequestURI(), request.getMethod(), e); | |
| 148 | + } | |
| 149 | + return null; | |
| 150 | + } | |
| 151 | + | |
| 152 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.filter; | |
| 2 | + | |
| 3 | +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; | |
| 4 | +import cn.hutool.crypto.asymmetric.KeyType; | |
| 5 | +import cn.hutool.crypto.symmetric.SymmetricEncryptor; | |
| 6 | +import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties; | |
| 7 | +import jakarta.servlet.ServletOutputStream; | |
| 8 | +import jakarta.servlet.WriteListener; | |
| 9 | +import jakarta.servlet.http.HttpServletResponse; | |
| 10 | +import jakarta.servlet.http.HttpServletResponseWrapper; | |
| 11 | + | |
| 12 | +import java.io.ByteArrayOutputStream; | |
| 13 | +import java.io.IOException; | |
| 14 | +import java.io.OutputStreamWriter; | |
| 15 | +import java.io.PrintWriter; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * 加密响应 {@link HttpServletResponseWrapper} 实现类 | |
| 19 | + * | |
| 20 | + * @author 芋道源码 | |
| 21 | + */ | |
| 22 | +public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { | |
| 23 | + | |
| 24 | + private final ByteArrayOutputStream byteArrayOutputStream; | |
| 25 | + private final ServletOutputStream servletOutputStream; | |
| 26 | + private final PrintWriter printWriter; | |
| 27 | + | |
| 28 | + public ApiEncryptResponseWrapper(HttpServletResponse response) { | |
| 29 | + super(response); | |
| 30 | + this.byteArrayOutputStream = new ByteArrayOutputStream(); | |
| 31 | + this.servletOutputStream = this.getOutputStream(); | |
| 32 | + this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); | |
| 33 | + } | |
| 34 | + | |
| 35 | + public void encrypt(ApiEncryptProperties properties, | |
| 36 | + SymmetricEncryptor symmetricEncryptor, | |
| 37 | + AsymmetricEncryptor asymmetricEncryptor) throws IOException { | |
| 38 | + // 1.1 清空 body | |
| 39 | + HttpServletResponse response = (HttpServletResponse) this.getResponse(); | |
| 40 | + response.resetBuffer(); | |
| 41 | + // 1.2 获取 body | |
| 42 | + this.flushBuffer(); | |
| 43 | + byte[] body = byteArrayOutputStream.toByteArray(); | |
| 44 | + | |
| 45 | + // 2. 加密 body | |
| 46 | + String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body) | |
| 47 | + : asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey); | |
| 48 | + response.getWriter().write(encryptedBody); | |
| 49 | + | |
| 50 | + // 3. 添加加密 header 标识 | |
| 51 | + this.addHeader(properties.getHeader(), "true"); | |
| 52 | + // 特殊:特殊:https://juejin.cn/post/6867327674675625992 | |
| 53 | + this.addHeader("Access-Control-Expose-Headers", properties.getHeader()); | |
| 54 | + } | |
| 55 | + | |
| 56 | + @Override | |
| 57 | + public PrintWriter getWriter() { | |
| 58 | + return printWriter; | |
| 59 | + } | |
| 60 | + | |
| 61 | + @Override | |
| 62 | + public void flushBuffer() throws IOException { | |
| 63 | + if (servletOutputStream != null) { | |
| 64 | + servletOutputStream.flush(); | |
| 65 | + } | |
| 66 | + if (printWriter != null) { | |
| 67 | + printWriter.flush(); | |
| 68 | + } | |
| 69 | + } | |
| 70 | + | |
| 71 | + @Override | |
| 72 | + public void reset() { | |
| 73 | + byteArrayOutputStream.reset(); | |
| 74 | + } | |
| 75 | + | |
| 76 | + @Override | |
| 77 | + public ServletOutputStream getOutputStream() { | |
| 78 | + return new ServletOutputStream() { | |
| 79 | + | |
| 80 | + @Override | |
| 81 | + public boolean isReady() { | |
| 82 | + return false; | |
| 83 | + } | |
| 84 | + | |
| 85 | + @Override | |
| 86 | + public void setWriteListener(WriteListener writeListener) { | |
| 87 | + } | |
| 88 | + | |
| 89 | + @Override | |
| 90 | + public void write(int b) { | |
| 91 | + byteArrayOutputStream.write(b); | |
| 92 | + } | |
| 93 | + | |
| 94 | + @Override | |
| 95 | + @SuppressWarnings("NullableProblems") | |
| 96 | + public void write(byte[] b) throws IOException { | |
| 97 | + byteArrayOutputStream.write(b); | |
| 98 | + } | |
| 99 | + | |
| 100 | + @Override | |
| 101 | + @SuppressWarnings("NullableProblems") | |
| 102 | + public void write(byte[] b, int off, int len) { | |
| 103 | + byteArrayOutputStream.write(b, off, len); | |
| 104 | + } | |
| 105 | + | |
| 106 | + }; | |
| 107 | + } | |
| 108 | + | |
| 109 | +} | |
| 0 | 110 | \ No newline at end of file | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java
0 → 100644
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java
| 1 | 1 | package cn.iocoder.yudao.framework.web.core.filter; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 4 | - | |
| 5 | 4 | import jakarta.servlet.ReadListener; |
| 6 | 5 | import jakarta.servlet.ServletInputStream; |
| 7 | 6 | import jakarta.servlet.http.HttpServletRequest; |
| 8 | 7 | import jakarta.servlet.http.HttpServletRequestWrapper; |
| 8 | + | |
| 9 | 9 | import java.io.BufferedReader; |
| 10 | 10 | import java.io.ByteArrayInputStream; |
| 11 | -import java.io.IOException; | |
| 12 | 11 | import java.io.InputStreamReader; |
| 13 | 12 | |
| 14 | 13 | /** |
| ... | ... | @@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { |
| 29 | 28 | } |
| 30 | 29 | |
| 31 | 30 | @Override |
| 32 | - public BufferedReader getReader() throws IOException { | |
| 31 | + public BufferedReader getReader() { | |
| 33 | 32 | return new BufferedReader(new InputStreamReader(this.getInputStream())); |
| 34 | 33 | } |
| 35 | 34 | |
| 36 | 35 | @Override |
| 37 | - public ServletInputStream getInputStream() throws IOException { | |
| 36 | + public int getContentLength() { | |
| 37 | + return body.length; | |
| 38 | + } | |
| 39 | + | |
| 40 | + @Override | |
| 41 | + public long getContentLengthLong() { | |
| 42 | + return body.length; | |
| 43 | + } | |
| 44 | + | |
| 45 | + @Override | |
| 46 | + public ServletInputStream getInputStream() { | |
| 38 | 47 | final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); |
| 39 | 48 | // 返回 ServletInputStream |
| 40 | 49 | return new ServletInputStream() { | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
| ... | ... | @@ -3,4 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration |
| 3 | 3 | cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration |
| 4 | 4 | cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration |
| 5 | 5 | cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration |
| 6 | -cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration | |
| 7 | 6 | \ No newline at end of file |
| 7 | +cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration | |
| 8 | +cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration | |
| 8 | 9 | \ No newline at end of file | ... | ... |
yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.RandomUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.hutool.crypto.SecureUtil; | |
| 6 | +import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm; | |
| 7 | +import cn.hutool.crypto.asymmetric.KeyType; | |
| 8 | +import cn.hutool.crypto.asymmetric.RSA; | |
| 9 | +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; | |
| 10 | +import org.junit.jupiter.api.Test; | |
| 11 | + | |
| 12 | +import java.util.Objects; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * 各种 API 加解密的测试类:不是单测,而是方便大家生成密钥、加密、解密等操作。 | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@SuppressWarnings("ConstantValue") | |
| 20 | +public class ApiEncryptTest { | |
| 21 | + | |
| 22 | + @Test | |
| 23 | + public void testGenerateAsymmetric() { | |
| 24 | + String asymmetricAlgorithm = AsymmetricAlgorithm.RSA.getValue(); | |
| 25 | +// String asymmetricAlgorithm = "SM2"; | |
| 26 | +// String asymmetricAlgorithm = SM4.ALGORITHM_NAME; | |
| 27 | +// String asymmetricAlgorithm = SymmetricAlgorithm.AES.getValue(); | |
| 28 | + String requestClientKey = null; | |
| 29 | + String requestServerKey = null; | |
| 30 | + String responseClientKey = null; | |
| 31 | + String responseServerKey = null; | |
| 32 | + if (Objects.equals(asymmetricAlgorithm, AsymmetricAlgorithm.RSA.getValue())) { | |
| 33 | + // 请求的密钥 | |
| 34 | + RSA requestRsa = SecureUtil.rsa(); | |
| 35 | + requestClientKey = requestRsa.getPublicKeyBase64(); | |
| 36 | + requestServerKey = requestRsa.getPrivateKeyBase64(); | |
| 37 | + // 响应的密钥 | |
| 38 | + RSA responseRsa = new RSA(); | |
| 39 | + responseClientKey = responseRsa.getPrivateKeyBase64(); | |
| 40 | + responseServerKey = responseRsa.getPublicKeyBase64(); | |
| 41 | + } else if (Objects.equals(asymmetricAlgorithm, SymmetricAlgorithm.AES.getValue())) { | |
| 42 | + // AES 密钥可选 32、24、16 位 | |
| 43 | + // 请求的密钥(前后端密钥一致) | |
| 44 | + requestClientKey = RandomUtil.randomNumbers(32); | |
| 45 | + requestServerKey = requestClientKey; | |
| 46 | + // 响应的密钥(前后端密钥一致) | |
| 47 | + responseClientKey = RandomUtil.randomNumbers(32); | |
| 48 | + responseServerKey = responseClientKey; | |
| 49 | + } | |
| 50 | + | |
| 51 | + // 打印结果 | |
| 52 | + System.out.println("requestClientKey = " + requestClientKey); | |
| 53 | + System.out.println("requestServerKey = " + requestServerKey); | |
| 54 | + System.out.println("responseClientKey = " + responseClientKey); | |
| 55 | + System.out.println("responseServerKey = " + responseServerKey); | |
| 56 | + } | |
| 57 | + | |
| 58 | + @Test | |
| 59 | + public void testEncrypt_aes() { | |
| 60 | + String key = "52549111389893486934626385991395"; | |
| 61 | + String body = "{\n" + | |
| 62 | + " \"username\": \"admin\",\n" + | |
| 63 | + " \"password\": \"admin123\",\n" + | |
| 64 | + " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" + | |
| 65 | + " \"code\": \"1024\"\n" + | |
| 66 | + "}"; | |
| 67 | + String encrypt = SecureUtil.aes(StrUtil.utf8Bytes(key)) | |
| 68 | + .encryptBase64(body); | |
| 69 | + System.out.println("encrypt = " + encrypt); | |
| 70 | + } | |
| 71 | + | |
| 72 | + @Test | |
| 73 | + public void testEncrypt_rsa() { | |
| 74 | + String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB"; | |
| 75 | + String body = "{\n" + | |
| 76 | + " \"username\": \"admin\",\n" + | |
| 77 | + " \"password\": \"admin123\",\n" + | |
| 78 | + " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" + | |
| 79 | + " \"code\": \"1024\"\n" + | |
| 80 | + "}"; | |
| 81 | + String encrypt = SecureUtil.rsa(null, key) | |
| 82 | + .encryptBase64(body, KeyType.PublicKey); | |
| 83 | + System.out.println("encrypt = " + encrypt); | |
| 84 | + } | |
| 85 | + | |
| 86 | +} | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http
| ... | ... | @@ -11,6 +11,24 @@ tag: Yunai.local |
| 11 | 11 | "code": "1024" |
| 12 | 12 | } |
| 13 | 13 | |
| 14 | +### 请求 /login 接口【加密 AES】 => 成功 | |
| 15 | +POST {{baseUrl}}/system/auth/login | |
| 16 | +Content-Type: application/json | |
| 17 | +tenant-id: {{adminTenantId}} | |
| 18 | +tag: Yunai.local | |
| 19 | +X-API-ENCRYPT: true | |
| 20 | + | |
| 21 | +WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRtad+JrqChAul/sR/SdOsUKqjBhvvQx1JVhzxr6s8uUP67aKTSZ6Psv7O32ELxXrzSaQvG5CInzz3w6sLtbNNLd1kXe6Q= | |
| 22 | + | |
| 23 | +### 请求 /login 接口【加密 RSA】 => 成功 | |
| 24 | +POST {{baseUrl}}/system/auth/login | |
| 25 | +Content-Type: application/json | |
| 26 | +tenant-id: {{adminTenantId}} | |
| 27 | +tag: Yunai.local | |
| 28 | +X-API-ENCRYPT: true | |
| 29 | + | |
| 30 | +e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVAEd27wwrXBmupOOA/bhpuzzDwcRuJRD+z+YgiNoEXFDRHERxPYlPqAe9zAHtihD0ceub1AjybQsEsROew4C3Q602XYW0= | |
| 31 | + | |
| 14 | 32 | ### 请求 /login 接口 => 成功(无验证码) |
| 15 | 33 | POST {{baseUrl}}/system/auth/login |
| 16 | 34 | Content-Type: application/json | ... | ... |
yudao-server/src/main/resources/application.yaml
| ... | ... | @@ -245,6 +245,13 @@ yudao: |
| 245 | 245 | security: |
| 246 | 246 | permit-all_urls: |
| 247 | 247 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 |
| 248 | + api-encrypt: | |
| 249 | + enable: true # 是否开启 API 加密 | |
| 250 | + algorithm: AES # 加密算法,支持 AES、RSA 等 | |
| 251 | + request-key: 52549111389893486934626385991395 # 【AES 案例】请求加密的秘钥,,必须 16、24、32 位 | |
| 252 | + response-key: 96103715984234343991809655248883 # 【AES 案例】响应加密的秘钥,AES 案例,必须 16、24、32 位 | |
| 253 | +# 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 案例】请求解密的私钥 | |
| 254 | +# response-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDh/CHyBcS/zEfVyINVA7+c9Xxl0CPdxPMK1OIjxaLy/7BLfbkoEpI8onQtjuzfpuxCraDem9bu3BMF0pMH95HytI3Vi0kGjaV+WLIalwgc2w37oA2sbsmKzQOP7SDLO5s2QJNAD7kVwd+Q5rqaLu2MO0xVv+0IUJhn83hClC0L5wIDAQAB # 【RSA 案例】响应加密的公钥 | |
| 248 | 255 | websocket: |
| 249 | 256 | enable: true # websocket的开关 |
| 250 | 257 | path: /infra/ws # 路径 | ... | ... |