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
协议,其工作流程如下:
- 建立 TCP 连接: 客户端与服务器通过 TCP 三次握手建立连接。
- 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==
.......................................
服务端收到报文,对请求报文做解密处理,同时对响应报文做加密处理
- 加载本地的非对称加密的私钥
- 判断报文头数据是否存在,同时从报文头获取需要的数据(X-Secret,X-Nonce,X-Timestamp,X-Signature)
- 判断时间戳是否符合要求
- 通过非对称加密的私钥对对称加密的密钥进行解密,获取对称加密的密钥
HMAC
校验,重新生成签名判断是否一致- 解密请求报文给后端接口处理
- 接口处理完成返回响应,通过 对称加密的密钥对响应报文进行加密
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
项目的接口加密实现服务端的编码
- 在
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);
// 添加多因素认证
。。。。。。。。
- 处理请求报文的解密,响应报文的加密
过滤器方法,这里利用 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));
}
需要注意的问题:
request.getInputStream()
只能读一次,并且在过滤器链里面使用,处理完请求报文,还要在塞回去。- 请求报文和响应报文的二次封装通过内部类
EncryptingResponseWrapper
和CustomHttpServletRequestWrapper
实现 - 对于加密和解密使用的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)