调用Spring RestController时,使用Spring的RestTemplate转义URL变量的正确方法是什么?



调用RestTemplate.exchange执行获取请求时,例如:

String foo = "fo+o";
String bar = "ba r";
restTemplate.exchange("http://example.com/?foo={foo}&bar={bar}", HttpMethod.GET, null, foo, bar)

如何正确转义 GET 请求的 URL 变量?

具体来说,我如何正确转义加号(+(,因为 Spring 正在解释为空格,因此我需要对它们进行编码。

我尝试使用这样的UriComponentsBuilder

String foo = "fo+o";
String bar = "ba r";
UriComponentsBuilder ucb = UriComponentsBuilder.fromUriString("http://example.com/?foo={foo}&bar={bar}");
System.out.println(ucb.build().expand(foo, bar).toUri());
System.out.println(ucb.build().expand(foo, bar).toString());
System.out.println(ucb.build().expand(foo, bar).toUriString());
System.out.println(ucb.build().expand(foo, bar).encode().toUri());
System.out.println(ucb.build().expand(foo, bar).encode().toString());
System.out.println(ucb.build().expand(foo, bar).encode().toUriString());
System.out.println(ucb.buildAndExpand(foo, bar).toUri());
System.out.println(ucb.buildAndExpand(foo, bar).toString());
System.out.println(ucb.buildAndExpand(foo, bar).toUriString());
System.out.println(ucb.buildAndExpand(foo, bar).encode().toUri());
System.out.println(ucb.buildAndExpand(foo, bar).encode().toString());
System.out.println(ucb.buildAndExpand(foo, bar).encode().toUriString());

并打印:

http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r
http://example.com/?foo=fo+o&bar=ba%20r

在某些情况下,空格会正确转义,但加号永远不会转义。

我也尝试了这样的UriTemplate

String foo = "fo+o";
String bar = "ba r";
UriTemplate uriTemplate = new UriTemplate("http://example.com/?foo={foo}&bar={bar}");
Map<String, String> vars = new HashMap<>();
vars.put("foo", foo);
vars.put("bar", bar);
URI uri = uriTemplate.expand(vars);
System.out.println(uri);

结果完全相同:

http://example.com/?foo=fo+o&bar=ba%20r

显然,正确的方法是定义工厂并更改编码模式:

String foo = "fo+o";
String bar = "ba r";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
URI uri = factory.uriString("http://example.com/?foo={foo}&bar={bar}").build(foo, bar);
System.out.println(uri);

打印出来:

http://example.com/?foo=fo%2Bo&bar=ba%20r

此处记录了以下内容:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#web-uri-encoding

我认为您的问题是 RFC 3986(UriComponents和扩展UriTemplate所基于的 RFC 3986 不强制要求在查询字符串中转义+

规范对此的看法很简单:

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
query       = *( pchar / "/" / "?" )
URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

如果你的Web框架(例如Spring MVC!(将+解释为一个空格,那么这是它的决定,在URI规范下不是必需的。

参考上述内容,您还将看到!$'()*+,;没有被UriTemplate转义。=&转义,因为Spring对查询字符串的外观采取了"固执己见"的观点 - 一系列键=值对。

同样,#[]和空格转义,因为它们在规范下的查询字符串中是非法的。

当然,如果您只是非常合理地希望查询参数被转义,那么这些都不可能对您有任何安慰!

要实际对查询参数进行编码,以便您的 Web 框架可以容忍它们,您可以使用类似org.springframework.web.util.UriUtils.encode(foo, charset).

我开始相信这是一个错误,我在这里报告:https://jira.spring.io/browse/SPR-16860

目前,我的解决方法是这样的:

String foo = "fo+o";
String bar = "ba r";
String uri = UriComponentsBuilder.
fromUriString("http://example.com/?foo={foo}&bar={bar}").
buildAndExpand(vars).toUriString();
uri = uri.replace("+", "%2B"); // This is the horrible hack.
try {
return new URI(uriString);
} catch (URISyntaxException e) {
throw new RuntimeException("UriComponentsBuilder generated an invalid URI.", e);
}

这是一个可怕的黑客,在某些情况下可能会失败。

对于这个,我仍然希望使用正确的方法解决编码,而不是像您那样使用 Hack。我只会使用下面这样的东西

String foo = "fo+o";
String bar = "ba r";
MyUriComponentsBuilder ucb = MyUriComponentsBuilder.fromUriString("http://example.com/?foo={foo}&bar={bar}");
UriComponents uriString = ucb.buildAndExpand(foo, bar);
// http://example.com/?foo=fo%252Bo&bar=ba+r
URI x = uriString.toUri();
// http://example.com/?foo=fo%2Bo&bar=ba+r
String y = uriString.toUriString();
// http://example.com/?foo=fo%2Bo&bar=ba+r
String z = uriString.toString();

当然,课程如下

class MyUriComponentsBuilder extends UriComponentsBuilder {
protected UriComponentsBuilder originalBuilder; 
public MyUriComponentsBuilder(UriComponentsBuilder builder) {
// TODO Auto-generated constructor stub
originalBuilder = builder;
}

public static MyUriComponentsBuilder fromUriString(String uri) {
return new MyUriComponentsBuilder(UriComponentsBuilder.fromUriString(uri));
}

@Override
public UriComponents buildAndExpand(Object... values) {
// TODO Auto-generated method stub
for (int i = 0; i< values.length; i ++) {
try {
values[i] = URLEncoder.encode((String) values[i], StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return originalBuilder.buildAndExpand(values);
}
}

仍然不是一个最干净的方法,但比做硬编码的替换方法更好

你可以在 Spring 中使用 UriComponentsBuilder (org.springframework.web.util.UriComponentsBuilder(

String url = UriComponentsBuilder
.fromUriString("http://example.com/")
.queryParam("foo", "fo+o")
.queryParam("bar", "ba r")
.build().toUriString();
restTemplate.exchange(url , HttpMethod.GET, httpEntity);

最新更新