UPTools UPTools
安全 主笔:工具匠

URL 里的中文和空格怎么传?encodeURI 和 encodeURIComponent 的区别

上周帮人调一个 OAuth 登录回调,前端拼好的 redirect_uri 传到授权服务器,回来时回调地址被截断了一半。我让他把那个 URL 打印出来看一眼,里面嵌着另一个带 & 的 URL,& 后面的部分被授权服务器当成自己的查询参数吃掉了。他对着屏幕愣了三秒。

URL 编码踩坑的姿势很多,但绝大多数都指向同一个根因:没分清 encodeURIencodeURIComponent。这两个 API 长得像,行为差一截,用错就是乱码或参数错乱。

URL 只允许一部分 ASCII 直接出现

URL 的字符集是有规矩的。字母、数字、还有 -_.~ 这几个保留字符可以直接写,其他字符比如中文、空格、#&=? 等,必须做百分号编码(percent-encoding):把字符的字节写成 % 加两位十六进制。

中文尤其要留意。一个中文字符在 UTF-8 下通常占 3 个字节。比如「你」的 UTF-8 字节是 E4 BD A0,编码后就是 %E4%BD%A0。所以你看到地址栏里搜索「你好」时变成一长串 %E4%BD%A0%E5%A5%BD,不是谁写错了,是规矩本来就这样。一个汉字对应三个 %XX,反过来推字数也方便。

两套 API 长得像,行为差一截

这是全文的核心。JavaScript 给了两套编码函数,名字像,行为完全不同:

encodeURI('https://a.com/search?q=你好&src=微信')
// 'https://a.com/search?q=%E4%BD%A0%E5%A5%BD&src=%E5%BE%AE%E4%BF%A1'
// ↑ 中文被编码,但 : / ? & = 这些结构符全保留

encodeURIComponent('https://a.com/search?q=你好&src=微信')
// 'https%3A%2F%2Fa.com%2Fsearch%3Fq%3D%E4%BD%A0%E5%A5%BD%26src%3D%E5%BE%AE%E4%BF%A1'
// ↑ 结构符 : / ? & = 也被编码

看出来了吗?encodeURI 只编码「URL 非法字符」,保留 :/?#&= 等结构符,因为它的设计目标是编码一整条 URL,结构符是 URL 的骨架,不能动。encodeURIComponent 把结构符也编码了,因为它的设计目标是编码单个参数值,参数值里出现的 &= 已经失去结构意义,必须当作普通字符。

判断方法很简单:你手里是一整条 URL,用 encodeURI;你手里是一个参数值,用 encodeURIComponent

回到开头那个 OAuth 的坑。他拼的 URL 大概长这样:

https://auth.server.com/authorize?client_id=xxx&redirect_uri=https://a.com/cb?src=微信&state=1

redirect_uri 的值是 https://a.com/cb?src=微信,但里面的 ?& 没编码,授权服务器解析时把 &state=1 当成了它自己的参数。src 后面那部分回不来。正确做法是用 encodeURIComponentredirect_uri 的值包一层,把嵌套的 URL 当成一个纯字符串塞进去。

const redirect = 'https://a.com/cb?src=微信'
const authUrl = 'https://auth.server.com/authorize?client_id=xxx&redirect_uri='
  + encodeURIComponent(redirect) + '&state=1'

参数值永远用 encodeURIComponent,嵌套 URL 也是参数值。

空格的坑:%20 和 + 不等价

空格在 URL 里也有两种写法,两者不能混。纯 URL 场景里空格编码成 %20,这是 RFC 3986 的规定。但 HTML 表单提交(Content-Type: application/x-www-form-urlencoded)里有个老约定,空格用 + 表示。

encodeURI('a b')             // 'a%20b'
encodeURIComponent('a b')    // 'a%20b'
// 两者都是 %20,JS 这俩 API 不会给你 +

// 但表单场景,比如 jQuery 的 $.param、PHP 的 http_build_query:
'a b''a+b'

混用就会出问题。后端用 decodeURIComponent 解一个带 + 的字符串,+ 不会被还原成空格,原样保留;反过来用表单解码函数解一个 %20,结果也对,但习惯上容易看错。简单记:前端用 JS 编码就用 %20,只有走表单 application/x-www-form-urlencoded 时才用 +,别自己手动把 + 和空格来回换。

别对已经编码的字符串再编码一次

这个坑在自动化脚本里特别常见。某个上游已经返回了编码后的 URL,比如 %E4%BD%A0,你以为没编码,又调一次 encodeURIComponent% 这个字符本身是特殊字符,会被编码成 %25。于是 %E4%BD%A0 变成 %25E4%25BD%25A0,到服务端解码回来变成字面字符串 %E4%BD%A0 而不是「你」。

判断字符串是否已经编码,看有没有大量 % 后跟两个十六进制字符的模式。编码和解码都只做一次。如果链路上有多个环节都可能做编码,约定清楚只在最外层做一次。

实操:先把 URL 丢进工具看一眼

排查这类问题最快的方法,是把 URL 粘到 UPTools URL 编码解码工具 里,先解码一次看看还原出来的是什么,再决定要不要编码。整条 URL 和单个参数值都能处理,支持 UTF-8 中文,本地运算不传任何数据。看着还原结果对不对,基本就能定位是编码漏了、用错 API、还是被重复编码了。

三句话总结

URL 只允许一部分 ASCII 直接出现,中文等字符必须百分号编码,中文「你」UTF-8 三字节对应 %E4%BD%A0。整条 URL 用 encodeURI(保留结构符),单个参数值用 encodeURIComponent(结构符也编码),嵌套 URL 永远是参数值。空格在纯 URL 里是 %20,在表单场景里是 +,两者不等价;对已编码字符串再编码一次,% 会变成 %25,编码解码都只做一次。

相关文章