仅转整型不安全,因无法拦截超范围页码或高频翻页;需结合最大页码校验、翻页频率控制(如Session记录时间与页码)及总条数估算。

为什么 $_GET['page'] 直接转整型还不够安全
很多人以为只要用 (int)$_GET['page'] 或 filter_var($_GET['page'], FILTER_SANITIZE_NUMBER_INT) 就能防暴力翻页,其实不然。攻击者可以构造极大值(比如 page=999999999),绕过前端页码限制,直接触发全表扫描或内存溢出——尤其当分页 SQL 写成 LIMIT 0,20 却没校验总页数时,LIMIT 999999999,20 会让 MySQL 扫描几乎全表再丢弃结果。
真正要拦的不是“非法字符”,而是“超出业务合理范围的请求”:
- 单次请求
page值超过最大允许页码(比如总数据才 200 条,每页 10 条,最多只该有 20 页) - 同一 IP/用户在短时间(如 60 秒)内请求页码跨度 > 5 页(比如从 page=1 突然跳到 page=100)
- 连续请求非递增页码(如 1→3→1→5→100),疑似脚本遍历
用 $_SESSION + 时间戳做轻量级翻页频率控制
不需要引入 Redis 或数据库,纯 PHP Session 就能挡住大部分低频暴力翻页。关键是记录「上一次合法翻页动作」的时间和页码,而不是只记次数。
示例逻辑:
立即学习“PHP免费学习笔记(深入)”;
// 启动 session(确保已在页首调用 session_start()) $now = time(); $last_page_req = $_SESSION['last_page_req'] ?? ['time' => 0, 'page' => 1]; $interval = $now - $last_page_req['time'];// 10 秒内翻页跨度超过 3 页,拒绝 if ($interval < 10 && abs((int)$_GET['page'] - $last_page_req['page']) > 3) { http_response_code(429); die('Too many page jumps'); }
// 更新记录 $_SESSION['last_page_req'] = [ 'time' => $now, 'page' => (int)$_GET['page'] ];
注意:$_SESSION 默认基于 Cookie,若用户禁用 Cookie,需 fallback 到 URL 参数 + IP 组合(但 IP 可伪造,仅作辅助)。
SELECT COUNT(*) 不是必须的,但必须有总条数上限
很多人为了省一次 COUNT(*) 查询,直接用 LIMIT $offset, $limit 加空结果判断是否到末页。这会导致无法预知最大页码,也就没法校验 page 是否越界。
更务实的做法:
- 缓存总条数(比如用
apcu_store('total_users', 12345, 300)),5 分钟更新一次 - 用覆盖索引快速估算:如
SELECT MAX(id) FROM users(假设 id 连续且自增) - 硬性设上限:不管真实多少条,
max_page = 1000,超出则 404 或重定向到第 1000 页
校验代码示例:
$page = max(1, (int)$_GET['page']);
$max_page = 1000; // 或从缓存读取
if ($page > $max_page) {
header('Location: /list?page=' . $max_page);
exit;
}
$offset = ($page - 1) * $per_page;
前端隐藏页码链接 ≠ 安全,后端必须重复校验
前端用 JS 禁用「下一页」按钮、或只渲染前 5 页链接,对爬虫和 curl 请求完全无效。所有分页参数最终都落到 $_GET,而它可被任意篡改。
所以必须在每次分页逻辑开头就完成三件事:
- 强制转换
page为正整数(max(1, (int)$_GET['page'])) - 对比最大允许页码(来自缓存/配置/估算)
- 检查本次请求是否符合频率策略(Session/IP/Token 三选一,至少一种)
最容易被忽略的是:**频率控制必须和分页逻辑耦合在同一请求生命周期里**。如果把限流逻辑放在中间件但没共享 $_SESSION 上下文,或者用了异步日志记录却没阻塞响应,那等于没做。











