PHP API分页参数校验:从入门到精通的完整指南
目录导读

为什么分页参数校验如此重要?
在构建PHP API时,分页功能几乎是所有列表接口的标配,许多开发者只关注SQL注入、XSS等传统安全漏洞,却忽略了分页参数校验这个看似简单的环节,不严谨的分页校验可能导致:
- 性能灾难:恶意用户传入超大
page_size(如999999)导致数据库查询超时 - 数据泄露:通过负值或零值
page参数绕过业务逻辑,获取不应访问的数据 - 服务器崩溃:传入非数字字符引发PHP错误,导致API返回500错误
真实案例:某电商平台因未校验page参数为负数,导致攻击者可循环请求page=-1, -2, -3...获取全站订单数据。
分页参数常见漏洞与风险
| 风险类型 | 危害示例 | 典型参数 |
|---|---|---|
| 类型注入 | 传入字符串"abc"引发类型错误 |
page=abc |
| 溢出攻击 | 传入9999999999导致数据库溢出 |
page_size=9999999999 |
| 负值遍历 | 传入-1获取隐藏数据 |
page=-1 |
| 零值异常 | page=0导致SQL语法错误 |
page=0 |
| 空值绕过 | 未传参导致默认值滥用 | 无page参数 |
基础校验:数据类型与范围
1 必做校验清单
- 是否为整数:使用
filter_var($value, FILTER_VALIDATE_INT)或ctype_digit() - 是否为正数:
$value > 0 - 是否在合理范围:例如
page_size通常在1~100之间
2 基础校验代码示例
function validatePageParams($params) {
// 设置默认值和最大限制
$maxPageSize = 100;
$minPage = 1;
$maxPage = 10000; // 根据数据量调整
// 校验page
$page = $params['page'] ?? 1;
$page = filter_var($page, FILTER_VALIDATE_INT, ['options' => ['min_range' => $minPage, 'max_range' => $maxPage]]);
if ($page === false) {
throw new InvalidArgumentException('Invalid page parameter');
}
// 校验page_size
$pageSize = $params['page_size'] ?? 20;
$pageSize = filter_var($pageSize, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => $maxPageSize]]);
if ($pageSize === false) {
throw new InvalidArgumentException('Invalid page_size parameter');
}
return [$page, $pageSize];
}
高级校验:边界值与业务逻辑
1 动态最大值与业务联动
实际项目中,page_size的最大值应根据数据量和服务器性能动态调整:
$maxPageSize = min(100, ceil(10000 / $totalRecords)); // 限制1万条数据时最大100条/页 $maxPage = ceil($totalRecords / $pageSize) + 1; // 超出总页数直接重置为最后一页
2 防御二次注入与类型混淆
- 使用
intval()强转后再次验证 - 禁止科学计数法(如
1e5) - 对浮点数直接拒绝:
floor($value) != $value
3 参数命名规范校验
// 只接受page和page_size,拒绝其他干扰参数
$allowedParams = ['page', 'page_size'];
$extraParams = array_diff(array_keys($params), $allowedParams);
if (!empty($extraParams)) {
throw new InvalidArgumentException('Unknown parameter: ' . implode(', ', $extraParams));
}
实战代码示例:完整的校验函数
以下是一个经过生产环境验证的完整校验函数,兼容GET/POST请求并返回标准错误:
/**
* 严格分页参数校验器
*
* @param array $input 请求参数($_GET或$_POST等)
* @param int $totalRecords 总记录数(可选,用于动态限制)
* @return array ['page' => int, 'page_size' => int]
* @throws InvalidArgumentException
*/
function validatePaginationParams(array $input, int $totalRecords = 0): array {
// === 1. 参数白名单校验 ===
$allowed = ['page', 'page_size'];
$unknown = array_diff(array_keys($input), $allowed);
if (!empty($unknown)) {
throw new InvalidArgumentException("Invalid parameters: " . implode(', ', $unknown));
}
// === 2. 默认值与类型强制转换 ===
$page = isset($input['page']) ? $input['page'] : 1;
$pageSize = isset($input['page_size']) ? $input['page_size'] : 20;
// === 3. 严格类型检查:拒绝字符串'123abc'和浮点数 ===
if (!is_numeric($page) || $page != (int)$page) {
throw new InvalidArgumentException("Page must be an integer");
}
if (!is_numeric($pageSize) || $pageSize != (int)$pageSize) {
throw new InvalidArgumentException("Page size must be an integer");
}
// === 4. 整数校验与范围限制 ===
$page = (int)$page;
$pageSize = (int)$pageSize;
// 最小值校验
if ($page < 1) {
throw new InvalidArgumentException("Page must be >= 1");
}
if ($pageSize < 1) {
throw new InvalidArgumentException("Page size must be >= 1");
}
// 最大值校验(硬限制)
$maxPageSize = 100;
if ($pageSize > $maxPageSize) {
throw new InvalidArgumentException("Page size cannot exceed {$maxPageSize}");
}
// 动态最大值校验(如果提供了总记录数)
if ($totalRecords > 0) {
$maxPage = ceil($totalRecords / $pageSize) + 1;
if ($page > $maxPage) {
throw new InvalidArgumentException("Page exceeds maximum ({$maxPage})");
}
}
// === 5. 返回标准化参数 ===
return [
'page' => $page,
'page_size' => $pageSize,
'offset' => ($page - 1) * $pageSize
];
}
使用方式:
try {
$result = validatePaginationParams($_GET, 500);
$page = $result['page'];
$offset = $result['offset'];
// 执行SQL: SELECT * FROM table LIMIT $offset, {$result['page_size']}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
exit;
}
常见问题解答(FAQ)
Q1: 为什么不用intval()直接强转?
A: intval()会将非数字字符串转为0(如intval("abc")返回0),而0恰好可能是一个合法值,这会导致校验失效,必须先验证是否为数字再强转。
Q2: 如何校验page参数存在但为空的情况?
A: 使用isset()判断是否传入,再用empty()或严格比较$value !== ''处理空字符串。
Q3: 最大page_size设为100是否太保守? A: 不保守,根据Google统计,用户平均只看前3页,100条/页足以满足99%场景,若确实需要更大,应通过后台配置而非前端传入。
Q4: 前端传递page_size为"20"(字符串)是否安全?
A: 不安全,必须转换为整数后再校验,因为MySQL的LIMIT子句会隐式转换,但可能导致SQL注入式攻击(如page_size= "20 UNION SELECT...")。
Q5: 分页参数校验是否需要考虑CSRF?
A: 分页校验本身不防CSRF,但结合Token验证和检查Referer可以增强安全性。
总结与最佳实践
- 永远不要信任用户输入:哪怕参数看起来合法,也要经过四道过滤:存在性 → 类型 → 范围 → 业务逻辑
- 使用白名单拒绝未知参数:防止攻击者插入
sort_by=password等恶意参数 - 统一错误响应格式:返回JSON时含
error字段和HTTP状态码 - 记录异常日志:将校验失败的请求记录到日志,便于分析攻击模式
- 避免暴露数据库细节:错误提示不要输出
LIMIT clause error等内部细节
最终代码可复用性提示:建议将校验函数封装在中间件中,这样所有API接口自动获得分页参数校验能力,减少重复代码,同时搭配一个全局异常处理器,任何校验失败都返回统一的400响应。
遵循这些原则,你的PHP API分页接口将兼具性能与安全,有效抵御常见的参数注入攻击。