什么是 URL 编码?
URL 编码(又称百分号编码,Percent-Encoding)是将 URL 中不允许直接出现的字符转换为 % 后跟两位十六进制数的过程。它依据 RFC 3986 标准,确保 URL 在不同系统、不同网络协议间安全传输。
URL 只允许使用 ASCII 字符集的一个子集:
- 未保留字符:
A-Z a-z 0-9 - _ . ~ - 保留字符:
: / ? # [ ] @ ! $ & ' ( ) * + , ; = - 其他所有字符(包括中文、空格、特殊符号)都必须进行编码
保留字符与未保留字符
未保留字符(Unreserved Characters)
这些字符在 URL 中没有特殊含义,可以直接使用,无需编码:
| 字符 | 说明 |
|---|---|
A-Z a-z 0-9 |
字母和数字 |
- |
连字符 |
_ |
下划线 |
. |
点号 |
~ |
波浪号 |
保留字符(Reserved Characters)
保留字符在 URL 中有特定的语法作用,分两类:
通用分隔符:: / ? # [ ] @
子分隔符:! $ & ' ( ) * + , ; =
保留字符是否需要编码,取决于它在 URL 中的位置。例如 / 在路径中表示层级分隔,但如果它作为查询参数值的一部分,就需要编码为 %2F。
查询字符串编码
查询字符串(Query String)是 URL 中 ? 之后的部分,由 & 分隔的键值对组成。编码规则如下:
编码规则
- 空格编码为
+(application/x-www-form-urlencoded)或%20(标准百分号编码) &、=、+作为参数值时必须编码为%26、%3D、%2B- 中文等非 ASCII 字符先转换为 UTF-8 字节序列,再逐字节编码
构建示例
假设要传递参数 name=张三 & 李四:
?name=%E5%BC%A0%E4%B8%89+%26+%E6%9D%8E%E5%9B%9B
各语言构建方式
JavaScript:
const params = new URLSearchParams({ name: '张三 & 李四' });
const url = `/api?${params.toString()}`;
// /api?name=%E5%BC%A0%E4%B8%89+%26+%E6%9D%8E%E5%9B%9B
Python:
from urllib.parse import urlencode
query = urlencode({'name': '张三 & 李四'})
# name=%E5%BC%A0%E4%B8%89+%26+%E6%9D%8E%E5%9B%9B
Go:
import "net/url"
v := url.Values{}
v.Set("name", "张三 & 李四")
query := v.Encode()
永远不要手动拼接查询字符串,始终使用标准库的编码函数。
国际化 URL
非 ASCII 字符编码
URL 标准要求仅使用 ASCII 字符。对于中文、日文、韩文、emoji 等,处理方式为:
- 将字符转换为 UTF-8 字节序列
- 对每个字节执行百分号编码
例如 中文 的 UTF-8 字节为 E4 B8 AD E6 96 87,编码后为 %E4%B8%AD%E6%96%87。
Punycode 与国际化域名(IDN)
域名部分使用 Punycode(RFC 3492)进行编码。例如 中文.com 会被转换为 xn--fiq228c.com。现代浏览器会在地址栏显示原始字符,但实际请求使用 Punycode。
路径中的 Unicode
路径部分使用百分号编码。现代浏览器和 HTTP 客户端会自动处理,但在服务端日志中你看到的是编码后的形式。
常见编码 Bug
1. 重复编码
对已编码的字符串再次编码会导致 % 变成 %25:
原始: 张三
一次编码:%E5%BC%A0%E4%B8%89
二次编码:%25E5%25BC%25A0%25E4%25B8%2589 ❌
原因:框架自动编码后,又手动调用了一次 encodeURIComponent。
2. 编解码不一致
前端使用 encodeURI,后端使用 URLDecoder.decode 且字符集不是 UTF-8,导致乱码。
3. 空格处理差异
encodeURIComponent(' ')->%20application/x-www-form-urlencoded->+
在拼接 URL 路径时,空格应为 %20;在表单提交的查询字符串中,空格为 +。混用会导致服务端解析错误。
4. 保留字符未编码
直接将用户输入拼入 URL 路径,如果输入包含 ?、#、/,会破坏 URL 结构,甚至引发开放重定向漏洞。
5. Hash 片段不发送到服务端
URL # 之后的部分(Fragment)不会被发送到服务端。将敏感参数放在 hash 中是一种安全误区。
encodeURI vs encodeURIComponent
| 函数 | 用途 | 不编码的字符 |
|---|---|---|
encodeURI |
编码完整 URL | A-Za-z0-9;-._~:/?#[]@!$&'()*+,;= |
encodeURIComponent |
编码 URL 组件(参数值) | A-Za-z0-9;-._!~'()* |
// 编码完整 URL
encodeURI('https://example.com/路径?name=张三')
// https://example.com/%E8%B7%AF%E5%BE%84?name=%E5%BC%A0%E4%B8%89
// 编码参数值
encodeURIComponent('name=张三&p=1')
// name%3D%E5%BC%A0%E4%B8%89%26p%3D1
最佳实践
- 始终使用标准库:
URLSearchParams、url.parse、urllib.parse等 - 区分编码场景:路径用
encodeURI,参数值用encodeURIComponent - 统一 UTF-8 编码:前后端约定使用 UTF-8
- 不要编码后再编码:检查框架是否已自动编码
- 验证用户输入:防止保留字符注入
- 使用 HTTPS:编码后的 URL 在传输层同样需要加密保护