URL 特殊字符怎么编码:&、+、%、# 各走哪条路,一条规则说清

参数莫名被截断、多出一个 key、加号变空格、中文变乱码、签名怎么都对不上——五个症状都源于「该用哪条编码路径」没分清。给出三条规则:哪些字符必须编码、encodeURI / encodeURIComponent / URLSearchParams 各自的分工、以及二次编码是在哪一层被叠加进去的。

约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

先说结论:什么时候该用哪一个函数

三个函数各管一段,混用是绝大多数 URL 问题的根源。encodeURIComponent 编「一个值」,它会把 & = ? # / + 连同所有非 ASCII 字符一起编掉——放进查询串的参数值就用它。encodeURI 编「一整段已拼好的 URL」,它故意保留 & = ? / # 这些结构符号,所以拿它编值是错的(& 留着,参数照样被切开)。URLSearchParams 一步到位地做「键=值」的组装与编码,内部用的就是 component 级规则,最省心。路径段(path segment)属于 component 规则,锚点(# 后面的 fragment)根本不会发到服务器。

症状 → 真正的原因 → 修法

症状真正的原因修法
参数值里带 &,服务端多收到一个参数未编码,& 被当成参数分隔符用 URLSearchParams 或 encodeURIComponent 编值
值里的 = 号导致 key 被截断手写 query 串,第一个 = 之后全归 value同样走参数化组装,别手拼
搜 "a b" 查不到结果空格在表单编码里变 +,而服务端只解 %20统一用 URLSearchParams,或解码后把 + 还原为空格
中文参数变乱码按 GBK / Latin-1 编解码,或编了两层全程 UTF-8,且只编一层
值出现 %2520 这种三段式前端编了一次,库又编了一次只在一层编,或在明确边界解码一次
带 # 的值让后面的内容全丢# 之后是 fragment,从不发给服务器编码 # 为 %23
签名校验总是失败两端编码规则或参数排序不同约定「先排序、再编码、只编值」的统一规则
路径里的斜杠被当成目录分隔路径段也按值编了 /按段编码,保留分隔斜杠
✗ 手工拼 query:三个字符各坏一次
const q = "Tom & Jerry";
const note = "a=b";
const tag = "C# 教程";

// ?q=Tom & Jerry&note=a=b&tag=C# 教程
fetch("/api/search?q=" + q + "&note=" + note + "&tag=" + tag);

// 服务端收到:
// q=Tom         (& 之后成了新的分隔符)
// Jerry、note=a、b、tag=C   (= 和 # 各切一刀)
// 整条 URL 里的中文根本没编码
✓ 交给 URLSearchParams 组装
const p = new URLSearchParams();
p.set("q", "Tom & Jerry");
p.set("note", "a=b");
p.set("tag", "C# 教程");

// ?q=Tom+%26+Jerry&note=a%3Db&tag=C%23+%E6%95%99%E7%A8%8B
fetch("/api/search?" + p.toString());

// 空格编成 +(表单约定),& = # 编成 %26 %3D %23,中文按 UTF-8 逐字节编
// 读取:new URL(req.url, "http://x").searchParams.get("q")
三个函数对同一个值的输出对比
const v = "a&b=c 中文#d/e+f";

encodeURIComponent(v)  // a%26b%3Dc%20%E4%B8%AD%E6%96%87%23d%2Fe%2Bf   ← 值用它
encodeURI(v)            // a&b=c%20%E4%B8%AD%E6%96%87#d/e+f               ← & = # / 保留,别用来编值
new URLSearchParams({k: v}).toString()
                       // k=a%26b%3Dc+%E4%B8%AD%E6%96%87%23d%2Fe%2Bf     ← 空格是 +

加号变空格:为什么同一个空格有时变 + 有时变 %20

空格有两种编码,取决于编码器声明的上下文。application/x-www-form-urlencoded(HTML 表单、URLSearchParams 默认)把空格写成 +;RFC 3986 的通用百分号编码(encodeURIComponent)把空格写成 %20。服务端解码时的行为决定了它能不能对上:多数语言把查询串里的 + 还原成空格,但只有一部分在路径段里也这么做。所以如果你的值可能出现在路径中,或者服务端解码器不认 +,就统一用 %20(把空格写进 URL 前先 encodeURIComponent 整段)。反过来,解析端必须显式把 + 还原成空格,否则 "a b" 会变成 "a+b"。

二次编码是谁加的:在哪一层解一次

