欢迎访问晨星博客!
处理 JSON 请求体时,稳定性通常不是由某一行 json_decode() 决定的,而是取决于读取、限流、解码和字段校验是否形成完整链路。只要其中一个环节缺少边界,接口就可能出现字段类型错误、异常信息丢失,甚至因过大的请求体消耗过多内存。

php://input 与 $_POST 的区别$_POST 适合读取传统表单提交的数据,例如 application/x-www-form-urlencoded 或 multipart/form-data。当客户端发送的是 application/json 时,PHP 不会自动把请求体转换成 $_POST,这时应从 php://input 读取原始内容。
php://input 是一个只读输入流,能够访问当前请求的原始请求体。它适合处理 JSON、XML 或其他需要自行解析的格式。对于文件上传等 multipart/form-data 请求,则应使用 PHP 提供的表单字段和上传文件数据,而不是把 JSON 请求体的处理逻辑直接套过去。
读取请求体时,还应尽量只读取一次,并在读取后立即进行长度检查。反复读取或把原始字符串在多个变量之间复制,都会增加内存占用,尤其是请求体较大时。
请求体长度限制应该发生在 JSON 解码之前。因为 json_decode() 需要先拿到字符串,过大的输入已经可能在解码前占用大量内存。
下面的函数将读取、长度限制和空请求判断放在一起。$maxBytes 和 $maxDepth 由具体接口根据业务需求配置,不应该在所有接口中使用同一组固定值。
<?php
function readJsonRequestBody(int $maxBytes): array
{
if ($maxBytes <= 0) {
return [
'ok' => false,
'status' => 500,
'error' => '服务端未配置有效的请求体大小限制',
];
}
$rawBody = file_get_contents('php://input');
if ($rawBody === false) {
return [
'ok' => false,
'status' => 400,
'error' => '无法读取请求体',
];
}
if ($rawBody === '') {
return [
'ok' => false,
'status' => 400,
'error' => '请求体不能为空',
];
}
if (strlen($rawBody) > $maxBytes) {
return [
'ok' => false,
'status' => 413,
'error' => '请求体超过允许的大小',
];
}
return [
'ok' => true,
'raw' => $rawBody,
];
}
这里使用 strlen() 判断字节长度,而不是按字符数判断。请求体限制针对的是传输和内存占用,字节数更符合这个目的。
如果接口运行在 WordPress 插件或自定义接口中,应用层限制并不能替代 Web 服务器、PHP 运行环境或反向代理层的限制。应用层检查仍然有必要,因为它能让接口返回统一的错误结构,并避免已经进入 PHP 的内容继续进入解码流程。
直接判断 json_decode() 的返回值并不可靠。合法 JSON 可能解码为 false、0、空字符串或 null,这些值与解析失败不能简单混为一谈。
更清晰的方式是使用 JSON_THROW_ON_ERROR,让解析失败通过 JsonException 暴露出来。同时设置合理的最大嵌套深度,避免异常深的 JSON 结构带来额外解析开销。
<?php
function parseJsonRequestBody(int $maxBytes, int $maxDepth): array
{
$bodyResult = readJsonRequestBody($maxBytes);
if (!$bodyResult['ok']) {
return $bodyResult;
}
if ($maxDepth <= 0) {
return [
'ok' => false,
'status' => 500,
'error' => '服务端未配置有效的 JSON 嵌套深度限制',
];
}
try {
$data = json_decode(
$bodyResult['raw'],
true,
$maxDepth,
JSON_THROW_ON_ERROR
);
} catch (JsonException $exception) {
return [
'ok' => false,
'status' => 400,
'error' => '请求体不是有效的 JSON',
];
}
if (!is_array($data)) {
return [
'ok' => false,
'status' => 400,
'error' => 'JSON 根节点必须是对象',
];
}
return [
'ok' => true,
'data' => $data,
];
}
示例中没有把异常详情直接返回给客户端。异常信息可以写入服务端日志,但对外只返回稳定、简洁的错误描述,避免暴露内部实现细节。
json_decode() 的深度参数不是装饰项。嵌套结构越深,解析过程需要处理的层级越多。它应该结合接口数据模型设置,而不是为了“兼容所有数据”盲目放大。接口只接受简单对象时,较小且明确的深度限制更容易发现异常输入。
JSON 解码成功,只能说明文本符合 JSON 格式,不能说明业务数据合法。例如,user_id 可能被传成字符串,enabled 可能被传成数组,额外字段也可能被接口无意间保存或继续传递。
字段校验应采用白名单思路:先明确接口允许哪些字段,再逐个检查字段是否存在、类型是否正确以及值是否满足业务要求。
<?php
function validateProfilePayload(array $data): array
{
$allowedFields = [
'name',
'email',
'enabled',
];
foreach ($data as $field => $value) {
if (!in_array($field, $allowedFields, true)) {
return [
'ok' => false,
'status' => 400,
'error' => '请求包含不允许的字段',
'field' => $field,
];
}
}
if (!array_key_exists('name', $data) || !is_string($data['name'])) {
return [
'ok' => false,
'status' => 400,
'error' => 'name 必须是字符串',
];
}
if (!array_key_exists('email', $data) || !is_string($data['email'])) {
return [
'ok' => false,
'status' => 400,
'error' => 'email 必须是字符串',
];
}
if (array_key_exists('enabled', $data) && !is_bool($data['enabled'])) {
return [
'ok' => false,
'status' => 400,
'error' => 'enabled 必须是布尔值',
];
}
$name = trim($data['name']);
$email = trim($data['email']);
if ($name === '' || $email === '') {
return [
'ok' => false,
'status' => 400,
'error' => 'name 和 email 不能为空',
];
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return [
'ok' => false,
'status' => 400,
'error' => 'email 格式不正确',
];
}
return [
'ok' => true,
'data' => [
'name' => $name,
'email' => $email,
'enabled' => $data['enabled'] ?? false,
],
];
}
array_key_exists() 和 isset() 的用途不同。前者可以判断字段是否存在,即使字段值是 null;后者在值为 null 时也会返回 false。如果接口需要区分“字段不存在”和“字段存在但值为空”,应明确使用 array_key_exists()。
字段白名单还有一个实际作用:后续写入数据库、更新 WordPress 对象或传递给其他服务时,只使用已经筛选过的数据,而不是直接把整个请求数组交给业务层。这样可以减少未预期字段影响业务逻辑的机会。
读取、解码和业务校验可以保持职责分离,接口入口只负责串联它们。这样 JSON 解析函数能够被多个接口复用,而字段规则仍然属于具体业务。
<?php
$result = parseJsonRequestBody(
maxBytes: $configuredMaxBytes,
maxDepth: $configuredJsonDepth
);
if (!$result['ok']) {
http_response_code($result['status']);
echo json_encode([
'success' => false,
'error' => $result['error'],
], JSON_UNESCAPED_UNICODE);
exit;
}
$validation = validateProfilePayload($result['data']);
if (!$validation['ok']) {
http_response_code($validation['status']);
echo json_encode([
'success' => false,
'error' => $validation['error'],
], JSON_UNESCAPED_UNICODE);
exit;
}
$payload = $validation['data'];
// 只使用 $payload 进入后续业务逻辑。
echo json_encode([
'success' => true,
'data' => $payload,
], JSON_UNESCAPED_UNICODE);
如果项目使用的 PHP 版本支持命名参数,这种调用方式可以让配置含义更清楚;如果项目需要兼容不支持命名参数的环境,则改为普通位置参数即可。关键不在调用语法,而在于两个限制值必须由接口配置明确传入。
在 WordPress 插件或自定义 REST 接口中,错误响应还应与项目现有的响应机制保持一致。无论使用通用 PHP 输出,还是使用 WordPress 的响应对象,都应做到状态、错误信息和数据结构稳定,避免同一个接口在不同失败路径返回完全不同的格式。
请求体会先以原始字符串形式存在,解码后又会生成 PHP 数组或其他值。随后如果业务层继续复制、过滤或序列化数据,内存压力还会增加。因此,限制原始请求体大小只是第一步,以下做法同样重要:
php://input。JSON 解析本身无法替代业务校验,也不能替代认证、授权和请求频率控制。它解决的是“如何安全地把请求体转换成可处理的数据”这一层问题;调用者是否有权限修改目标资源,则应在更靠后的业务层单独判断。
一套可复用的请求体解析流程,可以概括为:从 php://input 读取原始内容,先做字节长度限制,再使用带深度限制的 json_decode() 解码并捕获错误,最后依据白名单校验字段和类型。只要每个阶段都明确失败结果,接口就不会把“格式正确”误当成“业务合法”,也更容易在请求变大或输入异常时保持稳定。
声明:原创文章请勿转载,如需转载请注明出处!