当前位置: 首页 > article >正文

SpringBoot 接口内容加密方案(RSA+AES+HMAC校验)认知

写在前面

  • 工作中遇到,简单整理
  • 博文内容涉及 Web接口内容 类似 https 的加密和防篡改校验
  • 以及具体Java Springboot 项目中如何编码。
  • 理解不足小伙伴帮忙指正 😃,生活加油

99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式

持续分享技术干货,感兴趣小伙伴可以关注下 _


在讲这部分内容之前,先看几个问题:


Q: 有了 https 为什么还需要接口 RSA+AES 加密+HMAC 校验

A:https通信加密,而 接口的 RSA+AES 加密+ HMAC 校验 属于内容加密,HTTPS 加密的是传输过程中的数据,确保数据在客户端和服务器之间传输时不会被窃听或篡改。而接口加密是对接口内容的加密,即报文实体加密


Q:用了 https 做通信加密,为什么本地抓包或者说浏览器还是可以看到报文内容 ?

A:浏览器是客户端,看的时候已经发生的解密,抓包工具利用根证书伪造来解密报文。


Q:那么为什么解密不发生在代码层面,而且在客户端就解密了? 这样就可以避免抓包工具解密了

A:换一种角度考虑,在客户端和服务端属于同一级的高信任区域,因为被信任所以可以看到报文数据,而客户端和服务端之间的链路属于低信任区域,所以加密。既解密和加密是对同一信任度的区域而言。加密和解密发生在由低信任度到高信任度之间,是对称的,在服务端高信任区域加密数据到链路的低信任区域,所以同样需要在链路的低信任区域到客户端的高信任区域解密。所以不在代码层面解密,因为加密发生着服务器端的同级信任区。


Q: 如果希望在代码层面解密,应该如何处理,即为什么需要做内容加密?

A: 通过 https 实现了通信加密,但是对于客户端本地来讲,还是可以利用浏览器或者抓包工具获取实际的报文数据,为了避免敏感数据的泄露,把解密控制在代码层面,我们就需要在 客户端和服务端的信任区域上面在加高一级别信任度的信任区域,这个区域就是代码级别的信任区,之前的 https 对全部报文做了加密,现在我们只对交互的报文实体做加密,这也就是内容加密,所以内容加密是为了数据到达客户端(如浏览器)后会被解密出实际响应报文响应实体仍然处于加密状态,防止通过浏览器或者抓包工具直接获取数据。


通过上面的问题,我们可以对内容加密有一定的认知。下面我们看一下如何对内容加密

接口内容加密方案简单介绍

先来简单看下 https 的加密原理:

https 实际上是 http + SSL/TSL(加密认证,防篡改) = https 是在 HTTP 协议的基础上,通过 SSL/TLS 协议对数据进行加密和认证,确保通信的安全性和完整性。

SSL/TLS 的核心功能 SSL/TLS 提供了以下核心功能:
1. 加密:对传输的数据进行加密,防止窃听。
2. 认证:验证服务器的身份,防止中间人攻击。
3. 完整性:确保数据在传输过程中未被篡改。

HTTPS 的工作流程: HTTPS 的加密原理主要依赖于 SSL/TLS 协议,其工作流程如下:

  1. 建立 TCP 连接: 客户端与服务器通过 TCP 三次握手建立连接。
  2. TLS 握手:
    • 客户端 Hello:客户端发送支持的 TLS 版本、加密套件列表和一个随机数。
    • 服务器 Hello:服务器选择 TLS 版本、加密套件,并返回自己的随机数和证书(包含公钥)。
    • 证书验证:客户端验证服务器证书的有效性(是否由可信 CA 签发,是否过期等),防止中间人攻击
    • 密钥交换:客户端生成一个预主密钥(Pre-Master Secret),用服务器的公钥非对称加密后发送给服务器。
    • 生成会话密钥:客户端和服务器使用预主密钥和随机数生成对称加密密钥(Session Key),用于后续通信。

加密通信

  • 客户端和服务器使用对称加密密钥对 HTTP 数据进行加密和解密。
  • 每次通信都会使用 HMAC 或 AEAD 模式验证数据的完整性。

