本文目录导读:

- 方案一:基于策略模式(Policy) + 拦截器(最推荐项目实践)
- 方案二:基于属性注解 / Attribute(PHP 8+)
- 方案三:基于规则引擎 / 表达式语言(适度复杂度)
- 方案四:前端配合后端脱敏(最实用的工程方案)
- 最佳实践建议(根据项目复杂度选择)
- 特别注意点
在 PHP 项目中实现细粒度权限控制(通常称为属性级权限或字段级权限,区别于粗粒度的“增删改查”),核心思路是将权限判断下放到数据的具体字段或属性层面,而不是仅仅停留在控制器或路由层面。
由于 PHP 本身没有内置的权限框架,通常需要结合设计模式与中间件来实现,以下是几种从浅入深、从简单到复杂的实现方案。
基于策略模式(Policy) + 拦截器(最推荐项目实践)
这种方案将权限判断逻辑封装到独立的“策略类”中,通过一个统一的授权服务(如 Laravel 的 Gate)或自定义拦截器来执行。
实现步骤:
-
定义权限点:在数据库中配置细粒度权限,通常包含:
- 资源(如
Order) - 操作(如
view,edit) - 字段(如
price,customer_phone) - 条件(仅当订单状态为
pending且用户为创建者时,可编辑amount字段)
- 资源(如
-
创建策略类:为每个实体创建一个类,专门判断该实体上的权限。
class OrderPolicy { // 判断能否查看订单的 'price' 字段 public function viewFieldPrice(User $user, Order $order): bool { // 规则:订单创建者 或 拥有财务角色的用户 return $user->id === $order->user_id || $user->hasRole('finance'); } // 判断能否编辑订单的 'amount' 字段 public function editFieldAmount(User $user, Order $order): bool { // 规则:仅创建者 且 订单状态为待处理 return $user->id === $order->user_id && $order->status === 'pending'; } } -
编写授权中间件:在处理请求时,动态识别请求要操作的字段,并调用对应的策略方法。
// 中间件或服务层 class FieldAuthorizationMiddleware { public function handle($request, \Closure $next) { $resource = $request->route('order'); // 假设路由绑定了 Order 模型 $action = $request->input('_action'); // 前端传入操作类型 $fields = $request->input('_fields'); // 前端传入要操作的字段数组 $user = auth()->user(); $allowedFields = []; foreach ($fields as $field) { // 调用对应策略 $policy = new OrderPolicy(); if ($policy->{$action . 'Field' . studly_case($field)}($user, $resource)) { $allowedFields[] = $field; } } // 只保留允许的字段 $request->merge(['_allowed_fields' => $allowedFields]); return $next($request); } }
优点:代码清晰,业务逻辑高度内聚,便于单元测试。 缺点:需要为每个策略编写大量方法,适合结构化业务。
基于属性注解 / Attribute(PHP 8+)
PHP 8 引入了原生属性(Attributes),可以将权限规则直接声明在类或属性上,通过反射动态读取。
实现步骤:
-
定义权限声明 Attribute:
#[Attribute(\Attribute::TARGET_PROPERTY)] class FieldPermission { public function __construct( public string $action, // edit, view public string $expression // user.id == order.user_id OR user.role == 'admin' ) {} } -
在模型属性上使用:
class Order { #[FieldPermission(action: 'view', expression: 'user.id == order.user_id')] #[FieldPermission(action: 'edit', expression: 'user.role == \'finance\'')] public float $price; #[FieldPermission(action: 'view', expression: 'true')] public string $status; } -
运行时解析:
// 在输出或保存数据前 function getFilteredAttributes($object, User $user, string $action): array { $reflection = new \ReflectionObject($object); $allowed = []; foreach ($reflection->getProperties() as $property) { $attributes = $property->getAttributes(FieldPermission::class); foreach ($attributes as $attribute) { $perm = $attribute->newInstance(); if ($perm->action === $action) { // 解析表达式(这里可以用 symfony/expression-language 或简单的 eval,生产慎用 eval) $expression = str_replace(['user.', 'order.'], ['$user->', '$object->'], $perm->expression); if (eval("return {$expression};")) { $allowed[] = $property->getName(); break; // 有一个权限满足即可 } } } } return $allowed; }
优点:声明式编程,可读性好,与模型绑定紧密。 缺点:难以处理复杂业务逻辑;表达式解析有性能开销;动态 eval 有安全风险。
基于规则引擎 / 表达式语言(适度复杂度)
适合权限规则变动频繁、需要非技术人员在后台配置的系统。
核心流程:
-
数据库存储规则:
- 表结构:
permission_rules(resource,field,action,condition_expression) condition_expression:如user.dept_id == order.dept_id && user.level >= 3
- 表结构:
-
引入表达式语言库(推荐
symfony/expression-language或hassankhan/config的eval()的替代品)。use Symfony\Component\ExpressionLanguage\ExpressionLanguage; $language = new ExpressionLanguage(); $rule = $db->findRule('Order', 'price', 'view'); $context = [ 'user' => ['id' => 1, 'role' => 'admin'], 'order' => ['id' => 5, 'user_id' => 1, 'amount' => 100], ]; if ($language->evaluate($rule->condition_expression, $context)) { // 允许查看或修改 price 字段 }
优点:高度灵活,规则可热更新,不需要改 PHP 代码。 缺点:调试困难;表达式写错会导致系统出问题;性能取决于表达式解析效率。
前端配合后端脱敏(最实用的工程方案)
对于 API 驱动的应用,最务实的做法是后端根据权限动态裁剪返回数据,前端只展示后端传递的字段。
实现方式(以 JSON API 为例):
-
后端使用资源转换器(如 Laravel Resource,自定义 Array 转换)。
// 在 toArray 中根据权限过滤 public function toArray($request) { $user = auth()->user(); $data = parent::toArray($request); if (!$user->can('view', 'order.price')) { unset($data['price']); } // 处理嵌套对象 if (isset($data['customer']) && !$user->can('view', 'order.customer.phone')) { unset($data['customer']['phone']); } return $data; } -
对于写操作(Create/Update):在验证器或控制器中校验每个字段。
// 在控制器更新方法中 $allowedFields = $this->getAllowedEditableFields($order, $user); $input = $request->only($allowedFields); // 只提取允许修改的字段 $order->update($input);
优点:实现简单,与现有框架集成好,后端完全控制数据边界。 缺点:代码分散在资源类或控制器中,难以统一管理。
最佳实践建议(根据项目复杂度选择)
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| CRUD 后台系统 | 方案四(前端脱敏) + 方案一(策略模式) | 快速实现,随业务增长可平滑迁移到策略模式 |
| SaaS、多租户、高安全要求 | 方案一(策略模式)或 方案三(规则引擎) | 策略模式可测试性强;规则引擎适合客户自定义 |
| 低代码 / 平台型产品 | 方案三(规则引擎) + 方案二(Attribute 声明) | 让非技术人员配置权限,开发者定义字段类型和权限锚点 |
| 微服务之间的细粒度控制 | 方案二(Attribute 注解) + OPA / Casbin | 使用外部策略引擎(如 Open Policy Agent)或 Casbin 统一管理 |
特别注意点
- 避免全局白名单:很多项目使用
$fillable+only()实现粗粒度过滤,细粒度权限需要在每个请求中动态计算允许字段。 - 性能考虑:不要在循环中查询数据库,提前加载权限规则(一行 SQL 查出当前用户所有字段权限)。
- 前端配合:后端应该同时返回
allowed_fields列表,让前端知道哪些字段可以显示和编辑,避免前端试图提交被禁用的字段。 - 审计日志:当发生“拒绝访问”时,记录详细日志(用户ID、请求字段、被拒绝的策略),方便排查权限配置问题。
对于大多数 PHP 项目,最推荐的方案是策略模式 + 后端脱敏,先用策略类封装判断逻辑,然后在控制器或资源层根据策略返回值过滤字段,这样既能保持代码结构清晰,又能灵活应对复杂的业务规则。