免费 URL 编码器/解码器
即时对 URL 和 URI 组件进行编码或解码。
URL 编码到底在做什么
URL 编码(也称为百分号编码)是网络用来将任意字符数据塞入 URL 的机制。URL 最初是为一个极小的 ASCII 字母表设计的(字母、数字和一小组标点符号)而该字母表之外的字符(空格、带重音的字母、与号、不作路径分隔符的斜杠、表情符号、任何西里尔字母、CJK 或阿拉伯字符)必须先被转义,才能安全地通过 HTTP、服务器日志、浏览器地址栏、复制粘贴处理器和 shell 管道传递。编码规则很直接:取该字符的 UTF-8 字节序列,把每个字节写成一个百分号后跟两位十六进制数字。一个空格(一字节,0x20)变成 %20。一个与号(0x26)变成 %26。欧元符号 €(三个 UTF-8 字节 E2 82 AC)变成 %E2%82%AC。编码形式是可逆的(网络上每一个 URL 解析器都懂得如何解码)结果是一个只包含“安全” ASCII 字符的字符串。
编码规则的简史
百分号编码的约定可追溯至 RFC 1738(“Uniform Resource Locators (URL)”,Tim Berners-Lee、Larry Masinter 和 Mark McCahill,1994 年 12 月),这是 IETF 发布的第一份正式 URL 规范。RFC 1738 定义了原始的“保留”字符集(在 URL 中具有结构含义、当作数据使用时必须编码的字符)和原始的“非保留”字符集(永远不需要编码的字符)。该规范在 RFC 2396(Berners-Lee、Fielding、Masinter,1998 年 8 月)中被实质性修订,随后在 RFC 3986(“Uniform Resource Identifier (URI): Generic Syntax”,Berners-Lee、Fielding、Masinter,2005 年 1 月)中再次(也是决定性地)修订,至今仍是正式的 URI 规范。RFC 3986 将非保留集扩大到包含波浪号 (~),将 UTF-8 正式确立为 IRI 中非 ASCII 字符的编码方式,并收紧了关于何时必须把保留字符百分号编码、何时保持原样的规则。对于非 ASCII URI,RFC 3987(“Internationalized Resource Identifiers”,Duerst 和 Suignard,2005 年 1 月)定义了 IRI 扩展,让 URL 可以包含原生 Unicode 字符,并通过 UTF-8 + 百分号编码把 IRI 形式映射到标准 URI 形式。专门针对域名,RFC 3492 定义了 Punycode(Costello,2003 年 3 月),即在 DNS 中表示 Unicode 域名标签 (xn--exmpla-...) 的编码方式,结构相似但使用不同的算法。现代网络事实上的 URL 规范是 WHATWG URL Living Standard,由 Anne van Kesteren 编辑,编码了浏览器的实际行为,为了与 URL 在实践中的工作方式保持向后兼容,有时会与 RFC 3986 出现分歧。
保留、非保留以及那些总要被编码的字符
RFC 3986 把 URI 字符分为三类。非保留字符(字母 A-Z a-z、数字 0-9 以及四个符号 - _ . ~)永远不需要编码,且即便编码了也不会获得任何含义。保留字符(: / ? # [ ] @ ! $ & ' ( ) * + , ; =)在 URL 中具有结构含义(斜杠分隔路径段,问号引出查询,与号分隔查询参数,井号引出片段)。保留字符在其结构性角色中可以不编码出现;当它们作为普通数据使用时必须编码。所有其他字符(控制字符、空格、百分号本身以及非 ASCII UTF-8 序列中的任何字节)都必须始终被百分号编码。百分号字符很特殊:当它作为数据出现时必须编码为 %25,因为在 URL 上下文中未编码的百分号会引出一个百分号编码的转义序列,解析器会试图将其解码。跳过这一步会产生一些最常见的 URL 处理 bug。
encodeURI 与 encodeURIComponent,何时用哪一个
JavaScript 自带两个内置编码器,自 1999 年(ES3)以来都已在 ECMAScript 中标准化。在它们之间的选择是 Web 代码中最常见的 URL 编码决定,选错会产生微妙的 bug,在简单输入上能通过测试,但碰到含有与号、井号或斜杠的真实用户数据时就会出问题。
encodeURIComponent(编码除非保留集之外的所有内容。当你在编码一段单一的数据并把它嵌入到 URL 中时使用)一个查询参数值、一个路径段、一个片段标识符、一个表单字段值。字符串hello world&more会变成hello%20world%26more,其中的与号被编码,因此当 URL 之后被读取时不会被解析为查询参数分隔符。这是用于“我有用户输入,我想安全地把它放进 URL 里”的合适工具。encodeURI,只编码那些在 URL 中处处都非法的字符。它有意保留保留字符(: / ? # [ ] @ ! $ & ' ( ) * + , ; =)原样,因为这些字符可能正在执行结构性作用。当你在编码一整条 URL,且其结构已经就位(一条带有自己的?和&分隔符的 URL,或一条在路径或查询中含有非 ASCII 字符、需要在不破坏 URL 语法的前提下编码的 URL)时使用。字符串https://example.com/search?q=hello world会变成https://example.com/search?q=hello%20world,斜杠和问号被保留,只有空格被编码。
心法:encodeURIComponent 用于数据,encodeURI 用于 URL。用错的那一个,要么会破坏 URL 结构(在整条 URL 上调用 encodeURIComponent 会转义你需要保留的斜杠和与号),要么会漏掉对用户输入的转义(在查询值上调用 encodeURI 让与号通过,于是你的数据被解析为多个参数)。
application/x-www-form-urlencoded,另一种编码
还有一种与之相关、专门用于 HTML 表单提交的编码:application/x-www-form-urlencoded。除一处细节外它与 URL 百分号编码几乎相同,空格被编码为 + 而非 %20。这一约定来自最初的 HTML 表单规范,并在 URL Living Standard 中专为 application/x-www-form-urlencoded 序列化器保留下来。表单编码数据的解码器必须反转该约定:表单数据中字面量 + 会被编码为 %2B,而编码字符串中的 + 会解码回空格。JavaScript 的 URLSearchParams API 自动处理这一约定;encodeURIComponent 并不处理,它始终把空格编码为 %20。对于为 HTML 表单 action 手工拼装的查询字符串,请使用 URLSearchParams,而不是对每个值单独调用 encodeURIComponent。
什么时候你确实需要这个工具
- 调试 API 调用。你在用一个含有空格或特殊字符的查询参数访问 REST 端点,但收到 400 错误或意外结果。在这里编码该参数值,把结果粘贴进你的请求,确认 API 正确地处理了输入。
- 手工构建一条可分享的 URL。组装一条指向搜索结果页、SaaS 仪表板筛选视图或预填表单的深链,任何你需要把搜索查询或选择状态嵌入 URL 的地方。
- 解码一条捕获的 URL,看看里面到底是什么。一条被记录的 URL 含有
%E2%9C%93%20done,它解码出来是什么?粘贴,点击解码,看到✓ done。 - OAuth 与 webhook URL。OAuth 流程中的
redirect_uri参数必须被精确地百分号编码;弄错会产生晦涩的“redirect_uri mismatch”错误。含有用户数据的查询参数的 webhook URL 需要谨慎编码。 - 下载链接中含有非 ASCII 字符的文件名。指向名为
café-menu.pdf的文件的直接链接,需要把é百分号编码,链接才能在所有浏览器和 shell 工具中正常工作。 - 数据库连接字符串。PostgreSQL、MySQL 及类似数据库的 URL(
postgres://user:p@ssw0rd@host/db)若密码中含有@、:、/或其他会让 URL 解析器困惑的保留字符,则必须把密码百分号编码。
安全:编码的意义不止于美观
不正确的 URL 编码是长期以来的网络漏洞来源。HTTP 响应拆分利用 URL 参数中未编码的换行符(CR、LF,%0D%0A),这些换行被反射进 HTTP 头,让攻击者能注入任意头部甚至一整条响应。开放重定向利用粗糙的 URL 处理:在重定向之前没有正确解码并校验用户提供的 URL。服务端请求伪造(SSRF)可以滥用编码字符绕过 URL 白名单,一段拦截 “http://internal” 的正则若服务器在正则检查后才解码,会漏掉 “http://%69nternal”。通过 URL 参数的 SQL 注入本身并非 URL 编码问题,但当单引号和其他 SQL 元字符存活到数据库查询时会被加剧。OWASP 自 2000 年代初起的建议是:进入边界时编码,离开边界时解码,永远不要在同一个缓冲区里混用编码和未编码的形式。本工具只做编码/解码这一步,你拿结果做什么取决于你,安全责任在应用层。
双重编码,最常见的 bug
双重编码发生在一个已经编码过的字符串再被编码一次时。在已编码 URL 中,空格字符是 %20;再编码一次就得到 %2520(因为 % 编码为 %25)。当接收方解码一次时,得到的是 %20 而不是空格。当他们解码两次时(在罕见的支持双重解码的场景里),他们得到空格(但大多数解析器并不会,于是 URL 就在地址栏里悄悄含有可见的 %20。修复办法永远是:如果你不知道输入是否已经编码,先解码,再恰好编码一次。同样的模式也适用于代码)绝不要对同一个字符串调用两次 encodeURIComponent。如果你必须把一个值在不同上下文之间往返传递,请先解码先前的编码,再为新上下文重新编码。本工具的“交换”按钮可以帮你完成诊断循环:粘贴可疑 URL,点击解码看里面是什么,点击交换,再点击编码取回标准的编码形式。
隐私:仅在浏览器中执行
URL 经常嵌入敏感数据,查询参数中的认证令牌、路径段中的用户标识、包含个人上下文的搜索查询、OAuth 状态值、含有 API 密钥的 webhook URL。服务端 URL 编码器会把你粘贴进去的每一条 URL 留一份在它们的日志里。本工具使用 JavaScript 内置的 encodeURIComponent、encodeURI、decodeURIComponent 和 decodeURI 函数,在你的浏览器标签页中本地执行。没有上传步骤、没有遥测、没有日志,你可以在点击编码时打开开发者工具的网络标签页查看(不会触发任何请求),或者在页面加载后让它离线(飞行模式),编码器仍然能用。对含有令牌、API 密钥、内部端点的 URL,或任何你不希望被复制到陌生人硬盘上的 URL 都很安全。
常见问题
何时应该对文本进行 URL 编码?
每当你把用户输入或含有特殊字符的数据嵌入 URL 时,查询字符串中的搜索查询、路径中的文件名、API 请求中的参数值、OAuth 流程中的重定向目标。没有编码的话,特殊字符要么破坏 URL 结构(值中未转义的 & 会被解析为参数分隔符),要么引入安全漏洞(未转义的换行符使 HTTP 响应拆分成为可能)。经验法则:任何含有字母、数字和四个符号 -_.~ 之外内容的字符串,进入 URL 之前都需要编码。
什么是双重编码?如何避免它?
双重编码发生在已经编码过的文本被第二次编码时,把 %20 变成 %2520(因为百分号字符本身被编码为 %25)。只解码一次的接收方拿到的是半解码的形式而非原始内容。永远记住:如果不知道一个字符串是否已经编码,先解码,再恰好编码一次。在代码中,绝不要在另一次 encodeURIComponent 的输出上再调用 encodeURIComponent。
此工具对敏感 URL 安全吗?
是的。编码/解码器使用 JavaScript 内置函数,完全在你的浏览器中运行。你粘贴的 URL 永远不会跨网络,你可以在点击编码时打开开发者工具的网络标签页查看(不会触发任何请求),或者在页面加载后让它离线(飞行模式)。对含有 OAuth 令牌、API 密钥、内部端点地址或任何你不希望被记录到第三方服务器上的值的 URL 都很安全。
为什么我的表单数据里是加号而不是 %20?
HTML 表单提交使用一种独立但相关的编码,application/x-www-form-urlencoded,按照历史约定把空格编码为 + 而非 %20。两种形式在 URL 查询字符串中都是有效的;现代解析器都接受。JavaScript 的 URLSearchParams API 使用表单编码约定;encodeURIComponent 始终使用 %20。如果你的数据需要与旧的表单处理代码互操作,请使用 URLSearchParams;其他场景两种形式都可以。
非 ASCII 字符和表情符号怎么办?
现代 URL 编码使用 UTF-8:每个非 ASCII 字符先转换为它的多字节 UTF-8 表示,然后每个字节再被百分号编码。欧元符号(€,三个 UTF-8 字节)变成 %E2%82%AC;像火箭 🚀 这样的表情符号(四个字节)变成 %F0%9F%9A%80。RFC 3987(IRI)和 WHATWG URL Standard 都把这种 UTF-8 优先的约定正式化。较旧的系统有时使用 Latin-1 或其他编码;如果你在解码古老的 URL 时看到结果像乱码,源头可能在百分号编码之前使用了不同的字符编码。