HTTP 像寄明信片,内容公开,容易被偷看或篡改。HTTPS 像寄加密信件,内容被锁在保险箱里,只有收件人有钥匙打开,确保安全性和完整性。 而我们要做的内容加密是在 HTTPS 的基础上,对明信片上面的内容进行加密处理,收件人用钥匙打开之后,明信片上面是密文,还需要用约定的密码来解密出明文

这里的内容加密也使用上面 https 加密的方案,当然还有其他的方案,不同的是 CA证书的获取和认证,即从获取非对称加密的公钥开始,所有的加解密是发生的代码层级的。

常见的加密方案(RSA + AES + HMAC TLS 1.2)

  • 对称加密(如 AES):加密和解密使用相同的密钥,速度快,但密钥分发不安全,用于加密实际传输的数据,保证高效性。
  • 非对称加密(如 RSA):加密和解密使用不同的密钥,安全性高,但速度慢,用于加密 AES 密钥,解决密钥分发问题
  • 消息认证码(如 HMAC):用于验证数据的完整性和真实性,生成签名。

对于一个完整的接口内容加密流程

客户端请求,加密报文过程:

密钥生成,类似 SSL

1. 获取非对称的公钥: MIIBIjAN................kqUXgQntOo3HOuzW9pqwIDAQAB
2. 生成使用的对称的密钥: buLZ...CsBsEcd
3. 通过生成的对称密钥对请求报文进行加密:bodySt......14g==
4. 对称密钥通过非对称公钥加密生成传输的密钥: NDI3q..................p86SyQ==

签名的生成,这里使用的是 HMAC ,也可以考虑使用 AEAD

签名需要的数据
================================== message :
接口类型: POST
接口地址:/hotel/web/threePartyI。。。。。。。。。。。nfoAnonymous
对称密钥通过非对称公钥加密后的密钥(X-Secret 报文头): NDI3qtS...................6SyQ==  
随机字符串(X-Nonce 报文头):M2jmJm6Yo9
时间戳(X-Timestamp 报文头):1738897905
请求报文对称加密后的密文哈希值: 6fb2a0229959e25706a7fc50b888f82dbe8688bc

上面的签名数据组合在通过哈希算法和对称密钥做种子生成签名

生成的签名(X-Signature 报文头) signature:cdd5bab8e.......73d61753d546b

调用接口:

11:11:46.081 [main] INFO ......- crm url:https:.....nymous method: POST body: {"gCertNo":"220882199608126526","gMobile":"18147405370"}
实际的请求报文:bodyStr{"gCertNo":"220882199608126526","gMobile":"18147405370"}
加密后的报文 bodyStroZtulHZvJrRN2sfBs6MvLOCRaLbIh8jgpGHIsE9DFwHBGGp0vuNylE/OOAeo6pYGRP/kkpL8DZPXOMapTbX14g==
.......................................

服务端收到报文,对请求报文做解密处理,同时对响应报文做加密处理

  1. 加载本地的非对称加密的私钥
  2. 判断报文头数据是否存在,同时从报文头获取需要的数据(X-Secret,X-Nonce,X-Timestamp,X-Signature)
  3. 判断时间戳是否符合要求
  4. 通过非对称加密的私钥对对称加密的密钥进行解密,获取对称加密的密钥
  5. HMAC校验,重新生成签名判断是否一致
  6. 解密请求报文给后端接口处理
  7. 接口处理完成返回响应,通过 对称加密的密钥对响应报文进行加密
11:11:46.433 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
11:11:46.434 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8"
================ 返回的消息:<200,auePdYEQfYaQgBt3RPMOGLXTxXxO72t8nSk6CQ5aqKXzZ+GWXoxml7pv9+OhwlAE,[vary:"Origin,Access-Control-Request-Method,Access-Control-Request-Headers", x-content-type-options:"nosniff", x-xss-protection:"1; mode=block", strict-transport-security:"max-age=31536000 ; includeSubDomains", x-frame-options:"SAMEORIGIN", content-type:"application/json;charset=UTF-8", content-length:"64", date:"Fri, 07 Feb 2025 03:11:46 GMT", x-envoy-upstream-service-time:"17", server:"istio-envoy"]>

