正确解析包含“+”字符的字段

Posted

技术标签:

【中文标题】正确解析包含“+”字符的字段【英文标题】:Properly parse field containing '+' char 【发布时间】:2019-08-13 15:59:42 【问题描述】:

我遇到了一个奇怪的情况,我在https://github.com/lgueye/uri-parameters-behavior 中转载了

由于我们在 GET 方法中请求我们的后端之一时迁移到了 spring-boot 2spring 框架 5),因此我们运行了进入以下情况:所有具有 + 字符的字段在到达后端时都被更改为 (空白)字符

以下值已更改:

+412386789(电话号码)转为** 412386789** 2019-03-22T17:18:39.621+02:00 (java8 ZonedDateTime) 变为 2019-03-22T17:18:39.621 02:00(导致 >org.springframework.validation.BindException

我在 *** (https://github.com/spring-projects/spring-framework/issues/14464#issuecomment-453397378) 和 github (https://github.com/spring-projects/spring-framework/issues/21577) 上花了很长时间

我已经实现了一个 mockMvc 单元测试和一个集成测试

单元测试正常运行 集成测试失败(就像我们的生产一样)

谁能帮我解决这个问题?我的目标显然是让集成测试通过。

感谢您的帮助。

路易

【问题讨论】:

【参考方案1】:

整个错位来自这样一个事实,即如何将空间编码/解码为"+" 的非标准做法。

可以说空间可以(正在)编码为"+""%20"

例如 Google 对搜索字符串这样做:

https://www.google.com/search?q=test+my+space+delimited+entry

rfc1866, section-8.2.2 声明 GET 请求的查询部分应编码为 'application/x-www-form-urlencoded'

所有表单的默认编码是`application/x-www-form- urlencoded'。表单数据集在此媒体类型中表示为 如下:

    表单字段名称和值被转义:空格 字符被替换为“+”

另一方面,rfc3986 声明 URL 中的空格必须使用 "%20" 进行编码。

这基本上意味着对空格进行编码有不同的标准,具体取决于它们在 URI syntax components 中的位置。

     foo://example.com:8042/over/there?name=ferret#nose
     \_/   \______________/\_________/ \_________/ \__/
      |           |            |            |        |
   scheme     authority       path        query   fragment
      |   _____________________|__
     / \ /                        \
     urn:example:animal:ferret:nose

基于这些评论,我们可以说明在 URI 中的 GET http 调用中:

"?" 之前的空格需要编码为"%20" 查询参数中"?"后面的空格需要编码为"+" 这意味着"+"符号需要在查询参数中编码为"%2B"

Spring 实现遵循 rfc 规范,这就是为什么当您在查询参数中发送 "+412386789" 时,"+" 符号被解释为空白字符,并以“412386789”

看着:

final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
                                    .port(port)
                                    .path("/events")
                                    .queryParams(params)
                                    .build()
                                    .toUri();

你会发现:

"foo#bar@quizz+foo-bazz//quir." 编码为"foo%23bar@quizz+foo-bazz//quir." 符合规范 (rfc3986)。

因此,如果您希望查询参数中的 "+" 字符不被解释为空格,则需要将其编码为 "%2B"

您发送到后端的参数应如下所示:

   params.add("id", id);
   params.add("device", device);
   params.add("phoneNumber", "%2B225697845");
   params.add("timestamp", "2019-03-25T15%3A09%3A44.703088%2B02%3A00");
   params.add("value", "foo%23bar%40quizz%2Bfoo-bazz%2F%2Fquir.");

为此,您可以在将参数传递给地图时使用UrlEncoder。当心 UriComponentsBuilder 双重编码你的东西!

您可以通过以下方式获得正确的 URL:

final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("id", id);
params.add("device", device);
String uft8Charset = StandardCharsets.UTF_8.toString();
params.add("phoneNumber", URLEncoder.encode(phoneNumber, uft8Charset));
params.add("timestamp", URLEncoder.encode(timestamp.toString(), uft8Charset));
params.add("value", URLEncoder.encode(value, uft8Charset));

final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
                                    .port(port)
                                    .path("/events")
                                    .queryParams(params)
                                    .build(true)
                                    .toUri();

请注意,将“true”传递给build() 方法会关闭编码,因此这意味着来自URI 部分的方案、主机等不会被UriComponentsBuilder 正确编码。

【讨论】:

【参考方案2】:

在与这个问题进行了一番斗争后,我终于让它按照我们期望的方式在我们公司工作。

有问题的组件不是 spring-boot 而是 UriComponentsBuilder

我最初的失败测试如下所示:

    @Test
public void get_should_properly_convert_query_parameters() 
    // Given
    final String device = UUID.randomUUID().toString();
    final String id = UUID.randomUUID().toString();
    final String phoneNumber = "+225697845";
    final String value = "foo#bar@quizz+foo-bazz//quir.";
    final Instant now = Instant.now();
    final ZonedDateTime timestamp = ZonedDateTime.ofInstant(now, ZoneId.of("+02:00"));

    final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("id", id);
    params.add("device", device);
    params.add("phoneNumber", phoneNumber);
    params.add("timestamp", timestamp.toString());
    params.add("value", value);

    final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port).path("/events").queryParams(params).build().toUri();
    final Event expected = Event.builder().device(device).id(id).phoneNumber(phoneNumber).value(value).timestamp(timestamp).build();

    // When
    final Event actual = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), Event.class).getBody();

    // Then
    assertEquals(expected, actual);

工作版本如下所示:

    @Test
public void get_should_properly_convert_query_parameters() 
    // Given
    final String device = UUID.randomUUID().toString();
    final String id = UUID.randomUUID().toString();
    final String phoneNumber = "+225697845";
    final String value = "foo#bar@quizz+foo-bazz//quir.";
    final Instant now = Instant.now();
    final ZonedDateTime timestamp = ZonedDateTime.ofInstant(now, ZoneId.of("+02:00"));
    final Map<String, String> params = new HashMap<>();
    params.put("id", id);
    params.put("device", device);
    params.put("phoneNumber", phoneNumber);
    params.put("timestamp", timestamp.toString());
    params.put("value", value);
    final MultiValueMap<String, String> paramTemplates = new LinkedMultiValueMap<>();
    paramTemplates.add("id", "id");
    paramTemplates.add("device", "device");
    paramTemplates.add("phoneNumber", "phoneNumber");
    paramTemplates.add("timestamp", "timestamp");
    paramTemplates.add("value", "value");

    final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port).path("/events").queryParams(paramTemplates).encode().buildAndExpand(params).toUri();
    final Event expected = Event.builder().device(device).id(id).phoneNumber(phoneNumber).value(value).timestamp(ZonedDateTime.ofInstant(now, ZoneId.of("UTC"))).build();

    // When
    final Event actual = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), Event.class).getBody();

    // Then
    assertEquals(expected, actual);

注意 4 所需的差异:

需要 MultiValueMap 参数模板 需要映射参数值 需要编码 需要带有参数值的 buildAndExpand

我有点难过,因为所有这些都非常容易出错且很麻烦(特别是 Map/MultiValueMap 部分)。我很乐意让它们从 java bean 生成。

这对我们的解决方案影响很大,但恐怕我们别无选择。我们现在就接受这个解决方案。

希望这可以帮助其他面临此问题的人。

最好的,

路易

【讨论】:

以上是关于正确解析包含“+”字符的字段的主要内容,如果未能解决你的问题,请参考以下文章

将泰国名字解析为第一个最后一个

如何正确地将 JArray 解析为字符串集合

为 EXECUTE IMMEDIATE 解析 PL/SQL 语句中的占位符

如何解析字符串化的 JSON 字段并使用 react-admin 呈现结果

解析单个 CSV 字符串?

如何正确解析一组 Fetch 响应