Commit 2cad35f3610a99566876c5bc35c94de8041af9ec

Authored by YunaiV
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,6 +15,8 @@ public interface WebFilterOrderEnum {
15 15
16 int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; 16 int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
17 17
  18 + int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1;
  19 +
18 // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 20 // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
19 21
20 int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 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 \ No newline at end of file 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
  1 +/**
  2 + * HTTP API 加密组件:支持 Request 和 Response 的加密、解密
  3 + */
  4 +package cn.iocoder.yudao.framework.encrypt;
0 \ No newline at end of file 5 \ No newline at end of file
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java
1 package cn.iocoder.yudao.framework.web.core.filter; 1 package cn.iocoder.yudao.framework.web.core.filter;
2 2
3 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; 3 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
4 -  
5 import jakarta.servlet.ReadListener; 4 import jakarta.servlet.ReadListener;
6 import jakarta.servlet.ServletInputStream; 5 import jakarta.servlet.ServletInputStream;
7 import jakarta.servlet.http.HttpServletRequest; 6 import jakarta.servlet.http.HttpServletRequest;
8 import jakarta.servlet.http.HttpServletRequestWrapper; 7 import jakarta.servlet.http.HttpServletRequestWrapper;
  8 +
9 import java.io.BufferedReader; 9 import java.io.BufferedReader;
10 import java.io.ByteArrayInputStream; 10 import java.io.ByteArrayInputStream;
11 -import java.io.IOException;  
12 import java.io.InputStreamReader; 11 import java.io.InputStreamReader;
13 12
14 /** 13 /**
@@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { @@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
29 } 28 }
30 29
31 @Override 30 @Override
32 - public BufferedReader getReader() throws IOException { 31 + public BufferedReader getReader() {
33 return new BufferedReader(new InputStreamReader(this.getInputStream())); 32 return new BufferedReader(new InputStreamReader(this.getInputStream()));
34 } 33 }
35 34
36 @Override 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 final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); 47 final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
39 // 返回 ServletInputStream 48 // 返回 ServletInputStream
40 return new ServletInputStream() { 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,4 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration
3 cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration 3 cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration
4 cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration 4 cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration
5 cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration 5 cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration
6 -cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration  
7 \ No newline at end of file 6 \ No newline at end of file
  7 +cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
  8 +cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration
8 \ No newline at end of file 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,6 +11,24 @@ tag: Yunai.local
11 "code": "1024" 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 ### 请求 /login 接口 => 成功(无验证码) 32 ### 请求 /login 接口 => 成功(无验证码)
15 POST {{baseUrl}}/system/auth/login 33 POST {{baseUrl}}/system/auth/login
16 Content-Type: application/json 34 Content-Type: application/json
yudao-server/src/main/resources/application.yaml
@@ -245,6 +245,13 @@ yudao: @@ -245,6 +245,13 @@ yudao:
245 security: 245 security:
246 permit-all_urls: 246 permit-all_urls:
247 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 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 websocket: 255 websocket:
249 enable: true # websocket的开关 256 enable: true # websocket的开关
250 path: /infra/ws # 路径 257 path: /infra/ws # 路径