使用新的Spring UriComponentsBuilder进行URL编码

47

我试图使用Spring的UriComponentsBuilder生成一些用于OAuth交互的URL。查询参数包括回调URL和带有空格的参数值等实体。

由于UriUtils现在已过时,因此尝试使用UriComponentBuilder。

UriComponentsBuilder urlBuilder = UriComponentsBuilder.fromHttpUrl(oauthURL);
urlBuilder.queryParam("client_id", clientId);
urlBuilder.queryParam("redirect_uri", redirectURI);
urlBuilder.queryParam("scope", "test1 test2");

String url = urlBuilder.build(false).encode().toUriString();

很不幸,尽管作用域参数中的空格被成功替换为 '+',但 redirect_uri 参数根本没有进行 URL 编码。

例如,

redirect_uri=https://oauth2-login-demo.appspot.com/code

本应该结束

redirect_uri=https%3A%2F%2Foauth2-login-demo.appspot.com%2Fcode

但没有被触及。深入代码,具体地说是 org.springframework.web.util.HierarchicalUriComponents.Type.QUERY_PARAM.isAllowed(c) :

if ('=' == c || '+' == c || '&' == c) {
  return false;
}
else {
  return isPchar(c) || '/' == c || '?' == c;
}

明显允许':'和'/'字符,但它不应该这样。虽然我想象不出来它在做什么其他类型的编码,但它一定在做某些事情。我在这里找错了吗?

谢谢

5个回答

32

UriComponentsBuilder 根据RFC 3986编码您的URI,其中第3.4节关于URI“查询”组件特别值得注意。

在“查询”组件中,字符/:是允许的,不需要转义。

/字符为例:明显由未转义的?和(可选)#字符限定的“查询”组件不是分层结构,/字符没有特殊含义,因此无需编码。


9
这不正确,因为和其他有意义的字符也没有被转义。 UriComponentsBuilder没有对查询参数进行URL编码。 - Adam Millerchip
2
你可能会这样想,但事实并非如此。试一下就知道了。 - Adam Millerchip
1
我尝试了一下,确实转义了 & 和 =。鉴于 OP 中的代码片段,不确定为什么 + 没有被转义。我怀疑我的最初推断是正确的:在查询片段中,+ 符号没有任何意义。UriComponentsBuilder urlBuilder = UriComponentsBuilder.fromHttpUrl("http://example.org"); urlBuilder.queryParam("scope", "test1&test2=test3+test4"); String url = urlBuilder.build(false).encode().toUriString(); System.out.println(url);结果: http://example.org?scope=test1%26test2%3Dtest3+test4 - simonh
(抱歉,我的上面的评论中S.O.格式很差 - 'example.org'实际上有一个http://前缀,但在发布后被剥离了。)import requests response = requests.get('http://example.org') print(response.content)这段代码使用Python的requests库向example.org发送GET请求,并打印响应内容。 - simonh
8
我报告了toUriString()的行为后,Spring的开发人员很快修复了它,如果没有变量存在,则使用build().encode().toUriString()(原始行为)。现在,像“{}”这样的查询参数将被正确编码。 - rougou
显示剩余5条评论

28

据我所了解,UriComponentsBuilder不会自动编码查询参数,只会对其最初实例化的原始HttpUrl进行编码。换句话说,你仍然需要显式地进行编码:

String redirectURI= "https://oauth2-login-demo.appspot.com/code";
urlBuilder.queryParam("redirect_uri", URLEncoder.encode(redirectURI,"UTF-8" ));

1
好的... 'encode' 方法说明:使用它们特定的编码规则对所有 URI 组件进行编码,并将结果作为新的 {@code UriComponents} 实例返回。这似乎意味着它执行 URL 编码。它似乎把决定哪些字符需要编码留给了 'Type'(在这种情况下是 Type.QUERY_PARAM)。因此,它会对一些字符进行编码...但不包括一些非常重要的字符。如果不是用于对查询参数进行 URL 编码,那么 encode 方法会做什么? - ticktock
2
它会对传递给它的URL编码,但不会对每个查询参数进行编码。 - Black
21
嗯...这并不是非常有用。很奇怪,因为它是一个URL生成器,你添加查询参数,然后生成,然后再编码。我本来以为它会生成一个安全的URL。如果查询参数没有随着URL一起被编码,那么添加查询参数的意义在哪里呢? - ticktock
2
我同意,并且会像你一样假设。也许设计师有一些理由不隐式编码参数,但也可能没有。 - Black
5
UriComponentsBuilder 包含一个 encode() 方法,我认为它将对 URL 和查询参数进行编码。 - ashario
显示剩余2条评论

4
尝试查看UriComponentsBuilder文档,其中有一个名为build(boolean encoded)的方法。
示例代码1:
UriComponents uriComponents = UriComponentsBuilder.fromPath("/path1/path2").build(true);

这是我的示例代码2:
UriComponents uriComponents = UriComponentsBuilder.newInstance()
            .scheme("https")
            .host("my.host")
            .path("/path1/path2").query(parameters).build(true);

URI uri= uriComponents.toUri();

ResponseEntity<MyEntityResponse> responseEntity = restTemplate.exchange(uri,
            HttpMethod.GET, entity, typeRef);

2
.build(true) 单独使用并不能转义 .queryParam,它只是表示你的所有参数已经被转义了。感觉有点无用,因为它并没有进行任何转义... 可能是 Spring 方面正在进行的工作。 - jediz
3
如果您已经有一个编码过的URL并且不想让它再次被验证和编码,使用encoded参数将非常有用。也许他们应该将其从“encoded”改名为“alreadyEncoded”。 - rougou

2

我尝试了上面所有的解决方案,直到我让它工作起来。

在我的例子中,我试图编码ZonedDateTime格式2022-01-21T10:17:10.228+06:00。加号是一个问题。

解决我的问题的方法是手动编码值+使用URI而不是字符串值(两者都非常重要)。

之前:

restTemplate.exchange(
  UriComponentsBuilder
    .queryParam("fromDateTime", "2022-01-21T10:17:10.228+06:00")
    .build()
    .toUriString(),
  HttpMethod.GET,
  null,
  new ParameterizedTypeReference<List<MyDto>>() {}
);

之后:

restTemplate.exchange(
  UriComponentsBuilder
    .queryParam("fromDateTime", URLEncoder.encode("2022-01-21T10:17:10.228+06:00", StandardCharsets.UTF_8))
    .build(true)
    .toUri(),
  HttpMethod.GET,
  null,
  new ParameterizedTypeReference<List<MyDto>>() {}
);

0
我在build()之前使用encode(),以使查询参数编码对我起作用。这些测试比较了使用encode()调用和不使用encode()调用。
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;

public class EncodeQueryParametersTest {
    @Test
    public void withoutEncode() {
        final MultiValueMap<String, String> queryParameters = new LinkedMultiValueMap<>();
        queryParameters.add("fullname", "First Last");
        assertThat(UriComponentsBuilder.newInstance().queryParams(queryParameters).build().getQuery()).isEqualTo("fullname=First Last");
    }

    @Test
    public void withEncode() {
        final MultiValueMap<String, String> queryParameters = new LinkedMultiValueMap<>();
        queryParameters.add("fullname", "First Last");
        assertThat(UriComponentsBuilder.newInstance().queryParams(queryParameters).encode().build().getQuery()).isEqualTo("fullname=First%20Last");
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接