|
|
@@ -132,29 +132,42 @@ public class LeshouyingClient {
|
|
|
* 上游按它自己那份参数集重算,必然对不上,而失败只会表现为一句笼统的「签名错误」。
|
|
|
* 本方法不开任何旁路:签名仍走 {@link LeshouyingSigner},与另外两条腿同一套规则。
|
|
|
*
|
|
|
- * <p>【前置条件:三个**会进部件头**的入口拒绝「头部无法表示」的输入 —— 这是**我们**的约束,
|
|
|
+ * <p>【前置条件:三个**会进部件头**的入口拒绝三类输入 —— 这是**我们**的约束,
|
|
|
* 上游文档(page 196236572)**没有**提这件事】
|
|
|
- * 两族输入、同一个 {@link IllegalArgumentException}、同样**在任何出站之前**(连实体都不构造,
|
|
|
- * 一个字节都不出海):
|
|
|
+ * 三类输入、同一个 {@link IllegalArgumentException}、同样**在任何出站之前**(连实体都不构造,
|
|
|
+ * 一个字节都不出海);**但依据强度不同**,下面逐类标注(哪条是实测、哪条是取舍):
|
|
|
* <ol>
|
|
|
- * <li>**ISO 控制字符**。实测(真实 {@code FormHttpMessageConverter} + 捕获线上字节):
|
|
|
- * {@code fileFieldName}、{@code formFields} 的**键**、{@code fileName} 三个值都会被原样
|
|
|
- * 写进部件的 {@code Content-Disposition} 头,里面一个 CR / LF / tab 就能凭空多出一行头部
|
|
|
- * (线上一字不差长这样:{@code filename="a\r\nX-Evil: 1.jpg"} ⇒ {@code X-Evil: 1.jpg"}
|
|
|
- * 成了部件头块里的独立一行)。</li>
|
|
|
- * <li>**null**。实测(同一套捕获装置,把守卫的 null 分支临时改回 {@code return} 跑出来的真实字节)
|
|
|
- * ——三种**都不是异常,而是静默改形状**,所以更要拒:
|
|
|
+ * <li>**CR / LF:实测确实能在部件头块里造出一行**。{@code fileFieldName}、{@code formFields}
|
|
|
+ * 的**键**、{@code fileName} 三个值都会被原样写进部件的 {@code Content-Disposition}
|
|
|
+ * (线上一字不差:{@code filename="a\r\nX-Evil: 1.jpg"} ⇒ {@code X-Evil: 1.jpg"}
|
|
|
+ * 成了部件头块里的独立一行)。**成行的是 LF**:{@code \r\n} 与裸 {@code \n} 实测都让注入串
|
|
|
+ * 落到行首 —— 但**要用两个度量**才看得全:{@code \r\n} 在「只认 CRLF」与「认 CRLF/LF/CR」
|
|
|
+ * 两个度量下都成行,裸 {@code \n} 只有后一个度量看得到(裸 LF 不是 {@code \r\n},
|
|
|
+ * 只按 {@code \r\n} 数行会把它数成「没注入」)。</li>
|
|
|
+ * <li>**其余 ISO 控制字符(裸 CR、tab、NUL、DEL、VT、FF):实测并不成行** ——
|
|
|
+ * 它们打进 filename 后,注入串仍在**同一行内**,没有被任何分词度量算成独立的头部行。
|
|
|
+ * 我们**仍然拒**,但这是**取舍**、**不是**「实测的注入」:判据统一取
|
|
|
+ * {@link Character#isISOControl} 一整族(宽规则比「只堵 CR/LF 两个字面量」更难绕过),
|
|
|
+ * 理由是这些字符在字段名/文件名里没有合法位置。
|
|
|
+ * ★ 不要把这一族写成「实测能注入」—— 那是假的。
|
|
|
+ * <br>(裸 CR 单独说明:按 MIME 的合法行分隔符 {@code \r\n} 判,它**不成行**;
|
|
|
+ * 只有把裸 CR 也当分隔符的宽松分词才数得出「一行」。上游/中间件会不会那样宽容**未实测**,
|
|
|
+ * 所以它既不写成「实测成行」,也不写成「实测不成行」。)</li>
|
|
|
+ * <li>**null:部件头里没有「名字不存在」的表示法**。实测(同一套捕获装置,把守卫的 null 分支
|
|
|
+ * 临时改回 {@code return} 跑出来的真实字节)—— 三种**都不是异常,而是静默改形状**,所以更要拒:
|
|
|
* <ul>
|
|
|
* <li>{@code fileName == null} ⇒ 部件变成
|
|
|
* {@code Content-Disposition: form-data; name="img_file"}:<b>filename 段消失、
|
|
|
* Content-Type 从 {@code image/jpeg} 退化成 {@code application/octet-stream}</b>
|
|
|
- * —— 文件部件**静默退化成一个普通表单字段**,上游多半只回一句笼统的「img_file 必填 / 类型不对」,
|
|
|
- * 排查方向指向参数而不是客户端;</li>
|
|
|
+ * —— 文件部件**静默退化成一个普通表单字段**。(上游收到这种部件会怎么回**未实测**:
|
|
|
+ * 这里只写我们这侧的可观测事实,不写「上游会说 img_file 必填」这类推断。)</li>
|
|
|
* <li>{@code fileFieldName == null} ⇒ {@code Content-Disposition: form-data; filename="a.jpg"}
|
|
|
- * —— <b>连 name 段都没有</b>(部件无名字,上游无法归属到任何字段);</li>
|
|
|
+ * —— <b>连 name 段都没有</b>(部件无名字)。</li>
|
|
|
* <li>{@code formFields} 的**键**为 null ⇒ {@code Content-Disposition: form-data}
|
|
|
- * (无名无 filename),且该部件**不进签名原文**(同一次实测里,签名与不含该键时逐字相同)
|
|
|
- * ⇒ 线上多出一个**既不具名、又不被签名覆盖**的部件。</li>
|
|
|
+ * (无名无 filename),且该部件**不进我们这份签名原文**(同一次实测里,签名与不含该键时
|
|
|
+ * 逐字相同)⇒ 线上多出一个**既不具名、又不在我们签名原文里**的部件。
|
|
|
+ * ★ 只说到「我们这份签名原文」为止:上游拿哪份参数集、怎么重算**未实测**,
|
|
|
+ * 不能由此推断「上游也不会把它算进去」。</li>
|
|
|
* </ul>
|
|
|
* {@code fileName} 尤其不是假想路径:它一般来自
|
|
|
* {@code MultipartFile.getOriginalFilename()},那个 API 的契约就是可空的。</li>
|
|
|
@@ -186,17 +199,24 @@ public class LeshouyingClient {
|
|
|
*
|
|
|
* @param path 以 / 开头的路径
|
|
|
* @param formFields 表单里的**字符串**字段(不含 inst_no / sign,本方法自动补);
|
|
|
- * 文件**不**走这里。**键**不得为 null、不得含 ISO 控制字符
|
|
|
- * (**值**两者都不限 —— 值走部件体,见上面第 3 段的位置判据)。
|
|
|
+ * 文件**不**走这里。**键**不得为 null、不得含 ISO 控制字符(键进部件头,
|
|
|
+ * 见上面第 3 段)。**值**走部件体、不受那条守卫管,但**值也不是「不限」**:
|
|
|
+ * 值为 null ⇒ 该字段**整条消失**(签名原文与报文取自同一份 payload,
|
|
|
+ * 所以是**一致地**消失、两者不打架)—— 我们这侧**根本不会发出这个字段**,
|
|
|
+ * 上游看到的是一个缺字段的请求(上游具体怎么回**未实测**;但「必填字段缺失」
|
|
|
+ * 与 null fileName 让文件部件退化属于同一类失败形态);**空串值则原样保留**。
|
|
|
* 整个 map 为 null 是允许的(等价于没有表单字段)
|
|
|
* @param fileFieldName 文件部件的字段名(上游契约里是 {@code img_file});
|
|
|
* 不得为 null、不得含 ISO 控制字符
|
|
|
* @param fileName 文件部件的文件名(multipart 的 filename),不是表单字段;
|
|
|
* 不得为 null、不得含 ISO 控制字符
|
|
|
- * @param fileBytes 文件内容,**不得为 null**:{@code new ByteArrayResource(null)} 在
|
|
|
- * {@code try} **之外**抛 {@link IllegalArgumentException},
|
|
|
- * **不会**被包成 {@link LeshouyingApiException} —— 调用方若只 catch
|
|
|
- * 后者,这个错误会逃出它的错误通道(生产表现为 500)
|
|
|
+ * @param fileBytes 文件内容,**不得为 null**:由本方法**入口**的守卫拒绝(消息点名
|
|
|
+ * {@code fileBytes}),抛的 {@link IllegalArgumentException} 在
|
|
|
+ * {@code postForRawMultipart} 的 {@code try} **之外**,**不会**被包成
|
|
|
+ * {@link LeshouyingApiException} —— 调用方若只 catch 后者,
|
|
|
+ * 这个错误会逃出它的错误通道(生产表现为 500)。
|
|
|
+ * 这条守卫是**消息清晰度与对称性**的改动,**不是**补泄漏:
|
|
|
+ * 它一个字节都不出海(理由写在方法体那条注释里)
|
|
|
* @throws IllegalArgumentException 上面三个入口为 null 或含 ISO 控制字符,
|
|
|
* 或 {@code fileBytes} 为 null
|
|
|
*/
|
|
|
@@ -212,20 +232,32 @@ public class LeshouyingClient {
|
|
|
requireHeaderSafe("formFields 的键", name);
|
|
|
}
|
|
|
}
|
|
|
+ // ★ 这条与上面三处**不同族**:fileBytes 不进部件头(它进部件体),所以这既不是防注入、
|
|
|
+ // 也不是补泄漏 —— `new ByteArrayResource(null)` 本来就会在 try 之外抛 IAE,位置与类型都对。
|
|
|
+ // 加它是为了两件事:① 消息**点名我们自己的形参**(Spring 的 Assert 只说
|
|
|
+ // "Byte array must not be null",调用方从几十个形参里认不出是哪个);
|
|
|
+ // ② 与三个头部入口的守卫**形态一致**(都在入口、都是 IllegalArgumentException、
|
|
|
+ // 都一个字节不出海)。**不要**把它写成「堵了一个泄漏」。
|
|
|
+ if (fileBytes == null) {
|
|
|
+ throw new IllegalArgumentException(
|
|
|
+ "fileBytes 不得为 null:文件内容不能缺(multipart 的文件部件没有「内容不存在」的表示法)");
|
|
|
+ }
|
|
|
return parser.parseApi(
|
|
|
postForRawMultipart(path, formFields, fileFieldName, fileName, fileBytes));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 部件头注入守卫:只查三个**会进部件头**的入口(见 {@link #postMultipart} 的 javadoc 第 3 段)。
|
|
|
- * 拒**两族**「头部无法表示」的输入,判据一致、异常类型一致(同一个
|
|
|
- * {@link IllegalArgumentException}):
|
|
|
+ * 部件头守卫:只查三个**会进部件头**的入口(见 {@link #postMultipart} 的 javadoc 第 3 段)。
|
|
|
+ * 拒三类输入,异常类型一致(同一个 {@link IllegalArgumentException}),**但依据强度不同**:
|
|
|
* <ol>
|
|
|
- * <li>**ISO 控制字符**({@link Character#isISOControl},覆盖 CR / LF / tab / NUL / DEL)
|
|
|
- * —— 拼进 {@code Content-Disposition} 会凭空多出一行头部;</li>
|
|
|
- * <li>**null** —— 部件头里没有「名字不存在」的表示法,实测是**静默改形状**而非报错
|
|
|
- * (三个入口的线上读数见 {@link #postMultipart} 的 javadoc 第 3 段)。</li>
|
|
|
+ * <li>**CR / LF** —— **实测会成行**:拼进 {@code Content-Disposition} 能凭空多出一行头部(成行的是 LF);</li>
|
|
|
+ * <li>**其余 ISO 控制字符**(裸 CR、tab、NUL、DEL、VT、FF)—— **实测不成行**,
|
|
|
+ * 一并拒是**取舍**(统一的宽规则;这些字符在字段名/文件名里没有合法位置),
|
|
|
+ * **不是**「实测的注入」;</li>
|
|
|
+ * <li>**null** —— 部件头里没有「名字不存在」的表示法,实测是**静默改形状**而非报错。</li>
|
|
|
* </ol>
|
|
|
+ * 三类的线上读数与依据强度逐条写在 {@link #postMultipart} 的 javadoc 第 3 段,
|
|
|
+ * **不要在这里复述成另一套说法**(两处措辞已因「tab 能注入」这个假依据错过一次)。
|
|
|
*
|
|
|
* <p>为什么必须在**组实体之前**:这些值一旦交给 {@code FormHttpMessageConverter} 就会被原样
|
|
|
* 写进 {@code Content-Disposition},一个 CR/LF 就是一行伪造的头部;在入口抛 ⇒ 报文根本没生成,
|