若依plus请求加解密

若依plus请求加解密

本文将以 RuoYi Plus 框架 为例,详细介绍如何通过 RSA + AES 混合加密机制 实现接口的请求加密与响应加密,确保数据安全传输。

整个加密通信分为两个阶段:

  • 请求加密解密:前端加密 → 后端解密
  • 响应加密解密:后端加密 → 前端解密

这种模式采用 对称加密 + 非对称加密混合方案

加密方式使用场景特点
AES(对称加密)加密请求体、响应体加解密速度快,但需要安全地传递密钥
RSA(非对称加密)加密 AES 密钥密钥安全性高,但性能较低,适合加密小数据

因此,整个设计遵循以下原则:

用 RSA 保护 AES 密钥,用 AES 加密真实数据。

1.请求加密解密流程

前端加密流程

在前端(如 Vue / React)发起请求时,执行以下步骤:

  1. 随机生成 32 位的 AES 密钥 aesKey
  2. aesKey 进行 Base64 编码 得到 base64Aes
  3. 使用 RSA 公钥base64Aes 加密,得到 rsaAes
  4. rsaAes 放入请求头中:encrypt-key: rsaAes
  5. 使用 aesKey 对请求体 JSON 进行 AES 加密;
  6. 将加密后的密文作为请求 body 发送至后端。

后端解密流程

后端接收到请求后,反向解密流程如下:

  1. 从 Header 中读取 encrypt-key
  2. 使用 RSA 私钥 解密得到 base64Aes
  3. base64Aes 进行 Base64 解码得到真实的 aesKey
  4. 使用该 aesKey 对请求体(AES 密文)进行解密;
  5. 得到明文 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.响应加密解密流程

后端加密流程

  1. 生成一个新的 32 位 AES 密钥;
  2. 对密钥进行 Base64 编码;
  3. 使用 RSA 公钥加密后放入响应头:encrypt-key
  4. 使用该 AES 密钥加密响应内容;
  5. 返回加密后的响应体。

前端解密流程

  1. 从响应头读取 encrypt-key
  2. 使用 RSA 私钥解密 → base64Aes
  3. Base64 解码 → aesKey
  4. 使用 aesKey 对响应体进行 AES 解密;
  5. 得到明文 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加解密过滤器

  1. 读取请求头中的 encrypt-key
  2. 若存在 → 使用 DecryptRequestBodyWrapper 解密请求体
  3. 检查 Controller 方法上是否有 @ApiEncrypt(response=true)
  4. 若存在 → 使用 EncryptResponseBodyWrapper 包装响应
  5. 放行过滤链
  6. 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() {
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值