http_build_query之Java实现
同事Javaer对接三方支付渠道,对方只有PHP demo,签名始终不过,遂帮忙排查问题,定位到http_build_query,这个函数容易让Javaer误解,比较像是遍历拼接然后urlencode,所以就掉坑里了。先看下函数的定义
function http_build_query(
array|object $data,
string $numeric_prefix = "",
?string $arg_separator = null,
int $encoding_type = PHP_QUERY_RFC1738
): string
demo中只传了第一个参数,这里主要看下第四个参数默认值的含义: By default, PHP_QUERY_RFC1738. If enc_type is PHP_QUERY_RFC1738, then encoding is performed per » RFC 1738 and the application/x-www-form-urlencoded media type, which implies that spaces are encoded as plus (+) signs. If enc_type is PHP_QUERY_RFC3986, then encoding is performed according to » RFC 3986, and spaces will be percent encoded (%20).
RFC1738 有这么一段: Many URL schemes reserve certain characters for a special meaning: their appearance in the scheme-specific part of the URL has a designated semantics. If the character corresponding to an octet is reserved in a scheme, the octet must be encoded. The characters ";", "/", "?", ":", "@", "=" and "&" are the characters which may be reserved for special meaning within a scheme. No other characters may be reserved within a scheme.
没读懂,某些特殊字符会保留不转义?看来要追一下函数的源码(todo),上代码比较一下两种方式得到字符串的区别
$arr = [
'title' => '充值',
'userid' => '18',
'notify_url' => 'https://www.baidu.com',
'return_url' => 'https://www.baidu.com',
'amount' => '1',
'orderno' => '1626-116836307379@183#2064'
];
ksort($arr);
$str = '';
foreach ($arr as $k => $v) {
$str .= $k . '=' . $v . '&';
}
$str = rtrim($str, '&');
echo 'http_build_query: ' . http_build_query($arr) . PHP_EOL;
echo 'urlencode: ' . urlencode($str) . PHP_EOL;
// 输出
http_build_query: amount=1¬ify_url=https%3A%2F%2Fwww.baidu.com&orderno=1626-116836307379%40183%232064&return_url=https%3A%2F%2Fwww.baidu.com&title=%E5%85%85%E5%80%BC&userid=18
urlencode: amount%3D1%26notify_url%3Dhttps%3A%2F%2Fwww.baidu.com%26orderno%3D1626-116836307379%40183%232064%26return_url%3Dhttps%3A%2F%2Fwww.baidu.com%26title%3D%E5%85%85%E5%80%BC%26userid%3D18
可以看到http_build_query对字符串中的=和&未转义,既然如此替换一下urlencode结果中被转义的字符
echo str_replace(['%3D', '%26'], ['=', '&'], urlencode($str)) . PHP_EOL;
// 输出
amount=1¬ify_url=https%3A%2F%2Fwww.baidu.com&orderno=1626-116836307379%40183%232064&return_url=https%3A%2F%2Fwww.baidu.com&title=%E5%85%85%E5%80%BC&userid=18
ok,按这个方式Java实现
public static void main(String[] args) {
TreeMap<String, String> map = new TreeMap<>();
map.put("title", "充值");
map.put("userid", "18");
map.put("notify_url", "https://www.baidu.com");
map.put("return_url", "https://www.baidu.com");
map.put("amount", "1");
map.put("orderno", "1626-1168363073791832064");
System.out.println(httpBuildQuery(map));
}
public static String httpBuildQuery(Map<String, String> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key).append("=").append(value).append("&");
}
sb = new StringBuilder(sb.substring(0, sb.length() - 1));
sb = new StringBuilder(URLEncoder.encode(sb.toString(), StandardCharsets.UTF_8));
sb = new StringBuilder(sb.toString().replace("%3D", "=").replace("%26", "&"));
return sb.toString();
}
问题是解决了,但是很明显这样的方式很片面,因为无法确定http_build_query还有哪些特殊字符是不转义的,想到两个解决办法
- 看http_build_query的源码
- 上面 RFC 1738 提到特殊字符都试一遍