客户端收到响应报文,通过上面请求报文生成的对称加密密钥对响应报文进行解密处理,获取实际的响应报文

返回的报文:auePdYEQfYaQgBt......Xoxml7pv9+OhwlAE
解密后的报文=>>>{"msg":"......","code":200}

代码实现

下面是一个 SpringBoot 项目的接口加密实现服务端的编码

  1. Spring-Security 添加对应的过滤器,用于处理服务端的请求解密,响应加密:
/**
     *  加密过滤器
     */
    @Autowired
    private SecurityFilter securityFilter;

    .....................
 // 添加JWT filter
                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 添加CORS filter
                .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
                .addFilterBefore(corsFilter, LogoutFilter.class)
                // 添加接口加密过滤器
                .addFilterBefore(securityFilter, JwtAuthenticationTokenFilter.class);
               // 添加多因素认证
               。。。。。。。。
  1. 处理请求报文的解密,响应报文的加密

过滤器方法,这里利用 Java Web 的过滤器链,doFilterInternal 为核心的方法,用于对请求的报文进行解密,然后给传递给其他的过滤器,最后到实际的路由地址,处理完请求的返回响应在对响应报文进行加密给客户端返回数据。

    /**
     * 加密接口的处理(RSA+AES  )
     * @param request
     * @param response
     * @param filterChain
     */
    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)  {
        // 获取请求的路径
        String requestUri = request.getRequestURI();
        // 只处理特定的路由
        if (!requestUri.startsWith(API_PATH_PATTERN)) {
            filterChain.doFilter(request, response);
            return;
        }
        // todo 需要注意 `request.getInputStream()` 只能读一次,所以在这里读取
        String bodyStr = getRequestBody(request);
        // todo  ============================= 请求报文的解密处理 ==============================
        // 通过  RSA公钥要加的 AES密钥
        String secretHeader = request.getHeader("X-Secret");
        // 随机数
        String nonceHeader = request.getHeader("X-Nonce");
        // 时间戳
        String timestampHeader = request.getHeader("X-Timestamp");
        // 签名
        String signatureHeader = request.getHeader("X-Signature");
        if (secretHeader == null || nonceHeader == null || timestampHeader == null || signatureHeader == null) {
            log.error("请求报文不符合要求!");
            response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error");
            return;
        }
        // 加载私钥
        PrivateKey privateKey = loadPrivateKey(privateKeyStr);
        System.out.println("================= 加密的密钥:"+ secretHeader);
        // 通过私钥解密客户端用公钥加密的 AES 密钥
        String decrypt = RSAFacade.decrypt(CipherTypeEnums.RSA_PKCS1, secretHeader, privateKey);
        System.out.println("================= 解密的密钥:"+ decrypt);
        // 比较时间戳
        long timestamp = Long.parseLong(timestampHeader);
        long currTimestamp = Instant.now().getEpochSecond();
        if (currTimestamp - timestamp > REQUEST_EXPIRY_TIME) {
            log.error("时间戳异常!");
            response.sendError(HttpStatus.BAD_REQUEST.value(), "expired request");
            return;
        }
        // HMAC校验 校验签名
        String message = buildMessage(request, secretHeader, nonceHeader, timestampHeader,bodyStr);
        System.out.println("=========================: 消息"+ message);
        String computedSignature = null;
        // 生成HMAC校验签名
        computedSignature = toHMAC_SHA256( message,decrypt);
        System.out.println("========================== 生成的签名:" + computedSignature);
        if (!computedSignature.equals(signatureHeader)) {
            log.error("签名不一致!");
            response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error");
            return;
        }
        System.out.println("================加密的报文数据 requestBody: " + bodyStr);
        String encryptedBody = null;
        // 通过 AES 解密报文
        AES aes = AES.getInstance(CipherTypeEnums.AES_CBC_PKCS7, decrypt, decrypt);
        encryptedBody = aes.decrypt(bodyStr);
        System.out.println("===================== 获取到的报文数据:"+ encryptedBody);
        // TODO  ======================================= 重新封装请求和响应报文 ==================================
        CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper(request, encryptedBody.getBytes());
        EncryptingResponseWrapper wrappedResponse = new EncryptingResponseWrapper(response);
        // 调用过滤器链处理请求
        filterChain.doFilter(wrappedRequest, wrappedResponse);
        // TODO ========================================  加密响应报文 ========================
        // 获取响应报文的字节数组
        String string = wrappedResponse.getResponseData();
        System.out.println("====================返回的报文数据:" +  string);
        // 对响应报文进行处理,例如加密、压缩等
        bodyStr = aes.encrypt(string);
        System.out.println("====================返回的加密报文数据:" +  bodyStr);
        // 将处理后的响应报文写回到 response
        response.getOutputStream().write(bodyStr.getBytes(StandardCharsets.UTF_8));
    }

