PHP项目怎么实现数据权限?

wen java案例 3

本文目录导读:

PHP项目怎么实现数据权限?

  1. 核心模型:用户 -> 角色/组织 -> 数据
  2. 方案一:硬编码 + 基于Session/Token的用户级别(最简单,适合小型项目)
  3. 方案二:基于RBAC + 数据权限表(主流方案,适合大多数中大型项目)
  4. 方案三:基于数据库视图(适合查询多,极少写的情况)
  5. 关键设计原则与坑点
  6. 总结推荐

在PHP项目中实现数据权限,核心思路是:在每次数据查询时,动态附加一个“权限过滤条件”,确保用户只能看到其有权访问的数据行。

这通常不是靠一个简单的函数能解决的,而需要一套架构设计,以下是几种主流、实用的实现方案,从简单到复杂。

核心模型:用户 -> 角色/组织 -> 数据

数据权限的粒度通常分为几个级别:

  1. 仅本人:只能看自己的订单、文章。
  2. 本部门:能看到同部门同事的数据。
  3. 本部门及下属部门:经理级别的权限。
  4. 全部:超级管理员。
  5. 自定义:通过角色关联特定数据范围(如指定几个区域的数据)。

硬编码 + 基于Session/Token的用户级别(最简单,适合小型项目)

直接在查询逻辑中,根据用户角色 if...else 拼接SQL。

<?php
class OrderService {
    public function getOrderList($userId, $userRole, $departmentId) {
        $sql = "SELECT * FROM orders WHERE 1=1"; // 基础查询
        // 第1步:根据角色确定数据范围级别
        switch ($userRole) {
            case 'super_admin': // 超级管理员:不加任何限制
                break;
            case 'manager': // 部门经理:查看本部门(包括自己)
                $sql .= " AND department_id = " . intval($departmentId);
                break;
            case 'staff': // 普通员工:仅看自己
                $sql .= " AND created_by = " . intval($userId);
                break;
            default:
                $sql .= " AND 1=0"; // 无权限,返回空
        }
        // 第2步:执行查询
        return $this->db->query($sql)->fetchAll();
    }
}
  • 优点:实现快,无外部依赖。
  • 缺点:硬编码多,难以维护,权限逻辑分散在各个Model/Service里,需要修改所有查询方法。

基于RBAC + 数据权限表(主流方案,适合大多数中大型项目)

在角色(Role)上增加一个 data_scope 字段,或在角色与组织之间建立关系。

数据库设计补充

-- 用户表
users (id, name, department_id, ...)
-- 角色表 (RBAC核心)
roles (id, name, data_scope) -- data_scope: 'self', 'department', 'department_and_child', 'all', 'custom'
-- 用户-角色关联表
user_roles (user_id, role_id)
-- 部门表 (树形结构)
departments (id, parent_id, name)
-- (可选) 自定义数据权限表,用于极端灵活的场景
-- role_data_permissions (role_id, table_name, field_name, value_set)
--  role_id=5 在 'orders' 表的 'region_id' 字段上可以看 [1,2,3]

核心数据权限过滤器类

创建一个服务类,专门用于拼接权限SQL片段。

