若依plus请求加解密
若依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);}@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic int getContentLength() {return body.length;}@Overridepublic long getContentLengthLong() {return body.length;}@Overridepublic String getContentType() {return MediaType.APPLICATION_JSON_VALUE;}@Overridepublic ServletInputStream getInputStream() {final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() {return bais.read();}@Overridepublic int available() {return body.length;}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic 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));}@Overridepublic PrintWriter getWriter() {return printWriter;}@Overridepublic void flushBuffer() throws IOException {if (servletOutputStream != null) {servletOutputStream.flush();}if (printWriter != null) {printWriter.flush();}}@Overridepublic 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);}@Overridepublic ServletOutputStream getOutputStream() throws IOException {return new ServletOutputStream() {@Overridepublic boolean isReady() {return false;}@Overridepublic void setWriteListener(WriteListener writeListener) {}@Overridepublic void write(int b) throws IOException {byteArrayOutputStream.write(b);}@Overridepublic void write(byte[] b) throws IOException {byteArrayOutputStream.write(b);}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic void destroy() {}
}