一、为什么必须编码
URL 的语法只允许一部分 ASCII 字符直接出现,其余字符都有特殊含义或根本无法传输:
- 保留字符
: / ? # [ ] @ ! $ & ' ( ) * + , ; =在 URL 里承担分隔语义,出现在数据里必须转义,否则会被解析成结构的一部分。 - 非 ASCII 字符(中文、emoji)以及空格、引号、
%本身,都不能直接放进 URL。 - 控制字符(换行、制表)更不允许,常导致请求被中途截断。
编码的目的不是保密,而是让任意数据都能安全地塞进一段 ASCII 文本里。
二、规则:先 UTF-8,再按字节写 %HH
百分号编码的做法是:把字符按 UTF-8 编成字节,每个字节写成 % 加两位十六进制(大写小写都合法,习惯用大写)。
| 字符 | UTF-8 字节 | 编码结果 |
|---|---|---|
| 中 | E4 B8 AD | %E4%B8%AD |
| 你 | E4 BD A0 | %E4%BD%A0 |
| 空格 | 20 | %20 |
| & | 26 | %26 |
| / | 2F | %2F |
所以一个汉字通常变成 3 组 %XX(9 个字符),emoji 则是 4 组。看到 %E4%B8%AD 就知道它一定是 UTF-8 的产物——如果是 GBK,同一个「中」会编成 %D6%D0,两者不能互认,这也是跨系统乱码的常见来源。
三、空格:+ 还是 %20
两种写法都存在,取决于上下文:
application/x-www-form-urlencoded(表单提交、查询串)里,空格习惯写成+。- URL 的路径部分、以及严格的 RFC 3986 场景,空格必须写
%20。 - JSON 请求体不属于 URL,里面的空格不用编码。
很多服务端两种都接受,但做签名时不行:签名算法通常明确规定用哪一种,写错就验签失败。
四、JS 里两个函数的区别
encodeURI('https://a.com/s?q=中文&x=1')
// https://a.com/s?q=%E4%B8%AD%E6%96%87&x=1 ← 保留了 ://?&=
encodeURIComponent('中文&x=1')
// %E4%B8%AD%E6%96%87%26x%3D1 ← 连 & = 都编码
结论很直接:整条 URL 用 encodeURI,单个参数值用 encodeURIComponent。给参数值用 encodeURI 是典型错误——值里的 & 不会被编码,拼进查询串后就变成了一个新参数。
Python 对应 urllib.parse.quote(默认安全字符 /)和 quote_plus(空格转 +);Java 用 URLEncoder.encode(注意它把空格转成 +,用在路径上要再替换回 %20)。
五、五个高频坑
- 只编码了值、没编码键,或者反过来,导致键值错位。
- 双重编码:已经编码过的串又编一次,
%变成%25,服务端解出来仍是原文的编码形式。 +的歧义:值里本来就有加号,未编码时会被服务端当成空格。- 签名顺序不一致:云存储、支付类接口常要求「参数排序 → 用
&拼接 → 对整串编码」,步骤错一步就 403。 - 解码次数:网关解一次、应用再解一次,含
%25的数据会被解坏。约定只解一次最安全。
小结
URL 编码 = UTF-8 字节 + %HH;保留字符必须转义;整条 URL 用 encodeURI,参数值用 encodeURIComponent;空格在查询串可用 +,路径必须 %20。写签名逻辑前先读一遍文档规定的编码方式,比事后对 403 排查省时间。
需要反复试编码结果时,用 URL 编码解码工具 双向验证最快;如果要把二进制塞进文本,可以看 Base64 原理。