<?php
class DataPermissionFilter {
    private $currentUser;
    private $db;
    public function __construct($user, $db) {
        $this->currentUser = $user;
        $this->db = $db;
    }
    /**
     * 生成数据权限过滤条件 (WHERE子句片段)
     *
     * @param string $tableAlias 主表别名,如 'o'
     * @param string $userIdField 用户ID字段,如 'created_by'
     * @param string $deptIdField  部门ID字段,如 'department_id'
     * @return string 返回如 " o.department_id IN (2,3) " 的SQL片段
     */
    public function getFilterCondition($tableAlias = '', $userIdField = 'created_by', $deptIdField = 'department_id') {
        $prefix = $tableAlias ? $tableAlias . '.' : '';
        $userId = (int)$this->currentUser['id'];
        $deptId = (int)$this->currentUser['department_id'];
        // 获取用户的所有角色,取最高权限(data_scope值最小的,或者按优先级排序)
        $roles = $this->getUserRoles($userId);
        $dataScope = $this->getHighestScope($roles); // 'self' < 'department' < 'all'
        switch ($dataScope) {
            case 'all':
                return ' 1=1 '; // 无限制
            case 'department_and_child':
                // 获取本部门及所有子部门ID
                $deptIds = $this->getDepartmentAndChildrenIds($deptId);
                return " {$prefix}{$deptIdField} IN (" . implode(',', $deptIds) . ") ";
            case 'department':
                return " {$prefix}{$deptIdField} = {$deptId} ";
            case 'self':
            default:
                return " {$prefix}{$userIdField} = {$userId} ";
        }
    }
    private function getHighestScope($roles) {
        // 定义一个优先级数组,数字越小权限越大
        $priority = ['self' => 3, 'department' => 2, 'department_and_child' => 1, 'all' => 0];
        $highest = 'self';
        foreach ($roles as $role) {
            if (isset($priority[$role['data_scope']]) && $priority[$role['data_scope']] < $priority[$highest]) {
                $highest = $role['data_scope'];
            }
        }
        return $highest;
    }
    private function getDepartmentAndChildrenIds($deptId) {
        // 查询所有子部门ID,缓存结果到静态变量或Redis中
        // 实现从 departments 表递归查询 parent_id = ? 的所有ID
        return [1, 2, 3]; // 示例返回
    }
}

在业务代码中使用(Service层)

<?php
class OrderService {
    private $db;
    public function getOrderList($userId) {
        // 1. 获取当前用户信息(从Session/Token解析)
        $currentUser = $this->getUserById($userId);
        // 2. 创建权限过滤器
        $permission = new DataPermissionFilter($currentUser, $this->db);
        $whereClause = $permission->getFilterCondition('o', 'o.created_by', 'o.department_id');
        // 3. 将权限条件拼接到你的业务查询中
        $sql = "SELECT o.*, u.name 
                FROM orders o 
                LEFT JOIN users u ON o.created_by = u.id 
                WHERE o.status = 'active' 
                AND " . $whereClause . " 
                ORDER BY o.created_at DESC 
                LIMIT 20";
        return $this->db->query($sql)->fetchAll();
    }
}

使用ORM框架时的进阶实现(Laravel ThinkPHP等)

如果使用Eloquent (Laravel) 或 ThinkPHP,可以通过全局作用域(Global Scope)实现自动过滤,更优雅。

// Laravel 示例:创建一个全局作用域
class DataPermissionScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $user = auth()->user();
        // ... 获取数据范围 ...
        $builder->whereIn('department_id', $deptIds);
    }
}
// 在 Model 的 booted 方法中注册
class Order extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new DataPermissionScope);
    }
}
// 后续所有 Order::query() 都会自动带上权限条件

基于数据库视图(适合查询多,极少写的情况)

在数据库层面直接创建视图,视图里已经带上了权限过滤逻辑。

  • 缺点:权限逻辑难以用代码管理,视图性能在复杂场景下可能下降,调试困难。

关键设计原则与坑点

  1. 统一入口:所有数据查询都通过同一个Service或Repository层,不要绕过权限过滤器(比如直接 Model::query())。
  2. 字段映射getFilterCondition 方法需要参数来指定用户ID字段和部门ID字段,不同表的字段名可能不同,可能是 user_id, creator_id, dept_id
  3. SQL注入:使用参数绑定()或严格 intval() 处理传入的ID,防止注入。
  4. 缓存部门树:部门和子部门的关系比较固定,可以缓存到Redis或静态变量中,避免每次查询都递归数据库。
  5. 不要忽视“无部门”用户:有些用户可能没有部门,需要特判,否则 department_id = 0 可能查不到数据。

总结推荐

  • 小型项目(<10人团队,需求简单):使用 方案一(硬编码),快速上线。
  • 中大型项目(需要权限灵活,可维护性强):使用 方案二(RBAC + 数据权限表 + 过滤器类),配合ORM的全局作用域实现自动注入。
  • 极端复杂(如SaaS多租户,每个角色对每张表的每行有不同规则):可以研究 ABAC(基于属性的访问控制),或者引入成熟的权限框架(如Casbin的PHP版本),但这会显著增加复杂度。

推荐从方案二入手设计,这是最平衡、最标准的做法。

抱歉,评论功能暂时关闭!