如何在 REST API 中正确编码查询参数
根据 RFC 3986,REST API 的查询参数必须进行百分号编码,才能安全地包含 &、=、空格和 Unicode 等特殊字符。在 JavaScript 中使用 encodeURIComponent(),在 Python 中使用 urllib.parse.quote(),在 Java 中使用 URLEncoder.encode()。
为什么查询参数需要编码
REST API 中的查询字符串采用一种结构化格式,键值对之间用 & 分隔,键和值之间用 = 分隔。如果某个参数值包含了这些分隔符字符,或者其他保留字符(如 #、+ 或空格),查询字符串的结构就会被破坏,服务器接收到的数据也会出错。
来看这样一个例子:你想在某个 API 中搜索 "salt & pepper"。如果不做编码,URL /search?q=salt & pepper 会让服务器认为存在两个参数:q=salt 和 pepper(没有值)。而经过正确编码后,/search?q=salt%20%26%20pepper 就能正确地发送一个名为 q、值为 "salt & pepper" 的参数。
除了保证正确性之外,正确的编码还能防范安全漏洞。URL 中未经编码的用户输入可能导致注入攻击、缓存投毒以及其他各种攻击。在将参数值放入 URL 之前,务必先对其进行编码。
不同语言中的编码方式
每种主流编程语言都提供了用于 URL 编码的内置函数。下面介绍如何在最流行的几种语言中正确编码查询参数。
// JavaScript
// 对单个参数值使用 encodeURIComponent()
const query = 'price >= 100 & category = books';
const url = 'https://api.example.com/search?q=' + encodeURIComponent(query);
// "https://api.example.com/search?q=price%20%3E%3D%20100%20%26%20category%20%3D%20books"
// 或者对多个参数使用 URLSearchParams
const params = new URLSearchParams({
q: 'price >= 100 & category = books',
page: '1',
sort: 'price'
});
const url2 = 'https://api.example.com/search?' + params.toString();
# Python
from urllib.parse import urlencode, quote
# 对多个参数使用 urlencode
params = {
'q': 'price >= 100 & category = books',
'page': 1,
'sort': 'price'
}
query_string = urlencode(params)
url = f'https://api.example.com/search?{query_string}'
# 对单个值使用 quote
value = quote('price >= 100 & category = books', safe='')
url = f'https://api.example.com/search?q={value}'
// Java
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String query = "price >= 100 & category = books";
String encoded = URLEncoder.encode(query, StandardCharsets.UTF_8);
// "price+%3E%3D+100+%26+category+%3D+books"
// 注意:URLEncoder 使用 + 表示空格(表单编码)
// 若需符合 RFC 3986 的编码,将 + 替换为 %20
String rfc3986 = encoded.replace("+", "%20");
String url = "https://api.example.com/search?q=" + rfc3986;
// C#
using System.Net;
using System.Web;
string query = "price >= 100 & category = books";
// Uri.EscapeDataString 遵循 RFC 3986
string encoded = Uri.EscapeDataString(query);
// "price%20%3E%3D%20100%20%26%20category%20%3D%20books"
// HttpUtility.UrlEncode 使用表单编码(+ 表示空格)
string formEncoded = HttpUtility.UrlEncode(query);
// "price+%3e%3d+100+%26+category+%3d+books"
// Go
package main
import (
"fmt"
"net/url"
)
func main() {
// url.QueryEscape 用于编码查询字符串(+ 表示空格)
encoded := url.QueryEscape("price >= 100 & category = books")
// "price+%3E%3D+100+%26+category+%3D+books"
// url.PathEscape 用于编码路径片段(%20 表示空格)
pathEncoded := url.PathEscape("my file name.pdf")
// "my%20file%20name.pdf"
// 使用 url.Values 构建查询字符串
params := url.Values{}
params.Set("q", "price >= 100 & category = books")
params.Set("page", "1")
queryString := params.Encode()
fmt.Println(queryString)
}
编码数组和嵌套对象
REST API 常常需要接受数组或嵌套对象作为查询参数。对于这些复杂类型的编码并没有统一的标准,因此不同的 API 会采用不同的约定。
// 重复键(支持最广泛)
// GET /api/items?tag=javascript&tag=python&tag=go
const params = new URLSearchParams();
['javascript', 'python', 'go'].forEach(tag => params.append('tag', tag));
// 方括号表示法(常见于 PHP、Rails 以及搭配 qs 的 Express)
// GET /api/items?tags[]=javascript&tags[]=python&tags[]=go
// 必须手动编码:
const tags = ['javascript', 'python', 'go'];
const qs = tags.map(t => 'tags[]=' + encodeURIComponent(t)).join('&');
// 逗号分隔(简单但有局限)
// GET /api/items?tags=javascript,python,go
const tagList = encodeURIComponent('javascript,python,go');
// 嵌套对象(方括号表示法)
// GET /api/search?filter[status]=active&filter[min_price]=10
// 在 Python 中:
params = {
'filter[status]': 'active',
'filter[min_price]': '10'
}
请务必查阅你的 API 文档,确认它期望的格式。在设计新的 API 时,重复键的方式在各种编程语言和 HTTP 库之间的可移植性最好。
服务端解码的最佳实践
在服务端处理传入请求时,大多数 Web 框架会自动帮你解码查询参数。不过,要实现健壮的服务端处理,仍有一些重要事项需要牢记。
- 大多数框架(Express、Django、Spring、ASP.NET)会自动解码查询参数。不要再手动解码一次,否则会导致重复解码。
- 在使用解码后的值之前,先根据预期的类型和取值范围对其进行校验。
- 在 Web 服务器层面设置 URL 的最大长度限制(通常为 2048 或 8192 个字符)。
- 妥善处理
+既可能表示空格、也可能表示字面加号的情况,这取决于内容类型。 - 记录原始(编码后)的 URL 以便调试,因为解码后的值在日志中可能具有误导性。
// Express.js —— 参数会被自动解码
app.get('/search', (req, res) => {
const query = req.query.q; // 已经解码
// 不要这样做:decodeURIComponent(req.query.q)
// 校验输入
if (typeof query !== 'string' || query.length > 200) {
return res.status(400).json({ error: 'Invalid query parameter' });
}
// 安全地使用解码后的值
const results = search(query);
res.json(results);
});
安全注意事项
正确的查询参数编码是 API 安全的关键环节。以下是需要遵循的几条最重要的安全实践。
- 绝不要信任客户端的编码。 即使客户端已经正确编码,也始终要在服务端对解码后的参数进行校验和净化。
- 警惕双重编码攻击。 攻击者可能对恶意字符进行两次编码,以绕过只解码一次的输入过滤器。要确保你的安全过滤器看到的是完全解码后的值。
- 防范路径遍历。 路径参数中类似
%2e%2e%2f(../)的编码序列可能被用来访问目标目录之外的文件。在进行文件操作之前,先解码并校验路径。 - 使用参数化查询。 即便完成了 URL 解码,也绝不要把用户输入直接拼接进 SQL 或 NoSQL 查询中。请使用参数化查询或 ORM。
- 限制 URL 长度。 过长的查询字符串可能导致拒绝服务。配置你的 Web 服务器,拒绝超出合理长度的 URL。
- 对输出同样要编码。 当把查询参数值回显到 HTML 响应中时,要应用 HTML 实体编码,以防范跨站脚本(XSS)攻击。