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 encodeURIComponentphp urlencode/rawurlencode go url.QueryEscape
js
encodeURIComponent
console.log(encodeURIComponent("hello233 ~-_."))
hello233%20~-_.可以看到 js 完全遵循 rfc3986,保留了 ~-_.,空格 被转码为 %20,正规。
php
urlencode
<?phpecho urlencode("hello233 ~-_.");
hello233+%7E-_.空格 转 +,只保留 -_. 没保留 ~,典型的 x-www-form-urlencode 规则。
rawurlencode
<?phpecho rawurlencode("hello233 ~-_.");
hello233%20~-_.rfc3986 模式。
http_build_query
这里要清楚,http_build_query 只对 key 和 val 做了 urlencode 处理,= 和 & 符号没有处理(go 的 url.Values.Encode 能清楚的看到如述的处理过程)。
<?phpecho 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())后计算签名。