若依plus请求加解密
本文将以 RuoYi Plus 框架 为例,详细介绍如何通过 RSA + AES 混合加密机制 实现接口的请求加密与响应加密,确保数据安全传输。
整个加密通信分为两个阶段:
- 请求加密解密:前端加密 → 后端解密
- 响应加密解密:后端加密 → 前端解密
这种模式采用 对称加密 + 非对称加密混合方案:
| 加密方式 | 使用场景 | 特点 |
|---|---|---|
| AES(对称加密) | 加密请求体、响应体 | 加解密速度快,但需要安全地传递密钥 |
| RSA(非对称加密) | 加密 AES 密钥 | 密钥安全性高,但性能较低,适合加密小数据 |
因此,整个设计遵循以下原则:
用 RSA 保护 AES 密钥,用 AES 加密真实数据。
1.请求加密解密流程
前端加密流程
在前端(如 Vue / React)发起请求时,执行以下步骤:
- 随机生成 32 位的 AES 密钥
aesKey; - 对
aesKey进行 Base64 编码 得到base64Aes; - 使用 RSA 公钥 对
base64Aes加密,得到rsaAes; - 将
rsaAes放入请求头中:encrypt-key: rsaAes; - 使用
aesKey对请求体 JSON 进行 AES 加密; - 将加密后的密文作为请求 body 发送至后端。
后端解密流程
后端接收到请求后,反向解密流程如下:
- 从 Header 中读取
encrypt-key; - 使用 RSA 私钥 解密得到
base64Aes; - 对
base64Aes进行 Base64 解码得到真实的aesKey; - 使用该
aesKey对请求体(AES 密文)进行解密; - 得到明文 JSON 请求参数。
┌─────────────────────────────┐
│ 前端 Vue/React │
│─────────────────────────────│
│ 1. 随机生成 AES 密钥(32位) aesKey
│ 2. 将 aesKey Base64 编码 → base64Aes
│ 3. 使用 RSA 公钥 加密 base64Aes → rsaAes
│ 4. 把 rsaAes 放到请求头:encrypt-key
│ 5. 使用 aesKey 对请求体(JSON)进行 AES 加密
│ 6. 发送 HTTP 请求(body 为 AES密文)→ 后端
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 后端 API │
│─────────────────────────────│
│ 1. 从 header 获取 rsaAes
│ 2. 使用 RSA 私钥 解密 → base64Aes
│ 3. Base64 解码 → aesKey
│ 4. 使用 aesKey 解密请求体(AES)
│ 5. 得到明文 JSON 参数
└─────────────────────────────┘
2.响应加密解密流程
后端加密流程
- 生成一个新的 32 位 AES 密钥;
- 对密钥进行 Base64 编码;
- 使用 RSA 公钥加密后放入响应头:
encrypt-key; - 使用该 AES 密钥加密响应内容;
- 返回加密后的响应体。
前端解密流程
- 从响应头读取
encrypt-key; - 使用 RSA 私钥解密 →
base64Aes; - Base64 解码 →
aesKey; - 使用
aesKey对响应体进行 AES 解密; - 得到明文 JSON 响应。
┌─────────────────────────────┐
│ 后端 API │
│─────────────────────────────│
│ 1. 生成新的 AES 密钥(32位) aesKey
│ 2. Base64 编码 → base64Aes
│ 3. 使用 RSA 公钥 加密 → rsaAes
│ 4. 响应头设置 encrypt-key = rsaAes
│ 5. 使用 aesKey 对响应内容进行 AES 加密
│ 6. 将 AES 密文作为响应 body 返回
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 前端 Vue/React │
│─────────────────────────────│
│ 1. 从响应头读取 encrypt-key (rsaAes)
│ 2. 使用 RSA 私钥 解密 → base64Aes
│ 3. Base64 解码 → aesKey
│ 4. 使用 aesKey 解密响应体(AES)
│ 5. 得到明文 JSON 响应
└─────────────────────────────┘
3.请求解密包装器
核心逻辑:
- 从请求头中读取加密的
encrypt-key; - 使用 RSA 私钥解密得到 AES 密钥;
- 读取原始请求流;
- 使用 AES 密钥解密请求体;
- 将解密后的内容重新写入请求流,供后续 Filter/Controller 使用。
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
super(request);
// 获取 AES 密码 采用 RSA 加密
String headerRsa = request.getHeader(headerFlag);
String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
// 解密 AES 密码
String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
request.setCharacterEncoding(Constants.UTF8);
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 解密 body 采用 AES 加密
String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public int getContentLength() {
return body.length;
}
@Override
public long getContentLengthLong() {
return body.length;
}
@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.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) {
}
};
}
}
4.响应加密包装器
核心逻辑:
- 拦截输出流,暂存在
ByteArrayOutputStream; - 生成新的 AES 密钥;
- 对密钥 Base64 编码后用 RSA 公钥加密;
- 将加密密钥写入响应头;
- 使用 AES 加密原始响应内容;
- 将加密后的结果写回给客户端。
public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream byteArrayOutputStream;
private final ServletOutputStream servletOutputStream;
private final PrintWriter printWriter;
public EncryptResponseBodyWrapper(HttpServletResponse response) throws IOException {
super(response);
this.byteArrayOutputStream = new ByteArrayOutputStream();
this.servletOutputStream = this.getOutputStream();
this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
}
@Override
public PrintWriter getWriter() {
return printWriter;
}
@Override
public void flushBuffer() throws IOException {
if (servletOutputStream != null) {
servletOutputStream.flush();
}
if (printWriter != null) {
printWriter.flush();
}
}
@Override
public void reset() {
byteArrayOutputStream.reset();
}
public byte[] getResponseData() throws IOException {
flushBuffer();
return byteArrayOutputStream.toByteArray();
}
public String getContent() throws IOException {
flushBuffer();
return byteArrayOutputStream.toString();
}
/**
* 获取加密内容
*
* @param servletResponse response
* @param publicKey RSA公钥 (用于加密 AES 秘钥)
* @param headerFlag 请求头标志
* @return 加密内容
* @throws IOException
*/
public String getEncryptContent(HttpServletResponse servletResponse, String publicKey, String headerFlag) throws IOException {
// 生成秘钥
String aesPassword = RandomUtil.randomString(32);
// 秘钥使用 Base64 编码
String encryptAes = EncryptUtils.encryptByBase64(aesPassword);
// Rsa 公钥加密 Base64 编码
String encryptPassword = EncryptUtils.encryptByRsa(encryptAes, publicKey);
// 设置响应头
// vue版本需要设置
servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
servletResponse.setHeader("Access-Control-Allow-Origin", "*");
servletResponse.setHeader("Access-Control-Allow-Methods", "*");
servletResponse.setHeader(headerFlag, encryptPassword);
servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
// 获取原始内容
String originalBody = this.getContent();
// 对内容进行加密
return EncryptUtils.encryptByAes(originalBody, aesPassword);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
byteArrayOutputStream.write(b, off, len);
}
};
}
}
5加解密过滤器
- 读取请求头中的 encrypt-key
- 若存在 → 使用 DecryptRequestBodyWrapper 解密请求体
- 检查 Controller 方法上是否有 @ApiEncrypt(response=true)
- 若存在 → 使用 EncryptResponseBodyWrapper 包装响应
- 放行过滤链
- Controller 执行完毕后,对响应内容进行加密输出
public class CryptoFilter implements Filter {
private final ApiDecryptProperties properties;
public CryptoFilter(ApiDecryptProperties properties) {
this.properties = properties;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
boolean responseFlag = apiEncrypt != null && apiEncrypt.response();
ServletRequest requestWrapper = null;
ServletResponse responseWrapper = null;
EncryptResponseBodyWrapper responseBodyWrapper = null;
// 是否为 put 或者 post 请求
if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
// 是否存在加密标头
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
if (StringUtils.isNotBlank(headerValue)) {
// 请求解密
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
} else {
// 是否有注解,有就报错,没有放行
if (ObjectUtil.isNotNull(apiEncrypt)) {
HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
exceptionResolver.resolveException(
servletRequest, servletResponse, null,
new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
return;
}
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
chain.doFilter(
ObjectUtil.defaultIfNull(requestWrapper, request),
ObjectUtil.defaultIfNull(responseWrapper, response));
if (responseFlag) {
servletResponse.reset();
// 对原始内容加密
String encryptContent = responseBodyWrapper.getEncryptContent(
servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
// 对加密后的内容写出
servletResponse.getWriter().write(encryptContent);
}
}
/**
* 获取 ApiEncrypt 注解
*/
private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
RequestMappingHandlerMapping handlerMapping = SpringUtils.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
// 获取注解
try {
HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
if (ObjectUtil.isNotNull(mappingHandler)) {
Object handler = mappingHandler.getHandler();
if (ObjectUtil.isNotNull(handler)) {
// 从handler获取注解
if (handler instanceof HandlerMethod handlerMethod) {
return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
}
}
}
} catch (Exception e) {
return null;
}
return null;
}
@Override
public void destroy() {
}
}
1992

被折叠的 条评论
为什么被折叠?



