From be3ee886c0adc4dbf9dcefe58e43e9cb5362911f Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 10 Oct 2024 22:24:51 +0800 Subject: [PATCH 1/3] fix bug --- .../core/io/stream/SyncInputStream.java | 14 ++--- .../dromara/hutool/http/client/Response.java | 18 +++++-- .../hutool/http/client/body/ResponseBody.java | 10 ++++ .../engine/httpclient4/HttpClient4Engine.java | 6 ++- .../httpclient4/HttpClient4Response.java | 35 ++++++++---- .../engine/httpclient5/HttpClient5Engine.java | 6 ++- .../httpclient5/HttpClient5Response.java | 46 ++++++++++------ .../client/engine/jdk/JdkClientEngine.java | 25 +++------ .../client/engine/jdk/JdkHttpResponse.java | 54 +++++++------------ .../client/engine/okhttp/OkHttpEngine.java | 2 +- .../client/engine/okhttp/OkHttpResponse.java | 37 +++++++++---- .../hutool/http/client/Issue3765Test.java | 41 ++++++++++++++ 12 files changed, 190 insertions(+), 104 deletions(-) create mode 100644 hutool-http/src/test/java/org/dromara/hutool/http/client/Issue3765Test.java diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/SyncInputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/SyncInputStream.java index 1b41760c2..c2e8e3e12 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/SyncInputStream.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/SyncInputStream.java @@ -58,7 +58,8 @@ public class SyncInputStream extends FilterInputStream { } /** - * 同步数据到内存 + * 同步数据到内存,同步后关闭原流 + * * @return this */ public SyncInputStream sync() { @@ -76,18 +77,19 @@ public class SyncInputStream extends FilterInputStream { * @return bytes */ public byte[] readBytes() { - final FastByteArrayOutputStream bytesOut = new FastByteArrayOutputStream(length > 0 ? (int)length : 1024); + final FastByteArrayOutputStream bytesOut = new FastByteArrayOutputStream(length > 0 ? (int) length : 1024); final long length = copyTo(bytesOut, null); return length > 0 ? bytesOut.toByteArray() : new byte[0]; } /** - * 将流的内容拷贝到输出流 - * @param out 输出流 + * 将流的内容拷贝到输出流,拷贝结束后关闭输入流 + * + * @param out 输出流 * @param streamProgress 进度条 * @return 拷贝长度 */ - public long copyTo(final OutputStream out, final StreamProgress streamProgress){ + public long copyTo(final OutputStream out, final StreamProgress streamProgress) { long copyLength = -1; try { copyLength = IoUtil.copy(this.in, out, IoUtil.DEFAULT_BUFFER_SIZE, this.length, streamProgress); @@ -96,7 +98,7 @@ public class SyncInputStream extends FilterInputStream { throw e; } // 忽略读取流中的EOF错误 - }finally { + } finally { // 读取结束 IoUtil.closeQuietly(in); } diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java index 097efacb4..a9545b361 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java @@ -82,13 +82,21 @@ public interface Response extends Closeable { InputStream bodyStream(); /** - * 获取响应体,包含服务端返回的内容和Content-Type信息 + * 同步
+ * 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
+ * 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容(内存)中。如果已经是同步状态,则不进行任何操作。 + * + * @return this + */ + Response sync(); + + /** + * 获取响应体,包含服务端返回的内容和Content-Type信息
+ * 如果为HEAD、CONNECT、TRACE等方法无响应体,则返回{@code null} * * @return {@link ResponseBody} */ - default ResponseBody body() { - return new ResponseBody(this, bodyStream(), false, true); - } + ResponseBody body(); /** * 获取响应主体 @@ -112,7 +120,7 @@ public interface Response extends Closeable { */ default byte[] bodyBytes() { try (final ResponseBody body = body()) { - return body.getBytes(); + return null == body ? null : body.getBytes(); } catch (final IOException e) { throw new IORuntimeException(e); } diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java index 974962885..8163f99fe 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java @@ -44,6 +44,16 @@ public class ResponseBody implements HttpBody, Closeable { */ private final SyncInputStream bodyStream; + /** + * 构造,不读取响应体,忽略响应体EOF错误 + * + * @param response 响应体 + * @param in HTTP主体响应流 + */ + public ResponseBody(final Response response, final InputStream in) { + this(response, in, true, true); + } + /** * 构造 * diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java index eb86f796a..4e724002b 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Engine.java @@ -21,6 +21,7 @@ import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; @@ -75,11 +76,14 @@ public class HttpClient4Engine extends AbstractClientEngine { initEngine(); final HttpUriRequest request = buildRequest(message); + final CloseableHttpResponse response; try { - return this.engine.execute(request, response -> new HttpClient4Response(response, message.charset())); + //return this.engine.execute(request, response -> new HttpClient4Response(response, message)); + response = this.engine.execute(request); } catch (final IOException e) { throw new HttpException(e); } + return new HttpClient4Response(response, message); } @Override diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Response.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Response.java index c3ff0a554..ce2537603 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Response.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient4/HttpClient4Response.java @@ -24,13 +24,15 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.util.EntityUtils; import org.dromara.hutool.core.array.ArrayUtil; import org.dromara.hutool.core.io.IORuntimeException; +import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.lang.wrapper.SimpleWrapper; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.HttpUtil; +import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.Response; +import org.dromara.hutool.http.client.body.ResponseBody; -import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; @@ -52,18 +54,20 @@ public class HttpClient4Response extends SimpleWrapper implements * 请求时的默认编码 */ private final Charset requestCharset; + private final ResponseBody body; /** * 构造
* 通过传入一个请求时的编码,当无法获取响应内容的编码时,默认使用响应时的编码 * - * @param rawRes {@link HttpResponse} - * @param requestCharset 请求时的编码 + * @param rawRes {@link HttpResponse} + * @param message 请求消息 */ - public HttpClient4Response(final HttpResponse rawRes, final Charset requestCharset) { + public HttpClient4Response(final HttpResponse rawRes, final Request message) { super(rawRes); this.entity = rawRes.getEntity(); - this.requestCharset = requestCharset; + this.requestCharset = message.charset(); + this.body = message.method().isIgnoreBody() ? null : new ResponseBody(this, bodyStream()); } @@ -112,6 +116,21 @@ public class HttpClient4Response extends SimpleWrapper implements } } + @Override + public HttpClient4Response sync() { + final ResponseBody body = this.body; + if(null != body){ + body.sync(); + } + IoUtil.closeIfPossible(this.raw); + return this; + } + + @Override + public ResponseBody body() { + return this.body; + } + @Override public String bodyStr() throws HttpException { try { @@ -124,10 +143,8 @@ public class HttpClient4Response extends SimpleWrapper implements } @Override - public void close() throws IOException { - if(this.raw instanceof Closeable){ - ((Closeable) this.raw).close(); - } + public void close() { + IoUtil.closeIfPossible(this.raw); } @Override diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java index e91ca3512..cbb735a1a 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Engine.java @@ -29,6 +29,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.message.BasicHeader; @@ -81,11 +82,14 @@ public class HttpClient5Engine extends AbstractClientEngine { initEngine(); final ClassicHttpRequest request = buildRequest(message); + final ClassicHttpResponse response; try { - return this.engine.execute(request, (response -> new HttpClient5Response(response, message.charset()))); + //return this.engine.execute(request, (response -> new HttpClient5Response(response, message))); + response = this.engine.executeOpen(null, request, null); } catch (final IOException e) { throw new HttpException(e); } + return new HttpClient5Response(response, message); } @Override diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Response.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Response.java index 57c002bd5..f21b08a4a 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Response.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/httpclient5/HttpClient5Response.java @@ -16,28 +16,27 @@ package org.dromara.hutool.http.client.engine.httpclient5; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; -import org.dromara.hutool.core.io.IORuntimeException; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; import org.dromara.hutool.core.array.ArrayUtil; +import org.dromara.hutool.core.io.IORuntimeException; +import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.lang.wrapper.SimpleWrapper; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.HttpUtil; +import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.Response; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.dromara.hutool.http.client.body.ResponseBody; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * HttpClient响应包装
@@ -52,18 +51,20 @@ public class HttpClient5Response extends SimpleWrapper impl * 请求时的默认编码 */ private final Charset requestCharset; + private final ResponseBody body; /** * 构造
* 通过传入一个请求时的编码,当无法获取响应内容的编码时,默认使用响应时的编码 * - * @param rawRes {@link CloseableHttpResponse} - * @param requestCharset 请求时的编码 + * @param rawRes {@link CloseableHttpResponse} + * @param message 请求消息 */ - public HttpClient5Response(final ClassicHttpResponse rawRes, final Charset requestCharset) { + public HttpClient5Response(final ClassicHttpResponse rawRes, final Request message) { super(rawRes); this.entity = rawRes.getEntity(); - this.requestCharset = requestCharset; + this.requestCharset = message.charset(); + this.body = message.method().isIgnoreBody() ? null : new ResponseBody(this, bodyStream()); } @@ -112,6 +113,21 @@ public class HttpClient5Response extends SimpleWrapper impl } } + @Override + public HttpClient5Response sync() { + final ResponseBody body = this.body; + if(null != body){ + body.sync(); + } + IoUtil.closeQuietly(this.raw); + return this; + } + + @Override + public ResponseBody body() { + return this.body; + } + @Override public String bodyStr() throws HttpException { try { diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java index dfed64f97..53626c693 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java @@ -26,7 +26,6 @@ import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.HttpUtil; import org.dromara.hutool.http.client.ClientConfig; import org.dromara.hutool.http.client.Request; -import org.dromara.hutool.http.client.Response; import org.dromara.hutool.http.client.body.HttpBody; import org.dromara.hutool.http.client.engine.AbstractClientEngine; import org.dromara.hutool.http.meta.HeaderName; @@ -65,18 +64,7 @@ public class JdkClientEngine extends AbstractClientEngine { } @Override - public Response send(final Request message) { - return send(message, true); - } - - /** - * 发送请求 - * - * @param message 请求消息 - * @param isAsync 是否异步,异步不会立即读取响应内容 - * @return {@link Response} - */ - public JdkHttpResponse send(final Request message, final boolean isAsync) { + public JdkHttpResponse send(final Request message) { final JdkHttpConnection conn = buildConn(message); try { doSend(conn, message); @@ -86,7 +74,7 @@ public class JdkClientEngine extends AbstractClientEngine { throw new IORuntimeException(e); } - return sendRedirectIfPossible(conn, message, isAsync); + return sendRedirectIfPossible(conn, message); } @Override @@ -173,11 +161,10 @@ public class JdkClientEngine extends AbstractClientEngine { /** * 调用转发,如果需要转发返回转发结果,否则返回{@code null} * - * @param conn {@link JdkHttpConnection}} - * @param isAsync 最终请求是否异步 + * @param conn {@link JdkHttpConnection}} * @return {@link JdkHttpResponse},无转发返回 {@code null} */ - private JdkHttpResponse sendRedirectIfPossible(final JdkHttpConnection conn, final Request message, final boolean isAsync) { + private JdkHttpResponse sendRedirectIfPossible(final JdkHttpConnection conn, final Request message) { // 手动实现重定向 if (message.maxRedirects() > 0) { final int code; @@ -203,14 +190,14 @@ public class JdkClientEngine extends AbstractClientEngine { if (conn.redirectCount < message.maxRedirects()) { conn.redirectCount++; - return send(message, isAsync); + return send(message); } } } } // 最终页面 - return new JdkHttpResponse(conn, this.cookieManager, true, message.charset(), isAsync, message.method().isIgnoreBody()); + return new JdkHttpResponse(conn, this.cookieManager, message); } /** diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpResponse.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpResponse.java index 9cdd0d3d0..df341f879 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpResponse.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkHttpResponse.java @@ -21,6 +21,7 @@ import org.dromara.hutool.core.io.stream.EmptyInputStream; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.HttpUtil; +import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.http.client.body.ResponseBody; import org.dromara.hutool.http.meta.HeaderName; @@ -42,6 +43,10 @@ import java.util.Map; */ public class JdkHttpResponse implements Response, Closeable { + /** + * 持有连接对象 + */ + protected JdkHttpConnection httpConnection; /** * 请求时的默认编码 */ @@ -50,26 +55,14 @@ public class JdkHttpResponse implements Response, Closeable { * Cookie管理器 */ private final JdkCookieManager cookieManager; - /** - * 响应内容体,{@code null} 表示无内容 - */ - private ResponseBody body; /** * 响应头 */ private Map> headers; - /** - * 是否忽略响应读取时可能的EOF异常。
- * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
- * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。 + * 响应内容体,{@code null} 表示无内容 */ - private final boolean ignoreEOFError; - - /** - * 持有连接对象 - */ - protected JdkHttpConnection httpConnection; + private ResponseBody body; /** * 响应状态码 */ @@ -80,22 +73,15 @@ public class JdkHttpResponse implements Response, Closeable { * * @param httpConnection {@link JdkHttpConnection} * @param cookieManager Cookie管理器 - * @param ignoreEOFError 是否忽略响应读取时可能的EOF异常 - * @param requestCharset 编码,从请求编码中获取默认编码 - * @param isAsync 是否异步 - * @param isIgnoreBody 是否忽略读取响应体 + * @param message 请求消息 */ protected JdkHttpResponse(final JdkHttpConnection httpConnection, final JdkCookieManager cookieManager, - final boolean ignoreEOFError, - final Charset requestCharset, - final boolean isAsync, - final boolean isIgnoreBody) { + final Request message) { this.httpConnection = httpConnection; this.cookieManager = cookieManager; - this.ignoreEOFError = ignoreEOFError; - this.requestCharset = requestCharset; - init(isAsync, isIgnoreBody); + this.requestCharset = message.charset(); + init(message.method().isIgnoreBody()); } /** @@ -128,18 +114,13 @@ public class JdkHttpResponse implements Response, Closeable { return ObjUtil.defaultIfNull(Response.super.charset(), requestCharset); } - /** - * 同步
- * 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
- * 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态,则不进行任何操作。 - * - * @return this - */ + @Override public JdkHttpResponse sync() { if (null != this.body) { this.body.sync(); } - close(); + // 关闭连接 + this.httpConnection.closeQuietly(); return this; } @@ -250,9 +231,10 @@ public class JdkHttpResponse implements Response, Closeable { * 3、持有Http流,并不关闭流 * * + * @param isIgnoreBody 是否忽略消息体 * @throws HttpException IO异常 */ - private void init(final boolean isAsync, final boolean isIgnoreBody) throws HttpException { + private void init(final boolean isIgnoreBody) throws HttpException { // 获取响应状态码 try { this.status = httpConnection.getCode(); @@ -272,13 +254,13 @@ public class JdkHttpResponse implements Response, Closeable { } // 存储服务端设置的Cookie信息 - if(null != this.cookieManager){ + if (null != this.cookieManager) { this.cookieManager.saveFromResponse(this.httpConnection, this.headers); } // 获取响应内容流 if (!isIgnoreBody) { - this.body = new ResponseBody(this, new JdkHttpInputStream(this), isAsync, this.ignoreEOFError); + this.body = new ResponseBody(this, new JdkHttpInputStream(this)); } } // ---------------------------------------------------------------- Private method end diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java index da84d98fc..3aa5dac82 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java @@ -71,7 +71,7 @@ public class OkHttpEngine extends AbstractClientEngine { throw new IORuntimeException(e); } - return new OkHttpResponse(response, message.charset()); + return new OkHttpResponse(response, message); } @Override diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpResponse.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpResponse.java index 1ea5ca628..feb553f58 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpResponse.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpResponse.java @@ -16,19 +16,20 @@ package org.dromara.hutool.http.client.engine.okhttp; -import kotlin.Pair; -import okhttp3.Headers; -import okhttp3.ResponseBody; +import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.stream.EmptyInputStream; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.GlobalCompressStreamRegister; import org.dromara.hutool.http.HttpUtil; +import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.Response; +import org.dromara.hutool.http.client.body.ResponseBody; import org.dromara.hutool.http.meta.HeaderName; import java.io.InputStream; import java.nio.charset.Charset; -import java.util.*; +import java.util.List; +import java.util.Map; /** * OkHttp3的{@link okhttp3.Response} 响应包装 @@ -42,14 +43,16 @@ public class OkHttpResponse implements Response { * 请求时的默认编码 */ private final Charset requestCharset; + private final ResponseBody body; /** * @param rawRes {@link okhttp3.Response} - * @param requestCharset 请求时的默认编码 + * @param message 请求对象 */ - public OkHttpResponse(final okhttp3.Response rawRes, final Charset requestCharset) { + public OkHttpResponse(final okhttp3.Response rawRes, final Request message) { this.rawRes = rawRes; - this.requestCharset = requestCharset; + this.requestCharset = message.charset(); + this.body = message.method().isIgnoreBody() ? null : new ResponseBody(this, bodyStream()); } @Override @@ -74,7 +77,7 @@ public class OkHttpResponse implements Response { @Override public InputStream bodyStream() { - final ResponseBody body = rawRes.body(); + final okhttp3.ResponseBody body = rawRes.body(); if(null == body){ return EmptyInputStream.INSTANCE; } @@ -84,10 +87,22 @@ public class OkHttpResponse implements Response { } @Override - public void close() { - if(null != this.rawRes){ - rawRes.close(); + public OkHttpResponse sync() { + if (null != this.body) { + this.body.sync(); } + IoUtil.closeQuietly(this.rawRes); + return this; + } + + @Override + public ResponseBody body() { + return this.body; + } + + @Override + public void close() { + IoUtil.closeQuietly(this.rawRes); } @Override diff --git a/hutool-http/src/test/java/org/dromara/hutool/http/client/Issue3765Test.java b/hutool-http/src/test/java/org/dromara/hutool/http/client/Issue3765Test.java new file mode 100644 index 000000000..a8b207704 --- /dev/null +++ b/hutool-http/src/test/java/org/dromara/hutool/http/client/Issue3765Test.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.http.client; + +import org.dromara.hutool.http.HttpUtil; +import org.dromara.hutool.http.client.engine.ClientEngine; +import org.dromara.hutool.http.client.engine.ClientEngineFactory; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class Issue3765Test { + + public static void main(String[] args) { + HttpUtil.createServer(8888) + .setRoot("d:/test/www") + .start(); + } + @Test + @Disabled + void downloadTest() { + final String url = "http://localhost:8888/a.mp3"; + final ClientEngine engine = ClientEngineFactory.createEngine("httpclient4"); + Request.of(url) + .send(engine) + .body().write("d:/test/"); + } +} From 3227ea0979799f931ec26d0092b91a311e55ffd8 Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 10 Oct 2024 23:45:30 +0800 Subject: [PATCH 2/3] add masking --- .../dromara/hutool/core/data/MaskingUtil.java | 258 ++----------- .../core/data/masking/MaskingHandler.java | 33 ++ .../core/data/masking/MaskingManager.java | 345 ++++++++++++++++++ .../hutool/core/data/masking/MaskingType.java | 85 +++++ .../core/data/masking/package-info.java | 22 ++ .../hutool/core/data/MaskingUtilTest.java | 108 +++--- 6 files changed, 587 insertions(+), 264 deletions(-) create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java index 41a934751..df4a71ad7 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java @@ -16,8 +16,9 @@ package org.dromara.hutool.core.data; +import org.dromara.hutool.core.data.masking.MaskingManager; +import org.dromara.hutool.core.data.masking.MaskingType; import org.dromara.hutool.core.text.StrUtil; -import org.dromara.hutool.core.text.CharUtil; /** * 数据脱敏(Data Masking)工具类,对某些敏感信息(比如,身份证号、手机号、卡号、姓名、地址、邮箱等 )屏蔽敏感数据。
@@ -44,74 +45,6 @@ import org.dromara.hutool.core.text.CharUtil; */ public class MaskingUtil { - /** - * 支持的脱敏类型枚举 - * - * @author dazer and neusoft and qiaomu - */ - public enum MaskingType { - /** - * 用户id - */ - USER_ID, - /** - * 中文名 - */ - CHINESE_NAME, - /** - * 身份证号 - */ - ID_CARD, - /** - * 座机号 - */ - FIXED_PHONE, - /** - * 手机号 - */ - MOBILE_PHONE, - /** - * 地址 - */ - ADDRESS, - /** - * 电子邮件 - */ - EMAIL, - /** - * 密码 - */ - PASSWORD, - /** - * 中国大陆车牌,包含普通车辆、新能源车辆 - */ - CAR_LICENSE, - /** - * 银行卡 - */ - BANK_CARD, - /** - * IPv4地址 - */ - IPV4, - /** - * IPv6地址 - */ - IPV6, - /** - * 定义了一个first_mask的规则,只显示第一个字符。 - */ - FIRST_MASK, - /** - * 清空为null - */ - CLEAR_TO_NULL, - /** - * 清空为"" - */ - CLEAR_TO_EMPTY - } - /** * 脱敏,使用默认的脱敏策略 *
@@ -134,60 +67,8 @@ public class MaskingUtil {
 	 * @author dazer and neusoft and qiaomu
 	 * @since 5.6.2
 	 */
-	public static String masking(final CharSequence str, final MaskingType maskingType) {
-		if (StrUtil.isBlank(str)) {
-			return StrUtil.EMPTY;
-		}
-		String newStr = String.valueOf(str);
-		switch (maskingType) {
-			case USER_ID:
-				newStr = String.valueOf(userId());
-				break;
-			case CHINESE_NAME:
-				newStr = chineseName(String.valueOf(str));
-				break;
-			case ID_CARD:
-				newStr = idCardNum(String.valueOf(str), 1, 2);
-				break;
-			case FIXED_PHONE:
-				newStr = fixedPhone(String.valueOf(str));
-				break;
-			case MOBILE_PHONE:
-				newStr = mobilePhone(String.valueOf(str));
-				break;
-			case ADDRESS:
-				newStr = address(String.valueOf(str), 8);
-				break;
-			case EMAIL:
-				newStr = email(String.valueOf(str));
-				break;
-			case PASSWORD:
-				newStr = password(String.valueOf(str));
-				break;
-			case CAR_LICENSE:
-				newStr = carLicense(String.valueOf(str));
-				break;
-			case BANK_CARD:
-				newStr = bankCard(String.valueOf(str));
-				break;
-			case IPV4:
-				newStr = ipv4(String.valueOf(str));
-				break;
-			case IPV6:
-				newStr = ipv6(String.valueOf(str));
-				break;
-			case FIRST_MASK:
-				newStr = firstMask(String.valueOf(str));
-				break;
-			case CLEAR_TO_EMPTY:
-				newStr = clear();
-				break;
-			case CLEAR_TO_NULL:
-				newStr = clearToNull();
-				break;
-			default:
-		}
-		return newStr;
+	public static String masking(final MaskingType maskingType, final CharSequence str) {
+		return MaskingManager.getInstance().masking(maskingType.name(), str);
 	}
 
 	/**
@@ -219,6 +100,16 @@ public class MaskingUtil {
 		return 0L;
 	}
 
+	/**
+	 * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
+	 *
+	 * @param fullName 姓名
+	 * @return 脱敏后的姓名
+	 */
+	public static String chineseName(final CharSequence fullName) {
+		return firstMask(fullName);
+	}
+
 	/**
 	 * 定义了一个first_mask的规则,只显示第一个字符。
* 脱敏前:123456789;脱敏后:1********。 @@ -226,21 +117,8 @@ public class MaskingUtil { * @param str 字符串 * @return 脱敏后的字符串 */ - public static String firstMask(final String str) { - if (StrUtil.isBlank(str)) { - return StrUtil.EMPTY; - } - return StrUtil.hide(str, 1, str.length()); - } - - /** - * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李** - * - * @param fullName 姓名 - * @return 脱敏后的姓名 - */ - public static String chineseName(final String fullName) { - return firstMask(fullName); + public static String firstMask(final CharSequence str) { + return MaskingManager.EMPTY.firstMask(str); } /** @@ -251,20 +129,8 @@ public class MaskingUtil { * @param end 保留:后面的end位数;从1开始 * @return 脱敏后的身份证 */ - public static String idCardNum(final String idCardNum, final int front, final int end) { - //身份证不能为空 - if (StrUtil.isBlank(idCardNum)) { - return StrUtil.EMPTY; - } - //需要截取的长度不能大于身份证号长度 - if ((front + end) > idCardNum.length()) { - return StrUtil.EMPTY; - } - //需要截取的不能小于0 - if (front < 0 || end < 0) { - return StrUtil.EMPTY; - } - return StrUtil.hide(idCardNum, front, idCardNum.length() - end); + public static String idCardNum(final CharSequence idCardNum, final int front, final int end) { + return MaskingManager.EMPTY.idCardNum(idCardNum, front, end); } /** @@ -273,11 +139,8 @@ public class MaskingUtil { * @param num 固定电话 * @return 脱敏后的固定电话; */ - public static String fixedPhone(final String num) { - if (StrUtil.isBlank(num)) { - return StrUtil.EMPTY; - } - return StrUtil.hide(num, 4, num.length() - 2); + public static String fixedPhone(final CharSequence num) { + return MaskingManager.EMPTY.fixedPhone(num); } /** @@ -286,11 +149,8 @@ public class MaskingUtil { * @param num 移动电话; * @return 脱敏后的移动电话; */ - public static String mobilePhone(final String num) { - if (StrUtil.isBlank(num)) { - return StrUtil.EMPTY; - } - return StrUtil.hide(num, 3, num.length() - 4); + public static String mobilePhone(final CharSequence num) { + return MaskingManager.EMPTY.mobilePhone(num); } /** @@ -300,12 +160,8 @@ public class MaskingUtil { * @param sensitiveSize 敏感信息长度 * @return 脱敏后的家庭地址 */ - public static String address(final String address, final int sensitiveSize) { - if (StrUtil.isBlank(address)) { - return StrUtil.EMPTY; - } - final int length = address.length(); - return StrUtil.hide(address, length - sensitiveSize, length); + public static String address(final CharSequence address, final int sensitiveSize) { + return MaskingManager.EMPTY.address(address, sensitiveSize); } /** @@ -314,28 +170,23 @@ public class MaskingUtil { * @param email 邮箱 * @return 脱敏后的邮箱 */ - public static String email(final String email) { - if (StrUtil.isBlank(email)) { - return StrUtil.EMPTY; - } - final int index = StrUtil.indexOf(email, '@'); - if (index <= 1) { - return email; - } - return StrUtil.hide(email, 1, index); + public static String email(final CharSequence email) { + return MaskingManager.EMPTY.email(email); } /** - * 【密码】密码的全部字符都用*代替,比如:****** + * 【密码】密码的全部字符都用*代替,比如:******
+ * 密码位数不能被猜测,因此固定10位 * * @param password 密码 * @return 脱敏后的密码 */ - public static String password(final String password) { + public static String password(final CharSequence password) { if (StrUtil.isBlank(password)) { return StrUtil.EMPTY; } - return StrUtil.repeat('*', password.length()); + // 密码位数不能被猜测,因此固定10位 + return StrUtil.repeat('*', 10); } /** @@ -349,18 +200,8 @@ public class MaskingUtil { * @param carLicense 完整的车牌号 * @return 脱敏后的车牌 */ - public static String carLicense(String carLicense) { - if (StrUtil.isBlank(carLicense)) { - return StrUtil.EMPTY; - } - // 普通车牌 - if (carLicense.length() == 7) { - carLicense = StrUtil.hide(carLicense, 3, 6); - } else if (carLicense.length() == 8) { - // 新能源车牌 - carLicense = StrUtil.hide(carLicense, 3, 7); - } - return carLicense; + public static String carLicense(final CharSequence carLicense) { + return MaskingManager.EMPTY.carLicense(carLicense); } /** @@ -371,29 +212,8 @@ public class MaskingUtil { * @return 脱敏之后的银行卡号 * @since 5.6.3 */ - public static String bankCard(String bankCardNo) { - if (StrUtil.isBlank(bankCardNo)) { - return bankCardNo; - } - bankCardNo = StrUtil.cleanBlank(bankCardNo); - if (bankCardNo.length() < 9) { - return bankCardNo; - } - - final int length = bankCardNo.length(); - final int endLength = length % 4 == 0 ? 4 : length % 4; - final int midLength = length - 4 - endLength; - final StringBuilder buf = new StringBuilder(); - - buf.append(bankCardNo, 0, 4); - for (int i = 0; i < midLength; ++i) { - if (i % 4 == 0) { - buf.append(CharUtil.SPACE); - } - buf.append('*'); - } - buf.append(CharUtil.SPACE).append(bankCardNo, length - endLength, length); - return buf.toString(); + public static String bankCard(final CharSequence bankCardNo) { + return MaskingManager.EMPTY.bankCard(bankCardNo); } /** @@ -402,8 +222,8 @@ public class MaskingUtil { * @param ipv4 IPv4地址 * @return 脱敏后的地址 */ - public static String ipv4(final String ipv4) { - return StrUtil.subBefore(ipv4, '.', false) + ".*.*.*"; + public static String ipv4(final CharSequence ipv4) { + return MaskingManager.EMPTY.ipv4(ipv4); } /** @@ -412,7 +232,7 @@ public class MaskingUtil { * @param ipv6 IPv6地址 * @return 脱敏后的地址 */ - public static String ipv6(final String ipv6) { - return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*"; + public static String ipv6(final CharSequence ipv6) { + return MaskingManager.EMPTY.ipv6(ipv6); } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java new file mode 100644 index 000000000..d7d2ff733 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.core.data.masking; + +/** + * 脱敏处理器,用于自定义脱敏规则 + * + */ +@FunctionalInterface +public interface MaskingHandler { + + /** + * 处理传入的数据字符串,经过脱敏逻辑后,返回处理后的值 + * + * @param value 待处理的值 + * @return 处理后的值 + */ + String handle(CharSequence value); +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java new file mode 100644 index 000000000..67aef9d85 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.core.data.masking; + +import org.dromara.hutool.core.text.CharUtil; +import org.dromara.hutool.core.text.StrUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.dromara.hutool.core.data.masking.MaskingType.*; + +/** + * 脱敏管理器,用于管理所有脱敏处理器,使用方式有三种: + *
    + *
  • 全局默认:使用{@link MaskingManager#getInstance()},带有预定义的脱敏方法
  • + *
  • 自定义默认:使用{@link MaskingManager#ofDefault(char)},可以自定义脱敏字符,带有预定义的脱敏方法
  • + *
  • 自定义:使用{@link #MaskingManager(Map, char)}构造,不带有默认规则
  • + *
+ * + * @author Looly + */ +public class MaskingManager { + + /** + * 默认的脱敏字符:* + */ + public static final char DEFAULT_MASK_CHAR = '*'; + + /** + * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载 + */ + private static class SingletonHolder { + /** + * 静态初始化器,由JVM来保证线程安全 + */ + private static final MaskingManager INSTANCE = registerDefault(DEFAULT_MASK_CHAR); + } + + /** + * 空脱敏管理器,用于不处理任何数据的情况 + */ + public static final MaskingManager EMPTY = new MaskingManager(null); + + /** + * 获得单例的 MaskingManager + * + * @return MaskingManager + */ + public static MaskingManager getInstance() { + return SingletonHolder.INSTANCE; + } + + /** + * 创建默认的脱敏管理器,通过给定的脱敏字符,提供默认的脱敏规则 + * + * @param maskChar 脱敏字符,默认为* + * @return 默认的脱敏管理器 + */ + public static MaskingManager ofDefault(final char maskChar) { + return registerDefault(maskChar); + } + + private final Map handlerMap; + private final char maskChar; + + /** + * 构造 + * + * @param handlerMap 脱敏处理器Map,如果应用于单例,则需要传入线程安全的Map + */ + public MaskingManager(final Map handlerMap) { + this(handlerMap, DEFAULT_MASK_CHAR); + } + + /** + * 构造 + * + * @param handlerMap 脱敏处理器Map,如果应用于单例,则需要传入线程安全的Map + * @param maskChar 默认的脱敏字符,默认为* + */ + public MaskingManager(final Map handlerMap, final char maskChar) { + this.handlerMap = handlerMap; + this.maskChar = maskChar; + } + + /** + * 注册一个脱敏处理器 + * + * @param type 类型 + * @param handler 脱敏处理器 + * @return this + */ + public MaskingManager register(final String type, final MaskingHandler handler) { + this.handlerMap.put(type, handler); + return this; + } + + /** + * 脱敏处理
+ * 如果没有指定的脱敏处理器,则返回{@code null} + * + * @param type 类型 + * @param value 待脱敏值 + * @return 脱敏后的值 + */ + public String masking(String type, final CharSequence value) { + if (StrUtil.isEmpty(type)) { + type = CLEAR_TO_NULL.name(); + } + final MaskingHandler handler = handlerMap.get(type); + return null == handler ? null : handler.handle(value); + } + + /** + * 默认的脱敏处理器注册 + * + * @param maskChar 默认的脱敏字符,默认为* + * @return 默认的脱敏处理器注册 + */ + private static MaskingManager registerDefault(final char maskChar) { + final MaskingManager manager = new MaskingManager( + new ConcurrentHashMap<>(15, 1), maskChar); + + manager.register(USER_ID.name(), (str) -> "0"); + manager.register(CHINESE_NAME.name(), manager::firstMask); + manager.register(ID_CARD.name(), (str) -> manager.idCardNum(str, 1, 2)); + manager.register(FIXED_PHONE.name(), manager::fixedPhone); + manager.register(MOBILE_PHONE.name(), manager::mobilePhone); + manager.register(ADDRESS.name(), (str) -> manager.address(str, 8)); + manager.register(EMAIL.name(), manager::email); + manager.register(PASSWORD.name(), manager::password); + manager.register(CAR_LICENSE.name(), manager::carLicense); + manager.register(BANK_CARD.name(), manager::bankCard); + manager.register(IPV4.name(), manager::ipv4); + manager.register(IPV6.name(), manager::ipv6); + manager.register(FIRST_MASK.name(), manager::firstMask); + manager.register(CLEAR_TO_EMPTY.name(), (str) -> StrUtil.EMPTY); + manager.register(CLEAR_TO_NULL.name(), (str) -> null); + + return manager; + } + + /** + * 定义了一个first_mask的规则,只显示第一个字符。
+ * 脱敏前:123456789;脱敏后:1********。 + * + * @param str 字符串 + * @return 脱敏后的字符串 + */ + public String firstMask(final CharSequence str) { + if (StrUtil.isBlank(str)) { + return StrUtil.EMPTY; + } + return StrUtil.replaceByCodePoint(str, 1, str.length(), maskChar); + } + + /** + * 【身份证号】前1位 和后2位 + * + * @param idCardNum 身份证 + * @param front 保留:前面的front位数;从1开始 + * @param end 保留:后面的end位数;从1开始 + * @return 脱敏后的身份证 + */ + public String idCardNum(final CharSequence idCardNum, final int front, final int end) { + //身份证不能为空 + if (StrUtil.isBlank(idCardNum)) { + return StrUtil.EMPTY; + } + //需要截取的长度不能大于身份证号长度 + if ((front + end) > idCardNum.length()) { + return StrUtil.EMPTY; + } + //需要截取的不能小于0 + if (front < 0 || end < 0) { + return StrUtil.EMPTY; + } + return StrUtil.replaceByCodePoint(idCardNum, front, idCardNum.length() - end, maskChar); + } + + /** + * 【固定电话 前四位,后两位 + * + * @param num 固定电话 + * @return 脱敏后的固定电话; + */ + public String fixedPhone(final CharSequence num) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replaceByCodePoint(num, 4, num.length() - 2, maskChar); + } + + /** + * 【手机号码】前三位,后4位,其他隐藏,比如135****2210 + * + * @param num 移动电话; + * @return 脱敏后的移动电话; + */ + public String mobilePhone(final CharSequence num) { + if (StrUtil.isBlank(num)) { + return StrUtil.EMPTY; + } + return StrUtil.replaceByCodePoint(num, 3, num.length() - 4, maskChar); + } + + /** + * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区**** + * + * @param address 家庭住址 + * @param sensitiveSize 敏感信息长度 + * @return 脱敏后的家庭地址 + */ + public String address(final CharSequence address, final int sensitiveSize) { + if (StrUtil.isBlank(address)) { + return StrUtil.EMPTY; + } + final int length = address.length(); + return StrUtil.replaceByCodePoint(address, length - sensitiveSize, length, maskChar); + } + + /** + * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com + * + * @param email 邮箱 + * @return 脱敏后的邮箱 + */ + public String email(final CharSequence email) { + if (StrUtil.isBlank(email)) { + return StrUtil.EMPTY; + } + final int index = StrUtil.indexOf(email, '@'); + if (index <= 1) { + return email.toString(); + } + return StrUtil.replaceByCodePoint(email, 1, index, maskChar); + } + + /** + * 【密码】密码的全部字符都用定义的脱敏字符(如*)代替,比如:******
+ * 密码位数不能被猜测,因此固定10位 + * + * @param password 密码 + * @return 脱敏后的密码 + */ + public String password(final CharSequence password) { + if (StrUtil.isBlank(password)) { + return StrUtil.EMPTY; + } + // 密码位数不能被猜测,因此固定10位 + return StrUtil.repeat(maskChar, 10); + } + + /** + * 【中国车牌】车牌中间用脱敏字符(如*)代替 + * eg1:null -》 "" + * eg1:"" -》 "" + * eg3:苏D40000 -》 苏D4***0 + * eg4:陕A12345D -》 陕A1****D + * eg5:京A123 -》 京A123 如果是错误的车牌,不处理 + * + * @param carLicense 完整的车牌号 + * @return 脱敏后的车牌 + */ + public String carLicense(CharSequence carLicense) { + if (StrUtil.isBlank(carLicense)) { + return StrUtil.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) { + carLicense = StrUtil.replaceByCodePoint(carLicense, 3, 6, maskChar); + } else if (carLicense.length() == 8) { + // 新能源车牌 + carLicense = StrUtil.replaceByCodePoint(carLicense, 3, 7, maskChar); + } + return carLicense.toString(); + } + + /** + * 银行卡号脱敏 + * eg: 1101 **** **** **** 3256 + * + * @param bankCardNo 银行卡号 + * @return 脱敏之后的银行卡号 + */ + public String bankCard(CharSequence bankCardNo) { + if (StrUtil.isBlank(bankCardNo)) { + return StrUtil.toStringOrNull(bankCardNo); + } + bankCardNo = StrUtil.cleanBlank(bankCardNo); + if (bankCardNo.length() < 9) { + return bankCardNo.toString(); + } + + final int length = bankCardNo.length(); + final int endLength = length % 4 == 0 ? 4 : length % 4; + final int midLength = length - 4 - endLength; + final StringBuilder buf = new StringBuilder(); + + buf.append(bankCardNo, 0, 4); + for (int i = 0; i < midLength; ++i) { + if (i % 4 == 0) { + buf.append(CharUtil.SPACE); + } + buf.append(maskChar); + } + buf.append(CharUtil.SPACE).append(bankCardNo, length - endLength, length); + return buf.toString(); + } + + /** + * IPv4脱敏,如:脱敏前:192.0.2.1;脱敏后:192.*.*.*。 + * + * @param ipv4 IPv4地址 + * @return 脱敏后的地址 + */ + public String ipv4(final CharSequence ipv4) { + return StrUtil.subBefore(ipv4, '.', false) + StrUtil.repeat("." + maskChar, 3); + } + + /** + * IPv6脱敏,如:脱敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脱敏后:2001:*:*:*:*:*:*:* + * + * @param ipv6 IPv6地址 + * @return 脱敏后的地址 + */ + public String ipv6(final CharSequence ipv6) { + return StrUtil.subBefore(ipv6, ':', false) + StrUtil.repeat(":" + maskChar, 7); + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java new file mode 100644 index 000000000..21e83f74e --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.core.data.masking; + +/** + * 支持的脱敏类型枚举 + * + * @author dazer and neusoft and qiaomu + */ +public enum MaskingType { + /** + * 用户id + */ + USER_ID, + /** + * 中文名 + */ + CHINESE_NAME, + /** + * 身份证号 + */ + ID_CARD, + /** + * 座机号 + */ + FIXED_PHONE, + /** + * 手机号 + */ + MOBILE_PHONE, + /** + * 地址 + */ + ADDRESS, + /** + * 电子邮件 + */ + EMAIL, + /** + * 密码 + */ + PASSWORD, + /** + * 中国大陆车牌,包含普通车辆、新能源车辆 + */ + CAR_LICENSE, + /** + * 银行卡 + */ + BANK_CARD, + /** + * IPv4地址 + */ + IPV4, + /** + * IPv6地址 + */ + IPV6, + /** + * 定义了一个first_mask的规则,只显示第一个字符。 + */ + FIRST_MASK, + /** + * 清空为null + */ + CLEAR_TO_NULL, + /** + * 清空为"" + */ + CLEAR_TO_EMPTY +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java new file mode 100644 index 000000000..50fe35904 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 数据脱敏,提供各种数据类型(字符串、数字等)的脱敏方法。 + * + * @author looly + */ +package org.dromara.hutool.core.data.masking; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java index 2df776899..efbc8e3f2 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java @@ -16,9 +16,13 @@ package org.dromara.hutool.core.data; +import org.dromara.hutool.core.data.masking.MaskingManager; +import org.dromara.hutool.core.data.masking.MaskingType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * 脱敏工具类 MaskingUtil 安全测试 * @@ -29,94 +33,108 @@ public class MaskingUtilTest { @Test public void maskingTest() { - Assertions.assertEquals("", MaskingUtil.masking("100", MaskingUtil.MaskingType.CLEAR_TO_EMPTY)); - Assertions.assertNull(MaskingUtil.masking("100", MaskingUtil.MaskingType.CLEAR_TO_NULL)); + assertEquals("", MaskingUtil.masking(MaskingType.CLEAR_TO_EMPTY, "100")); + Assertions.assertNull(MaskingUtil.masking(MaskingType.CLEAR_TO_NULL, "100")); - Assertions.assertEquals("0", MaskingUtil.masking("100", MaskingUtil.MaskingType.USER_ID)); - Assertions.assertEquals("段**", MaskingUtil.masking("段正淳", MaskingUtil.MaskingType.CHINESE_NAME)); - Assertions.assertEquals("5***************1X", MaskingUtil.masking("51343620000320711X", MaskingUtil.MaskingType.ID_CARD)); - Assertions.assertEquals("0915*****79", MaskingUtil.masking("09157518479", MaskingUtil.MaskingType.FIXED_PHONE)); - Assertions.assertEquals("180****1999", MaskingUtil.masking("18049531999", MaskingUtil.MaskingType.MOBILE_PHONE)); - Assertions.assertEquals("北京市海淀区马********", MaskingUtil.masking("北京市海淀区马连洼街道289号", MaskingUtil.MaskingType.ADDRESS)); - Assertions.assertEquals("d*************@gmail.com.cn", MaskingUtil.masking("duandazhi-jack@gmail.com.cn", MaskingUtil.MaskingType.EMAIL)); - Assertions.assertEquals("**********", MaskingUtil.masking("1234567890", MaskingUtil.MaskingType.PASSWORD)); + assertEquals("0", MaskingUtil.masking(MaskingType.USER_ID, "100")); + assertEquals("段**", MaskingUtil.masking(MaskingType.CHINESE_NAME, "段正淳")); + assertEquals("5***************1X", MaskingUtil.masking(MaskingType.ID_CARD, "51343620000320711X")); + assertEquals("0915*****79", MaskingUtil.masking(MaskingType.FIXED_PHONE, "09157518479")); + assertEquals("180****1999", MaskingUtil.masking(MaskingType.MOBILE_PHONE, "18049531999")); + assertEquals("北京市海淀区马********", MaskingUtil.masking(MaskingType.ADDRESS, "北京市海淀区马连洼街道289号")); + assertEquals("d*************@gmail.com.cn", MaskingUtil.masking(MaskingType.EMAIL, "duandazhi-jack@gmail.com.cn")); + assertEquals("**********", MaskingUtil.masking(MaskingType.PASSWORD, "1234567890")); + assertEquals("**********", MaskingUtil.masking(MaskingType.PASSWORD, "123")); + assertEquals("1101 **** **** **** 3256", MaskingUtil.masking(MaskingType.BANK_CARD, "11011111222233333256")); + assertEquals("6227 **** **** **** 123", MaskingUtil.masking(MaskingType.BANK_CARD, "6227880100100105123")); + assertEquals("192.*.*.*", MaskingUtil.masking(MaskingType.IPV4, "192.168.1.1")); + assertEquals("2001:*:*:*:*:*:*:*", MaskingUtil.masking(MaskingType.IPV6, "2001:0db8:86a3:08d3:1319:8a2e:0370:7344")); + } - Assertions.assertEquals("0", MaskingUtil.masking("100", MaskingUtil.MaskingType.USER_ID)); - Assertions.assertEquals("段**", MaskingUtil.masking("段正淳", MaskingUtil.MaskingType.CHINESE_NAME)); - Assertions.assertEquals("5***************1X", MaskingUtil.masking("51343620000320711X", MaskingUtil.MaskingType.ID_CARD)); - Assertions.assertEquals("0915*****79", MaskingUtil.masking("09157518479", MaskingUtil.MaskingType.FIXED_PHONE)); - Assertions.assertEquals("180****1999", MaskingUtil.masking("18049531999", MaskingUtil.MaskingType.MOBILE_PHONE)); - Assertions.assertEquals("北京市海淀区马********", MaskingUtil.masking("北京市海淀区马连洼街道289号", MaskingUtil.MaskingType.ADDRESS)); - Assertions.assertEquals("d*************@gmail.com.cn", MaskingUtil.masking("duandazhi-jack@gmail.com.cn", MaskingUtil.MaskingType.EMAIL)); - Assertions.assertEquals("**********", MaskingUtil.masking("1234567890", MaskingUtil.MaskingType.PASSWORD)); - Assertions.assertEquals("1101 **** **** **** 3256", MaskingUtil.masking("11011111222233333256", MaskingUtil.MaskingType.BANK_CARD)); - Assertions.assertEquals("6227 **** **** **** 123", MaskingUtil.masking("6227880100100105123", MaskingUtil.MaskingType.BANK_CARD)); - Assertions.assertEquals("192.*.*.*", MaskingUtil.masking("192.168.1.1", MaskingUtil.MaskingType.IPV4)); - Assertions.assertEquals("2001:*:*:*:*:*:*:*", MaskingUtil.masking("2001:0db8:86a3:08d3:1319:8a2e:0370:7344", MaskingUtil.MaskingType.IPV6)); + @Test + public void maskingWithMaskCharTest() { + final MaskingManager manager = MaskingManager.ofDefault('#'); + + assertEquals("", manager.masking(MaskingType.CLEAR_TO_EMPTY.name(), "100")); + Assertions.assertNull(manager.masking(MaskingType.CLEAR_TO_NULL.name(), "100")); + + assertEquals("0", manager.masking(MaskingType.USER_ID.name(), "100")); + assertEquals("段##", manager.masking(MaskingType.CHINESE_NAME.name(), "段正淳")); + assertEquals("5###############1X", manager.masking(MaskingType.ID_CARD.name(), "51343620000320711X")); + assertEquals("0915#####79", manager.masking(MaskingType.FIXED_PHONE.name(), "09157518479")); + assertEquals("180####1999", manager.masking(MaskingType.MOBILE_PHONE.name(), "18049531999")); + assertEquals("北京市海淀区马########", manager.masking(MaskingType.ADDRESS.name(), "北京市海淀区马连洼街道289号")); + assertEquals("d#############@gmail.com.cn", manager.masking(MaskingType.EMAIL.name(), "duandazhi-jack@gmail.com.cn")); + assertEquals("##########", manager.masking(MaskingType.PASSWORD.name(), "1234567890")); + assertEquals("##########", manager.masking(MaskingType.PASSWORD.name(), "123")); + assertEquals("1101 #### #### #### 3256", manager.masking(MaskingType.BANK_CARD.name(), "11011111222233333256")); + assertEquals("6227 #### #### #### 123", manager.masking(MaskingType.BANK_CARD.name(), "6227880100100105123")); + assertEquals("192.#.#.#", manager.masking(MaskingType.IPV4.name(), "192.168.1.1")); + assertEquals("2001:#:#:#:#:#:#:#", manager.masking(MaskingType.IPV6.name(), "2001:0db8:86a3:08d3:1319:8a2e:0370:7344")); } @Test public void userIdTest() { - Assertions.assertEquals(Long.valueOf(0L), MaskingUtil.userId()); + assertEquals(Long.valueOf(0L), MaskingUtil.userId()); } @Test public void chineseNameTest() { - Assertions.assertEquals("段**", MaskingUtil.chineseName("段正淳")); + assertEquals("段**", MaskingUtil.chineseName("段正淳")); } @Test public void idCardNumTest() { - Assertions.assertEquals("5***************1X", MaskingUtil.idCardNum("51343620000320711X", 1, 2)); + assertEquals("5***************1X", MaskingUtil.idCardNum("51343620000320711X", 1, 2)); } @Test public void fixedPhoneTest() { - Assertions.assertEquals("0915*****79", MaskingUtil.fixedPhone("09157518479")); + assertEquals("0915*****79", MaskingUtil.fixedPhone("09157518479")); } @Test public void mobilePhoneTest() { - Assertions.assertEquals("180****1999", MaskingUtil.mobilePhone("18049531999")); + assertEquals("180****1999", MaskingUtil.mobilePhone("18049531999")); } @Test public void addressTest() { - Assertions.assertEquals("北京市海淀区马连洼街*****", MaskingUtil.address("北京市海淀区马连洼街道289号", 5)); - Assertions.assertEquals("***************", MaskingUtil.address("北京市海淀区马连洼街道289号", 50)); - Assertions.assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", 0)); - Assertions.assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", -1)); + assertEquals("北京市海淀区马连洼街*****", MaskingUtil.address("北京市海淀区马连洼街道289号", 5)); + assertEquals("***************", MaskingUtil.address("北京市海淀区马连洼街道289号", 50)); + assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", 0)); + assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", -1)); } @Test public void emailTest() { - Assertions.assertEquals("d********@126.com", MaskingUtil.email("duandazhi@126.com")); - Assertions.assertEquals("d********@gmail.com.cn", MaskingUtil.email("duandazhi@gmail.com.cn")); - Assertions.assertEquals("d*************@gmail.com.cn", MaskingUtil.email("duandazhi-jack@gmail.com.cn")); + assertEquals("d********@126.com", MaskingUtil.email("duandazhi@126.com")); + assertEquals("d********@gmail.com.cn", MaskingUtil.email("duandazhi@gmail.com.cn")); + assertEquals("d*************@gmail.com.cn", MaskingUtil.email("duandazhi-jack@gmail.com.cn")); } @Test public void passwordTest() { - Assertions.assertEquals("**********", MaskingUtil.password("1234567890")); + assertEquals("**********", MaskingUtil.password("1234567890")); } @Test public void carLicenseTest() { - Assertions.assertEquals("", MaskingUtil.carLicense(null)); - Assertions.assertEquals("", MaskingUtil.carLicense("")); - Assertions.assertEquals("苏D4***0", MaskingUtil.carLicense("苏D40000")); - Assertions.assertEquals("陕A1****D", MaskingUtil.carLicense("陕A12345D")); - Assertions.assertEquals("京A123", MaskingUtil.carLicense("京A123")); + assertEquals("", MaskingUtil.carLicense(null)); + assertEquals("", MaskingUtil.carLicense("")); + assertEquals("苏D4***0", MaskingUtil.carLicense("苏D40000")); + assertEquals("陕A1****D", MaskingUtil.carLicense("陕A12345D")); + assertEquals("京A123", MaskingUtil.carLicense("京A123")); } @Test public void bankCardTest() { Assertions.assertNull(MaskingUtil.bankCard(null)); - Assertions.assertEquals("", MaskingUtil.bankCard("")); - Assertions.assertEquals("1234 **** **** **** **** 9", MaskingUtil.bankCard("1234 2222 3333 4444 6789 9")); - Assertions.assertEquals("1234 **** **** **** **** 91", MaskingUtil.bankCard("1234 2222 3333 4444 6789 91")); - Assertions.assertEquals("1234 **** **** **** 6789", MaskingUtil.bankCard("1234 2222 3333 4444 6789")); - Assertions.assertEquals("1234 **** **** **** 678", MaskingUtil.bankCard("1234 2222 3333 4444 678")); + assertEquals("", MaskingUtil.bankCard("")); + assertEquals("1234 **** **** **** **** 9", MaskingUtil.bankCard("1234 2222 3333 4444 6789 9")); + assertEquals("1234 **** **** **** **** 91", MaskingUtil.bankCard("1234 2222 3333 4444 6789 91")); + assertEquals("1234 **** **** **** 6789", MaskingUtil.bankCard("1234 2222 3333 4444 6789")); + assertEquals("1234 **** **** **** 678", MaskingUtil.bankCard("1234 2222 3333 4444 678")); } } From 574b657854acb56bfe769d3b831d6dd848ecabd0 Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 11 Oct 2024 00:12:18 +0800 Subject: [PATCH 3/3] add test --- .../org/dromara/hutool/crypto/symmetric/AESTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/AESTest.java b/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/AESTest.java index a89cd93e1..f7f5d8aa2 100644 --- a/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/AESTest.java +++ b/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/AESTest.java @@ -19,9 +19,7 @@ package org.dromara.hutool.crypto.symmetric; import org.dromara.hutool.core.codec.binary.Base64; import org.dromara.hutool.core.codec.binary.HexUtil; import org.dromara.hutool.core.util.RandomUtil; -import org.dromara.hutool.crypto.KeyUtil; -import org.dromara.hutool.crypto.Mode; -import org.dromara.hutool.crypto.Padding; +import org.dromara.hutool.crypto.*; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -148,4 +146,10 @@ public class AESTest { final String decryptStr = aes.decryptStr(encrypt); Assertions.assertEquals(phone, decryptStr); } + + @Test + void issue3766Test() { + Assertions.assertThrows(CryptoException.class, ()-> + SecureUtil.aes("8888888888888888".getBytes()).decryptStr("哈哈")); + } }