Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【新增】web 模块新增 encrypt 实现 API 加解密 #681

Open
wants to merge 2 commits into
base: master-jdk17
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public interface WebFilterOrderEnum {

int TRACE_FILTER = CORS_FILTER + 1;

int ENCRYPT_FILTER = CORS_FILTER + 2;

int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;

// OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.framework.encrypt.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* @author Zhougang
*/
@ConfigurationProperties(prefix = "yudao.encrypt")
@Data
public class EncryptProperties {

/**
* 是否开启,默认为 false
*/
private boolean enable = false;
/**
* 私钥
*/
private String privateKey;
/**
* 公钥
*/
private String publicKey;
/**
* 请求头 key,客户端传递给服务端的 AES 加密密钥
*/
private String aesKey = "AES-Key";
/**
* 字符集
*/
private String charset = "UTF-8";
/**
* 是否打印日志
*/
private boolean showLog = false;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.framework.encrypt.config;

import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.encrypt.core.advice.ApiEncryptResponseBodyAdvice;
import cn.iocoder.yudao.framework.encrypt.core.filter.ApiDecryptRequestBodyFilter;
import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
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;

/**
* @author Zhougang
*/
@AutoConfiguration
@EnableConfigurationProperties(EncryptProperties.class)
@ConditionalOnProperty(prefix = "yudao.encrypt", name = "enable", havingValue = "true")
public class YudaoApiEncryptAutoConfiguration {

@Bean
public FilterRegistrationBean<ApiDecryptRequestBodyFilter> apiDecryptRequestBodyFilter(EncryptProperties encryptProperties,
GlobalExceptionHandler globalExceptionHandler) {
return YudaoWebAutoConfiguration.createFilterBean(new ApiDecryptRequestBodyFilter(encryptProperties, globalExceptionHandler), WebFilterOrderEnum.ENCRYPT_FILTER);
}

@Bean
public ApiEncryptResponseBodyAdvice apiEncryptResponseBodyAdvice(EncryptProperties properties) {
Assert.notBlank(properties.getPrivateKey(), "请配置 yudao.encrypt.privateKey");
return new ApiEncryptResponseBodyAdvice(properties);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.framework.encrypt.core.advice;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.encrypt.config.EncryptProperties;
import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiEncrypt;
import cn.iocoder.yudao.framework.encrypt.core.util.EncryptUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
* @author Zhougang
*/
@Slf4j
@ControllerAdvice
public class ApiEncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {

private final EncryptProperties encryptProperties;

public ApiEncryptResponseBodyAdvice(EncryptProperties encryptProperties) {
this.encryptProperties = encryptProperties;
}

@Override
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getMethod() == null) {
return false;
}
return returnType.getMethod().isAnnotationPresent(ApiEncrypt.class) && encryptProperties.isEnable();
}

@Override
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body == null) {
return null;
}
String content = body instanceof String ? (String) body : JsonUtils.toJsonString(body);
// 获取客户端 AES 加密密钥
String aesKey = request.getHeaders().getFirst(encryptProperties.getAesKey());
Assert.notBlank(aesKey, () ->
new ServiceException(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), StrUtil.format("请求头 {} 为空!", encryptProperties.getAesKey())));
try {
// 通过 RSA 私钥解密 AES 加密密钥
String decryptAesKey = EncryptUtils.decryptStrByRSA(aesKey, encryptProperties.getPrivateKey());
// 使用 AES 加密数据
String result = EncryptUtils.encryptBase64ByAES(content, decryptAesKey);
if (encryptProperties.isShowLog()) {
log.info("明文字符串为:{},加密后:{}", content, result);
}
return result;
} catch (Exception e) {
log.error("加密异常,body:{}", body, e);
throw new ServiceException(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "加密异常");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cn.iocoder.yudao.framework.encrypt.core.annotation;

import java.lang.annotation.*;

/**
* 请求参数解密
* @author Zhougang
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiDecrypt {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cn.iocoder.yudao.framework.encrypt.core.annotation;

import java.lang.annotation.*;

/**
* 响应参数加密
* @author Zhougang
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiEncrypt {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cn.iocoder.yudao.framework.encrypt.core.filter;

import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.encrypt.config.EncryptProperties;
import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiDecrypt;
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 org.springframework.web.filter.OncePerRequestFilter;
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;

/**
* 1、客户端传输重要信息给服务端,服务端返回的信息不需加密的情况:
* (1) 服务端利用 RSA 创建一对公私钥,服务端存储私钥,将公钥给客户端
* (2) 客户端每次请求前,将明文数据利用公钥进行加密,然后将密文传递给服务端
* (3) 服务端拿到密文,利用私钥进行解密,得到明文数据,然后进行业务处理
* <p>
* 2、如果想做到返回参数也进行加密,那么两个方法
* (1) 客户端创建一对公私钥,服务端再重新创建一对公私钥,互相各拿对方的公钥,这样就可以进行传参的加密解密,和响应数据的加解密!!!
* PS:私钥放在客户端就违背了非对称加密 RSA 的原则性了
* (2) 但是为了解决 RSA 加解密性能问题,所以采用了 RSA 非对称 + AES 对称加密,大致思路:
* *** 客户端:
* *** 1) 客户端随机产生 AES 密钥
* *** 2) 对数据(重要信息)进行 AES 加密
* *** 3) 通过服务端提供的 RSA 公钥对 AES 密钥进行加密
* *** 4) 把加密后的 AES 密钥,和 AES 加密后的数据一起传递给服务端
* *** 服务端:
* *** 1) 拿到客户端传来的 AES 加密密钥,利用 RSA 私钥进行解密
* *** 2) 利用解密后的 AES 对数据进行解密,然后进行业务处理
* *** 3) 返回数据进行加密,通过 AES 密钥进行加密,返回给客户端
* *** 4) 客户端拿到服务端返回的密文,利用 AES 密钥进行解密,得到明文数据
* <p>
* 这里采用的是 RSA + AES 混合加密方式,这样在传输的过程中,即时加密后的 AES 密钥被别人截取,
* 对其也无济于事,因为他并不知道 RSA 的私钥,无法解密得到原本的 AES 密钥。
*
* @author Zhougang
*/
public class ApiDecryptRequestBodyFilter extends OncePerRequestFilter {

private final GlobalExceptionHandler globalExceptionHandler;

private final EncryptProperties encryptProperties;

public ApiDecryptRequestBodyFilter(EncryptProperties encryptProperties, GlobalExceptionHandler globalExceptionHandler) {
this.encryptProperties = encryptProperties;
this.globalExceptionHandler = globalExceptionHandler;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ApiDecrypt apiEncrypt = getApiDecryptAnnotation(request);
if (apiEncrypt != null) {
ApiDecryptRequestBodyWrapper decryptRequestBodyWrapper;
try {
decryptRequestBodyWrapper = new ApiDecryptRequestBodyWrapper(request, encryptProperties);
} catch (Exception e) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, e);
ServletUtils.writeJSON(response, result);
return;
}
filterChain.doFilter(decryptRequestBodyWrapper, response);
return;
}
filterChain.doFilter(request, response);
}

private ApiDecrypt getApiDecryptAnnotation(HttpServletRequest servletRequest) {
RequestMappingHandlerMapping handlerMapping = SpringUtil.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
HandlerExecutionChain mappingHandler;
try {
mappingHandler = handlerMapping.getHandler(servletRequest);
} catch (Exception e) {
return null;
}
Object handler = mappingHandler.getHandler();
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
return handlerMethod.getMethodAnnotation(ApiDecrypt.class);
}
return null;
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// 只处理 json 请求内容
return !ServletUtils.isJsonRequest(request);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.framework.encrypt.core.filter;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.encrypt.config.EncryptProperties;
import cn.iocoder.yudao.framework.encrypt.core.util.EncryptUtils;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;

/**
* 请求参数解密 Wrapper
*
* @author Zhougang
*/
@Slf4j
public class ApiDecryptRequestBodyWrapper extends HttpServletRequestWrapper {

/**
* 缓存的内容
*/
private final byte[] body;

public ApiDecryptRequestBodyWrapper(HttpServletRequest request, EncryptProperties encryptProperties) throws Exception {
super(request);

byte[] body = ServletUtils.getBodyBytes(request);
if (ArrayUtil.isEmpty(body)) {
this.body = body;
return;
}
String bodyStr = new String(body);
// 获取客户端 AES 加密密钥
String aesKey = request.getHeader(encryptProperties.getAesKey());
Assert.notBlank(aesKey, () ->
new ServiceException(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), StrUtil.format("请求头 {} 为空!", encryptProperties.getAesKey())));
// 通过 RSA 私钥解密 AES 加密密钥
String decryptAesKey = EncryptUtils.decryptStrByRSA(aesKey, encryptProperties.getPrivateKey());
// 通过解密后的 AES 解密数据
String decryptBody = EncryptUtils.decryptStrByAES(bodyStr, decryptAesKey);
if (encryptProperties.isShowLog()) {
log.info("接收到的加密数据:{},解密后:{}", bodyStr, decryptBody);
}
this.body = decryptBody.getBytes(encryptProperties.getCharset());
}

@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}

@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
// 返回 ServletInputStream
return new ServletInputStream() {

@Override
public int read() {
return inputStream.read();
}

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {
}

@Override
public int available() {
return body.length;
}

};
}

}
Loading