分页查询前必须完成数据权限校验,且需在分页逻辑执行前基于可信后端上下文对每条记录校验归属;权限条件须同时应用于count和limit查询,分页参数也需严格边界控制。

分页查询前必须校验用户数据权限
直接在分页 SQL 里加 WHERE user_id = ? 或 tenant_id = ? 不够——攻击者可能篡改 page、limit 或 ID 参数,绕过前端限制拿到他人数据。权限校验必须在分页逻辑执行前完成,且要覆盖「当前页能查哪些记录」这个范围。
常见错误是只校验「用户能否访问该列表页」,却没校验「第 3 页的每条记录是否都属于该用户」。尤其当使用 OFFSET + LIMIT 分页时,若权限过滤放在分页之后(比如先取 100 条再 array_filter),就存在越权风险。
- 权限判断必须基于后端可信上下文(如已解密的 JWT payload、session 中的
user_role和org_id) - 避免把权限字段(如
created_by)从 URL 或 POST body 直接拼进 SQL —— 必须通过关联查询或预查表确认归属关系 - 对多租户场景,
tenant_id应作为所有分页查询的强制 WHERE 条件,且不能被请求参数覆盖
用 Laravel 的 Eloquent 做带权限的分页最稳妥
Laravel 的 paginate() 默认不自动注入权限条件,但你可以用全局作用域(Global Scope)或查询构造器链式调用确保每次分页都带上校验逻辑。
例如,给 Post 模型添加租户隔离:
立即学习“PHP免费学习笔记(深入)”;
class Post extends Model
{
protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $builder) {
$tenantId = auth()->check() ? auth()->user()->tenant_id : null;
if ($tenantId) {
$builder->where('tenant_id', $tenantId);
}
});
}
}这样所有 Post::paginate() 都自动加上 WHERE tenant_id = ?,包括后台管理页——前提是管理员也走同一套模型逻辑;否则需额外判断角色并跳过该作用域。
- 不要在控制器里手动拼
->where('user_id', auth()->id()),容易漏写或写错位置 - 全局作用域无法动态关闭,测试或 CLI 场景需用
withoutGlobalScopes() - 如果权限规则复杂(如「部门可见 + 标签白名单」),建议封装成自定义查询方法,如
Post::forCurrentUser()->paginate()
原生 PDO 分页必须手写 COUNT + LIMIT 查询,且两次都要加权限条件
很多人只在 SELECT ... LIMIT 查询里加权限,却忘了 COUNT(*) 也要一模一样的 WHERE 条件,否则总页数算错,导致最后一页空白或报错。
正确写法是复用同一个 $stmt 的条件构建逻辑:
$baseSql = "SELECT * FROM orders WHERE status != 'deleted'";
$baseCountSql = "SELECT COUNT(*) FROM orders WHERE status != 'deleted'";
<p>// 统一追加权限条件
if ($user->isTenantUser()) {
$baseSql .= " AND tenant_id = ?";
$baseCountSql .= " AND tenant_id = ?";
$params[] = $user->tenant_id;
}</p><p>$total = $pdo->prepare($baseCountSql)->execute($params)->fetchColumn();
$items = $pdo->prepare($baseSql . " ORDER BY id DESC LIMIT ? OFFSET ?")
->execute([...$params, $limit, $offset])
->fetchAll();- 切忌用
SQL_CALC_FOUND_ROWS—— MySQL 8.0+ 已弃用,且无法保证 FOUND_ROWS() 返回的是加了权限条件后的总数 - 如果权限依赖 JOIN(如查「我审批过的报销单」),COUNT 查询必须包含相同 JOIN 和 ON 条件,否则行数不一致
- 参数绑定顺序必须严格对应,
OFFSET和LIMIT要放在最后,且不能参与权限条件绑定
分页参数本身也要做权限边界检查
page=999999 或 per_page=1000 看似只是性能问题,实则可能暴露数据量、触发越权扫描。必须限制可访问的页码范围和单页最大条数。
- 硬性限制
per_page≤ 100,超出则截断为 100,避免 OOM 或慢查询 - 根据总数据量动态计算最大合法
page:若total = 256,per_page = 20,则最大page = ceil(256 / 20) = 13,传入page=14应返回空数组而非报错 - 对敏感模块(如财务流水),禁止跳转到非连续页码,只允许「上一页/下一页」链接,且链接中的
page必须经后端重新计算生成,不可回传原始参数
最易被忽略的一点:权限校验和分页参数校验必须在同一个事务或同一轮请求生命周期内完成,不能一个查 session,一个查缓存,导致状态不一致。











