URL 里的中文和空格怎么传?encodeURI 和 encodeURIComponent 的区别
上周帮人调一个 OAuth 登录回调,前端拼好的 redirect_uri 传到授权服务器,回来时回调地址被截断了一半。我让他把那个 URL 打印出来看一眼,里面嵌着另一个带 & 的 URL,& 后面的部分被授权服务器当成自己的查询参数吃掉了。他对着屏幕愣了三秒。
URL 编码踩坑的姿势很多,但绝大多数都指向同一个根因:没分清 encodeURI 和 encodeURIComponent。这两个 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 后面那部分回不来。正确做法是用 encodeURIComponent 把 redirect_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,编码解码都只做一次。
相关文章
- 用 Math.random 生成密码为什么不安全?CSPRNG 与密码熵 用 Math.random 生成密码或 Token 是常见错误,它可被预测。本文讲清为什么必须用 crypto.getRandomValues,以及密码强度由字符集和长度共同决定。
- 为什么在线工具比本地软件更注重隐私?本地处理如何保护你的数据 同样是处理敏感文件,为什么说浏览器本地完成的在线工具反而比某些本地软件更注重隐私?本文讲清本地处理的原理、它保护了什么、以及它的边界。
- SVG 直接内联 HTML 会触发 XSS?压缩与安全检测一次讲清 SVG 不只是图片,它是 XML,能内嵌 script 和事件属性。用 img 引入不会执行脚本,直接内联进 HTML 就可能触发 XSS。本文讲清风险边界和压缩要点。