超媒体驱动的动态权限

核心判断

超媒体驱动的动态权限,本质上是把权限控制从传统的、硬编码的能力检查(Can the user do X?),转为由服务端通过超媒体响应声明的能力发现(What can the user do?)。

前端不再维护权限规则,也不通过权限点列表决定按钮是否展示;它只根据 API 响应中是否存在某个操作链接来渲染 UI。真正的权限决策和最终校验仍然留在服务端。

这个模式的价值不在于替代授权系统,而在于让前端 UI 成为一个更纯粹的超媒体客户端:它根据服务端下发的资源状态和可用操作自动变化,从而降低前后端对权限规则的耦合。


我目前知道的内容

1. 它解决的是前端权限规则硬编码问题

传统权限模式通常要求前端持有权限点列表,然后在渲染组件时判断:

if (hasPermission('user:delete')) {
  // 显示删除按钮
}

这种模式的问题是,前端必须理解权限点的语义,并和后端共同维护权限字符串、角色规则和业务含义。一旦权限规则变化,前后端都可能需要同步修改。

超媒体驱动的动态权限则把这个判断前移到服务端。服务端根据当前用户、资源状态和业务规则,在响应中声明当前可用的操作。前端只负责发现和展示这些操作。

2. 核心转变是从“能力检查”到“能力发现”

传统模式下,前端问的是:

当前用户能不能执行 delete user

超媒体驱动模式下,前端问的是:

当前资源表示中有没有 delete 这个操作链接?

也就是说,前端不再直接判断权限,而是从服务端返回的超媒体文档中发现下一步可执行的动作。

3. 服务端通过超媒体链接声明可用操作

服务端根据当前登录用户的权限,在返回的资源表示中嵌入其可执行的操作链接。

// GET /api/users/123
// 管理员看到的响应:
{
  "user": {
    "id": 123,
    "name": "Alice",
    "email": "alice@example.com"
  },
  "_links": {
    "self": { "href": "/api/users/123" },
    "update": { "href": "/api/users/123", "method": "PUT" },
    "delete": { "href": "/api/users/123", "method": "DELETE" },
    "promote": { "href": "/api/users/123/promote", "method": "POST" }
  }
}
 
// 普通用户看到的响应(查看自己的资料):
{
  "user": {
    "id": 456,
    "name": "Bob",
    "email": "bob@example.com"
  },
  "_links": {
    "self": { "href": "/api/users/456" },
    "update": { "href": "/api/users/456", "method": "PUT" }
    // 没有 'delete' 和 'promote' 链接
  }
}

在这个模型里,_links.delete 是否存在,就是前端是否显示“删除用户”入口的依据。

4. 前端通过链接存在性控制 UI 渲染

前端组件无需知道权限逻辑,只需检查特定链接关系(rel)是否存在。

function UserProfile({ userData }) {
  const { user, _links } = userData;
 
  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
 
      <div className="actions">
        {_links.update && (
          <button onClick={() => handleEdit(user)}>编辑资料</button>
        )}
 
        {_links.delete && (
          <button onClick={() => handleDelete(user)}>删除用户</button>
        )}
 
        {_links.promote && (
          <button onClick={() => handlePromote(user)}>晋升为管理员</button>
        )}
      </div>
    </div>
  );
}

这里的关键不是前端“没有权限系统”,而是前端不再保存业务权限规则。它只消费服务端已经计算好的可用操作集合。

5. 它降低前后端耦合,但不会取消协作契约

超媒体驱动权限可以让前端不再依赖具体权限点,例如 user:deleteuser:promote。但前后端仍然需要围绕链接关系名建立稳定契约,例如:

  • self 表示当前资源;
  • update 表示可更新;
  • delete 表示可删除;
  • promote 表示可晋升。

因此,这个模式不是让前端完全“不知道操作语义”,而是让前端从依赖权限规则,转向依赖更稳定的超媒体操作语义。

6. 它隐藏的是交互入口,不是安全边界