%2520 这种三段式的成因永远是「同一个值被编了两次」:前端为了安全先编一次,URL 库组装 query 时又编一次,或者客户端编完、网关又编一次。判断方法很简单——把错误值整体解码一次,如果得到的是合法的 percent-encoding(%20、%E4%B8%AD 这种形态),说明你看到的就是双层编码的中间态,而不是最终形态。正确做法是划清边界:编码只发生在「离开你的进程」的那一刻(前端或 CLI 出口处),中间层一律不解码;服务端在入口处解码一次,之后代码里拿到的永远是明文,后面再拼 SQL、再放 JSON 都不再需要任何编码。

签名校验总失败:两边规则不同,不是算法不同

  1. 先约定编码规则并写进文档:值编码(component 级)、键名是否大小写敏感、是否 URL 解码后再参与签名。
  2. 参数排序要一致:常见做法是按键名字典序升序,跳过签名与时间戳这类签名自身的字段,用同样的分隔符拼接。
  3. 拼接时不要用分隔符拼接「值」,而是拼接「编码后的键=编码后的值」;已经编过的值不要编第二次。
  4. 时间戳与 nonce 要一起参与签名,并把时间戳的允许偏差(例如 ±5 分钟)与 nonce 的存储一起实现,否则重放攻击会打进来。
  5. 对齐字符集与换行:Windows 上多出的 \r 参与签名会让两边结果不同;请求体与查询串分别计算,不要混。

五个最容易漏掉的字符

  • & 和 =:query string 的结构字符。值里出现就必须编码,编码成 %26 和 %3D。
  • #:一旦出现,后面全部变成 fragment,服务器收不到。编码成 %23。注意 URL 解析器在解码时会先按 # 切分,所以编码要发生在拼接之前。
  • +:在值里它代表空格,编码成 %2B 才能表示加号本身。Base64 的输出含 + 和 /,走 URL 时应改用 base64url(- 和 _,无填充),或整体做 component 编码。
  • 空格与换行:换行(\n、\r)编码成 %0A、%0D,在日志、头值、表单里都是常见的注入载体,任何情况下不要留原字符。
  • 中文与全角标点:所有非 ASCII 字符按 UTF-8 逐字节编码(中文一个字 3 字节,%E4%B8%AD 这样的三段)。全角引号“ ” 会被某些后端当普通字符放过,但它们不是安全字符——能用半角就用半角。

从输入到落库的正确顺序

把上面所有规则压成一条流水线:用户输入保持原文;写进 URL 时只编一次、用 component 规则、只针对值;服务端在入口解码一次到明文;之后所有校验、存储、拼接都在明文上进行;输出到另一个上下文(HTML、SQL、另一个 URL)时重新按那个上下文的规则处理一次。「解码一次、之后全程明文」这条最难坚持,也最容易出事——很多系统在中间层「顺手」再解码一次,就从这里开始出问题了。

常见问题

URL 参数里的中文乱码怎么解决?

两端必须都是 UTF-8,而且只编一层。客户端用 encodeURIComponent 或 URLSearchParams 把中文按 UTF-8 逐字节编成 %E4%B8%AD 这样的形式;服务端用 UTF-8 解一次。如果服务端是 GBK(常见于老旧 Java 项目或未设 charset 的响应),要么改服务端,要么在响应头显式声明 charset=utf-8。解码后出现「锟斤拷」这类文字就是编码选错了,可以用 乱码修复工具反查真实编码。

encodeURI 和 encodeURIComponent 有什么区别?

encodeURI 编一整段已拼好的 URL,会保留 & = ? / # 这些结构符号;encodeURIComponent 编「一个值」,把这些结构字符全部编掉。规则很简单:拼 query 的值用 encodeURIComponent,用 encodeURI 只会让 & 继续把参数切开。拿不准的时候用 URLSearchParams 一步组装,它内部就是 component 规则。

值里的加号变成了空格,怎么修?

取决于方向。服务端把 + 收到当空格:客户端应把加号编成 %2B,或者整体改用 encodeURIComponent(它把空格编成 %20,不会产生 +)。前端解析查询串时把字面量 + 还原成空格:用 URLSearchParams 就自动处理,手写解析则要写 replace(/\+/g, " ")。

URL 编码两次(%2520)怎么排查?

这是同一个值被编了两次:前端编一次,URL 库组装 query 时又编一次。判断方法是手动解码一次,如果结果仍是合法的 percent-encoding(%20、%E4%B8%AD),就说明你看到的是中间态。修法是划清边界——编码只发生在值离开进程的那一刻,中间层不解码,服务端入口解码一次之后全程保持明文。用 URL 解析工具把整条 URL 拆开,逐个参数看服务端实际收到什么。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件