需要注意的问题:

  1. request.getInputStream() 只能读一次,并且在过滤器链里面使用,处理完请求报文,还要在塞回去。
  2. 请求报文和响应报文的二次封装通过内部类 EncryptingResponseWrapperCustomHttpServletRequestWrapper 实现
  3. 对于加密和解密使用的RSA,AES以及计算哈希值的算法,服务端和客户端要保证使用一致的密钥格式,即 加密算法(RSA, AES),加密模式(ECB,CBC等),加密补码方式(PKCS1_PADDING, PKCS5_PADDING) 要保持一致,开发中出现的加解密错误大都是这里的问题。

下面为过滤器中处理加密解密完整的代码. 关于算法部分这里没有展示

import com.ruoyi.framework.security.filter.tool.AES;
import com.ruoyi.framework.security.filter.tool.CipherTypeEnums;
import com.ruoyi.framework.security.filter.tool.RSAFacade;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.time.Instant;

@Component
@Slf4j
public class SecurityFilter extends OncePerRequestFilter {

    private static final String API_PATH_PATTERN = "/hot......eePartyInterfaceCRM/";  // 指

    private static final String RSA_PRIVATE_KEY =
            "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDaEFdIynY8WbH8" +
         "...................0W";

    // 接口响应失效时间
    private static final long REQUEST_EXPIRY_TIME = 30; // seconds

