RFC3986
首先,了解一下 RFC 3986 标准,简单讲就是规定了如下:除了 数字
+ 字母
+ -_.~
不会被转义,其他字符都会被以百分号(%)后跟两位十六进制数 %{hex}
的方式进行转义。
再者,了解下 www
的 post form data
也就是 x-www-form-urlencode
的编码规则:除 -_.
(没有 ~
) 之外的所有 非字母
、非数字
的字符都将被替换成 百分号(%)后跟两位十六进制数 %{hex}
,空格
(注意)则编码为加号 +
。
二者的区别如下:
1、rfc3986
对 ~
不做转码,x-www-form-urlencode
对 ~
做转码 %7E
。
2、rfc3986
对 空格
转为 %20
,x-www-form-urlencode
对 空格
转为 +
。
接下来看几个高级语言的 url
编码方式。
js encodeURIComponent
php urlencode/rawurlencode
go url.QueryEscape
js
encodeURIComponent
console.log(encodeURIComponent("hello233 ~-_."))
hello233%20~-_.
可以看到 js 完全遵循 rfc3986
,保留了 ~-_.
,空格
被转码为 %20
,正规。
php
urlencode
<?php
echo urlencode("hello233 ~-_.");
hello233+%7E-_.
空格
转 +
,只保留 -_.
没保留 ~
,典型的 x-www-form-urlencode
规则。
rawurlencode
<?php
echo rawurlencode("hello233 ~-_.");
hello233%20~-_.
rfc3986
模式。
http_build_query
这里要清楚,http_build_query
只对 key
和 val
做了 urlencode
处理,=
和 &
符号没有处理(go
的 url.Values.Encode
能清楚的看到如述的处理过程)。
<?php
echo http_build_query(["msg" => "hello233 ~-_.", "name" => "sqrtCat"]);
msg=hello233+%7E-_.&name=sqrtCat
go
go
的编码方式...就比较有意思了,这也是我写这篇文章的起因。go
提供的 url.Values.Encode
(相当于 php
的 ksort+http_build_query
)、url.QueryEscape
(相当于 php
的 urlencode/rawurlencode
) 。
url.Values.Encode
类似 php
的 http_query_builder
, 只对 key
和 val
做转义处理,=
和 &
不做处理,看下实现就明了了。
func (v Values) Encode() string {
if v == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
vs := v[k]
keyEscaped := QueryEscape(k)
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')//拼接
}
buf.WriteString(keyEscaped)//已转义
buf.WriteByte('=')//拼接
buf.WriteString(QueryEscape(v))//转义
}
}
return buf.String()
}
可以看到 key
和 val
是使用 url.QueryEscape
做的转义,那我们继续看它的转义标准。
url.QueryEscape
func main() {
fmt.Println(url.QueryEscape("hello233 ~-_."))
}
hello233+~-_.
是不是有些懵逼?~
被保留了,rfc3986
?但 空格
却转为了 +
而不是 %20
,这不是 x-www-form-urlencode
才会做的么,但 ~
也没转为 %70
呀。
总结
- 如果你的数据里没有
空格
和~
可以直接略过了。 其实无论是
rfc3986
还是x-www-form-urlencode
还是go
的混合编码,浏览器的地址栏都可以正确解析处理的,大家可以尝试打印一下GET
参数,三项都可以正确获取到数据的。(但拿rfc3986
编码的结果,让遵循x-www-form-urlencode
模式的解码是解不出正确数据的,你就简单认为这是浏览器地址栏的特性好了)。http://0.0.0.0:8888/?rfc3986=hello233%20~-_.&urlencode=hello233+%7E-_.&go=hello233+~-_.
array(3) { ["rfc3986"]=> string(12) "hello233 ~-_" ["urlencode"]=> string(13) "hello233 ~-_." ["go"]=> string(13) "hello233 ~-_." }
- 我掉坑的主要原因是一些历史遗留的服务还在使用
参数字典排序签名验证
的模式,碰巧数据中含有空格
和~
,go
用url.Values.Encode
对空格
转+
,对~
保留,php
用http_build_query
对空格
转+
,但对~
转%7E
,这就导致两端签名始终不匹配。
解决方案
- 遵循
x-www-form-urlencode
模式。
1.1go
端url.Values.Encode
做~
替换为%7E
的处理。
1.2php
端直接ksort + http_build_query
即可。 - 遵循
rfc3986
标准。
2.1 前提保证数据中没有空格
。go
在没有空格
时也变为了rfc3986
模式。
2.2php
端rawurlencode(urldecode(http_build_query($params)))
。
2.3go
端url.QueryEscape(url.QueryUnescape(url.Values.Encode()))
。
2.4http_build_query
/url.Values.Encode()
帮你快速构建queryString
但对key
和val
已转码,所以urldecode
/url.QueryUnescape
得到原始的key=val&key=val
后再编码。 - 不对代签名的
queryString
做转义。
3.1php
端urldecode(http_build_query($params))
后计算签名。
3.2go
端url.QueryUnescape(url.Values.Encode())
后计算签名。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。