超媒体驱动的动态权限可以让无权操作的入口不可见,减少误操作,也减少前端条件判断。但这不等于客户端成为安全边界。

即使前端没有展示删除按钮,用户仍然可能手动构造请求。因此服务端在执行 DELETE /api/users/123 之前,仍必须再次进行权限校验。

换句话说:

超媒体链接负责告诉客户端“当前可以尝试什么”;服务端授权负责决定“这个操作最终能不能执行”。


传统模式与超媒体驱动模式的差异

方面传统权限模式超媒体驱动动态权限
核心逻辑前端持有权限点列表,渲染前执行 hasPermission(...) 检查。前端不持有权限规则,只从 API 响应中发现可用操作链接。
前后端耦合前端必须和后端约定所有权限点字符串及其语义。前端主要依赖稳定的链接关系 rel,不关心具体权限规则。
动态性权限规则变化时,可能需要前后端同时调整。权限规则由服务端控制,前端 UI 随响应自动变化。
工作流程1. 前端加载用户权限列表。
2. 渲染组件时根据权限列表显示或隐藏入口。
1. 前端请求资源。
2. 服务端返回资源数据和当前可用操作链接。
3. 前端根据链接渲染对应 UI。
安全边界服务端仍需最终校验。服务端仍需最终校验,链接只影响客户端可见入口。

优势与收益

  1. 降低前后端对权限规则的耦合
    前端不需要知道“删除用户需要 admin 角色”这类业务规则,只需要知道资源响应中是否存在 delete 链接。

  2. 权限变化可以更动态地反映到 UI
    如果用户权限或资源状态发生变化,下次请求资源时,相关操作链接就会出现或消失,前端 UI 随之更新。

  3. API 更自描述
    通过响应中的 _links,客户端可以知道当前状态下有哪些可能的下一步操作。

  4. 更适合状态相关的操作控制
    某些操作不仅取决于用户角色,也取决于资源状态。例如订单是否可退款、流程是否可撤回、任务是否可转交。由服务端动态声明可用操作,比前端写死一组条件更可靠。

  5. 安全逻辑集中在服务端
    前端隐藏入口只是交互优化,真正的授权判断仍由服务端执行,避免把安全逻辑分散到客户端。


落地要点

  • 使用标准化超媒体格式:如 HAL、JSON:API、Siren 等,避免自定义结构过于随意。
  • 让链接包含足够信息:除了 href,通常还应包含 method;必要时可以包含 type、请求体 schema 或表单描述。
  • 稳定设计链接关系 rel:前端可以不依赖权限点,但仍会依赖操作语义。rel 命名要稳定、清晰、可复用。
  • 统一 UI 状态与 API 状态:按钮显示、禁用、可执行状态都可以由服务端响应驱动。
  • 保留错误处理:客户端仍应处理 403 Forbidden404 Not Found409 Conflict 等错误,因为资源状态可能在用户看到 UI 后发生变化。
  • 不要把隐藏按钮误当成授权:服务端执行操作前必须重新校验权限和资源状态。

这个主题可以拆成哪些部分

  • 能力检查 vs. 能力发现
  • 服务端如何在资源表示中声明可用操作
  • 前端如何根据链接存在性渲染操作入口
  • 超媒体权限如何降低前端对权限规则的依赖
  • rel 如何成为前后端协作契约
  • 超媒体权限为什么不能替代服务端最终校验
  • 动态操作控制与资源状态的关系
  • 超媒体驱动权限与 RBAC / ABAC / Policy Engine 的关系

相关概念 / 下层卡片

暂时结论

超媒体驱动的动态权限,是 HATEOAS 思想在权限与操作入口控制中的一种应用。它把“用户能做什么”从前端权限判断,转化为服务端资源表示的一部分。

这个模式最适合解决前端权限规则硬编码、操作入口状态分散、资源状态与 UI 状态不一致等问题。但它不应该被理解为完整授权系统。更准确地说,它是一种交互能力发现机制:前端通过超媒体链接发现当前可以尝试的操作,而服务端仍负责最终授权与执行校验。