    /**
     * 加密接口的处理(RSA+AES +  )
     * @param request
     * @param response
     * @param filterChain
     */
    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)  {


        // 获取请求的路径
        String requestUri = request.getRequestURI();
        // todo 需要注意 `request.getInputStream()` 只能读一次,所以在开头读取
        String bodyStr = getRequestBody(request);

        // 只处理特定的路由
        if (!requestUri.startsWith(API_PATH_PATTERN)) {
            filterChain.doFilter(request, response);
            return;
        }

        // todo  ============================= 请求报文的解密处理 ==============================

        // 通过  RSA公钥要加的 AES密钥
        String secretHeader = request.getHeader("X-Secret");
        // 随机数
        String nonceHeader = request.getHeader("X-Nonce");
        // 时间戳
        String timestampHeader = request.getHeader("X-Timestamp");
        // 签名
        String signatureHeader = request.getHeader("X-Signature");

        if (secretHeader == null || nonceHeader == null || timestampHeader == null || signatureHeader == null) {
            log.error("请求报文不符合要求!");
            response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error");
            return;
        }
        // 加载私钥
        PrivateKey privateKey = loadPrivateKey(RSA_PRIVATE_KEY);
        System.out.println("================= 加密的密钥:"+ secretHeader);
        // 通过私钥解密客户端用公钥加密的 AES 密钥
        String decrypt = RSAFacade.decrypt(CipherTypeEnums.RSA_PKCS1, secretHeader, privateKey);
        System.out.println("================= 解密的密钥:"+ decrypt);

        // 比较时间戳
        long timestamp = Long.parseLong(timestampHeader);
        long currTimestamp = Instant.now().getEpochSecond();

        if (currTimestamp - timestamp > REQUEST_EXPIRY_TIME) {
            log.error("时间戳异常!");
            response.sendError(HttpStatus.BAD_REQUEST.value(), "expired request");
            return;
        }

        // HMAC校验 校验签名
        String message = buildMessage(request, secretHeader, nonceHeader, timestampHeader,bodyStr);
        System.out.println("=========================: 消息"+ message);
        String computedSignature = null;
        // 生成HMAC校验签名
        computedSignature = toHMAC_SHA256( message,decrypt);
        System.out.println("========================== 生成的签名:" + computedSignature);

        if (!computedSignature.equals(signatureHeader)) {
            log.error("签名不一致!");
            response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error");
            return;
        }



        System.out.println("================加密的报文数据 requestBody: " + bodyStr);
        String encryptedBody = null;
        // 通过 AES 解密报文
        AES aes = AES.getInstance(CipherTypeEnums.AES_CBC_PKCS7, decrypt, decrypt);
        encryptedBody = aes.decrypt(bodyStr);
        System.out.println("===================== 获取到的报文数据:"+ encryptedBody);

        // TODO  ======================================= 重新封装请求和响应报文 ==================================

        CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper(request, encryptedBody.getBytes());
        EncryptingResponseWrapper wrappedResponse = new EncryptingResponseWrapper(response);


        // 调用过滤器链处理请求
        filterChain.doFilter(wrappedRequest, wrappedResponse);

        // TODO ========================================  加密响应报文 ========================
        // 获取响应报文的字节数组
        String string = wrappedResponse.getResponseData();

        System.out.println("====================返回的报文数据:" +  string);
        // 对响应报文进行处理,例如加密、压缩等
        bodyStr = aes.encrypt(string);
        System.out.println("====================返回的加密报文数据:" +  bodyStr);
        // 将处理后的响应报文写回到 response
        response.getOutputStream().write(bodyStr.getBytes(StandardCharsets.UTF_8));
    }


    /**
     * 加载服务端私钥
     * @param privateKeyString
     * @return
     * @throws Exception
     */
    private PrivateKey loadPrivateKey(String privateKeyString) throws Exception {
        System.out.println("==============================加载的私钥:"+  privateKeyString );
        return RSAFacade.getPrivateKey(CipherTypeEnums.RSA_PKCS1, privateKeyString);

    }


    /**
     * 生成消息
     * @param request
     * @param secret
     * @param nonce
     * @param timestamp
     * @param bodyStr
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    private String buildMessage(HttpServletRequest request, String secret, String nonce, String timestamp, String bodyStr) throws IOException, NoSuchAlgorithmException {
        String method = request.getMethod();

        String url = request.getRequestURI().toString();
        String bodyHash = getBodyHash(bodyStr);

        return method + "\n" + url + "\n" + secret + "\n" + nonce + "\n" + timestamp + "\n" + bodyHash + "\n";
    }

    /**
     * 计算请求报文的哈希值
     * @param request
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    private String getBodyHash(String request) throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(request.getBytes());
        byte[] hash = md.digest();
        return Hex.encodeHexString(hash);
    }

    /**
     * 计算给定字符串(str)基于指定密钥(key)的HMAC - SHA256值
     * @param str
     * @param key
     * @return
     * @throws Exception
     */
    private String toHMAC_SHA256(String str, String key) throws Exception {
        byte[] secret = key.getBytes(StandardCharsets.UTF_8);
        SecretKeySpec secretKey = new SecretKeySpec(secret, "HmacSHA256");
        Mac mac = Mac.getInstance(secretKey.getAlgorithm());
        mac.init(secretKey);
        byte[] macData = mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
        byte[] hex = (new Hex()).encode(macData);
        return new String(hex, StandardCharsets.UTF_8);
    }


    /**
     * 获取请求报文数据,
     * @param request
     * @return
     * @throws IOException
     * todo 需要注意 `request.getInputStream()` 只能读一次
     */
    public static String getRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        ServletInputStream inputStream = null;
        BufferedReader bufferedReader = null;

        try {
            inputStream = request.getInputStream();
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

            char[] charBuffer = new char[128];
            int bytesRead;

            while ((bytesRead = bufferedReader.read(charBuffer)) != -1) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
        } finally {
            if (bufferedReader != null) {
                bufferedReader.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        }

        return stringBuilder.toString();
    }
    /**
     *  响应报文的二次封装处理
     */
    private static class EncryptingResponseWrapper extends HttpServletResponseWrapper {
        private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        private PrintWriter writer = new PrintWriter(outputStream);

        public EncryptingResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return new EncryptingServletOutputStream(outputStream);
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            return writer;
        }

        public String getResponseData() throws IOException {
            writer.flush();
            return outputStream.toString(StandardCharsets.UTF_8.name());
        }
    }

    private static class EncryptingServletOutputStream extends ServletOutputStream {
        private final ByteArrayOutputStream byteArrayOutputStream;

        public EncryptingServletOutputStream(ByteArrayOutputStream byteArrayOutputStream) {
            this.byteArrayOutputStream = byteArrayOutputStream;
        }

        @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);
        }

        @Override
        public void flush() throws IOException {
            byteArrayOutputStream.flush();
        }

        @Override
        public void close() throws IOException {
            byteArrayOutputStream.close();
        }

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

        @Override
        public void setWriteListener(WriteListener writeListener) {

        }
    }
    /**
     *  请求报文的二次封装处理
     */

    public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {

        private  final ServletInputStream inputStream;

        // 构造器接受 HttpServletRequest 和请求体字节数组
        public CustomHttpServletRequestWrapper(HttpServletRequest request, byte[] body) throws IOException {
            super(request);
            this.inputStream = new ByteArrayServletInputStream(body);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            // 返回自定义的 ByteArrayInputStream,读取请求体内容
            return inputStream;
        }

        // 如果需要也可以重写 getReader() 方法,以便读取请求体作为字符流
    }

    public class ByteArrayServletInputStream extends ServletInputStream {

        private final ByteArrayInputStream byteArrayInputStream;

        public ByteArrayServletInputStream(byte[] data) {
            this.byteArrayInputStream = new ByteArrayInputStream(data);
        }

        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }

        @Override
        public int read(byte[] b) throws IOException {
            return byteArrayInputStream.read(b);
        }

        @Override
        public long skip(long n) throws IOException {
            return byteArrayInputStream.skip(n);
        }

        @Override
        public int available() throws IOException {
            return byteArrayInputStream.available();
        }

        @Override
        public void close() throws IOException {
            byteArrayInputStream.close();
        }

        @Override
        public synchronized void mark(int readlimit) {
            byteArrayInputStream.mark(readlimit);
        }

        @Override
        public synchronized void reset() throws IOException {
            byteArrayInputStream.reset();
        }

        @Override
        public boolean markSupported() {
            return byteArrayInputStream.markSupported();
        }

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

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

        @Override
        public void setReadListener(ReadListener readListener) {

        }
    }

}

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 😃



© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)


http://www.kler.cn/a/539108.html

相关文章:

  • 海量表格文字识别、PHP表格识别接口:从传统到智能
  • 【设计模式】【行为型模式】职责链模式(Chain of Responsibility)
  • 学习总结二十九
  • openAI官方prompt技巧(一)
  • 学习数据结构(6)单链表OJ上
  • 音频进阶学习十一——离散傅里叶级数DFS
  • Python基础语法精要
  • 笔记:理解借贷相等的公式
  • Linux debugfs虚拟文件系统
  • COBOL语言的安全开发
  • 背包问题1
  • 交易一张股指期货需要多少钱?
  • Snipaste 截图软件下载与使用教程:附百度网盘地址
  • Leetcode 3449. Maximize the Minimum Game Score
  • 【MQ】Spring3 中 RabbitMQ 的使用与常见场景
  • 2025.2.9机器学习笔记:PINN文献阅读
  • excel拆分表格
  • Processing P5js姓氏数据可视化项目
  • Maven 与企业项目的集成
  • python--sqlite
  • K8s —基础指南(K8s - Basic Guide)
  • DeepSeek本地安装+集成VScode使用
  • LM Studio本地调用模型的方法
  • rockmq配置出现的问题
  • 表单配置化方案:Formily
  • 攻防世界32 very_easy_sql