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

时间:2019-03-22 15:55:10

标签: spring-boot spring-mvc rfc3986

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

中转载了

因为当我们以 GET 方法请求后端之一时,我们已迁移到 spring-boot 2 spring framework 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

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

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

单元测试行为正常 集成测试失败(例如我们的产品)

有人可以帮助我解决此问题吗?我的目标显然是使集成测试通过。

谢谢您的帮助。

路易

2 个答案:

答案 0 :(得分:0)

在解决了这个问题之后,我终于使它按照我们公司的期望工作了。

有问题的组件不是 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生成。

这对我们的解决方案影响很大,但恐怕我们别无选择。我们将暂时解决此问题。

希望这有助于其他人解决此问题。

最好

路易

答案 1 :(得分:0)

整个失调是由于存在一种非标准的做法,即如何将空间编码/解码为"+"

可以说空间可以被编码为"+""%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”。表单数据集在此媒体类型中表示为
  如下:

     
      
  1. 对表单字段名称和值进行转义:空格       字符替换为'+'
  2.   

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

这基本上意味着,根据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正确编码。