TP8.0 实现 API 接口字段级别的细粒度权限控制 ACL

作者:袖梨 2026-07-14
TP8.0 字段级权限控制在数据组装阶段动态脱敏,通过 AclFieldService 结合用户上下文校验 resource_type 与 field_name 组合权限,支持条件表达式、缓存及 missing_permissions 响应提示。

TP8.0 实现 API 接口字段级细粒度权限控制,核心不是“拦住整个接口”,而是“放行请求但过滤响应字段”。ACL 在这里不用于路由拦截,而用于数据组装阶段的动态脱敏与裁剪。

字段权限必须在数据组装阶段干预

ThinkPHP 的中间件或 AuthRule::check() 作用于请求入口,只能决定“能否进”,无法控制“出来时带哪些字段”。字段级控制必须发生在模型数据转 JSON 前——比如在资源类(Resource)、DTO 构造器、或统一响应封装层中。

  • 避免在控制器里手动 unset() 字段:易遗漏、难复用、测试困难
  • 推荐在 Resource 层做字段白名单/黑名单:例如定义 $visible 或 $hidden,但需结合当前用户上下文动态计算
  • 若用 DTO 模式,构造 DTO 实例时传入 $user,由 DTO 决定是否填充 salary、id_card 等敏感字段

用 ACL 服务统一管理字段可读规则

不要把字段权限硬编码进模型或 Resource。应抽象出 AclFieldService,接收 user、resource_type(如 'user')、field_name(如 'salary')三参数,返回 true/false。

  • 规则来源可为数据库 permission_conditions 表,支持条件表达式(如 "role == 'hrbp' && dept_id == user.dept_id")
  • 缓存策略:key 为 field_acl_{$uid}_{$resource}_{$field},TTL 设为 10 分钟,权限变更时主动 del
  • 示例调用:if (! app(AclFieldService::class)->canRead($user, 'user', 'salary')) { $data['salary'] = null; }

资源标识与字段权限解耦设计

字段权限不是孤立存在的,它依附于具体资源实例和操作上下文。因此 resource_type 和 field_name 需配合业务语义组合使用,而非简单拼接。

  • 正确写法:user:profile:email(用户资料页的邮箱字段)、order:detail:refund_amount(订单详情中的退款金额)
  • 错误写法:/api/users/123/email —— 路径含 ID,无法缓存复用;且与前端字段名耦合过紧
  • 字段权限可继承:若 user:profile 整体不可读,则其下所有字段默认不可读,除非显式授权

API 响应明确反馈缺失字段权限

字段被隐藏时,不应静默丢弃,而应在响应中提供可追溯的提示,便于前端适配或审计排查。

  • 响应结构建议增加 missing_permissions 字段,值为字符串数组,如 ["user:profile:salary"]
  • 开发环境开启该字段,生产环境可关闭以减少暴露面
  • 日志中记录字段拦截事件,包含 user_id、resource_id、field、timestamp,供安全审计

相关文章

精彩推荐