Backstage 插件前端鉴权实战:用 usePermission 与 RequirePermission 为 UI 组件加权限保护
2026/9/12 3:03:12 网站建设 项目流程

Backstage 插件前端鉴权实战:用 usePermission 与 RequirePermission 为 UI 组件加权限保护

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

前端鉴权是 Backstage 权限框架(Permission Framework)的最后一公里:当后端路由已经通过权限策略得到保护后,前端还需要在用户体验层面做出响应——禁用按钮、隐藏组件、拦截路由。本文以 todo-list 示例插件为线索,讲解如何在 Backstage 插件前端使用@backstage/plugin-permission-react提供的usePermissionHook 与RequirePermission组件,并深入源码剖析其底层实现(stale-while-revalidate 缓存、请求批处理、资源权限短路等),帮助你写出"既安全又友好"的插件 UI。

为什么前端也要做鉴权

在权限框架系列教程的前几节中,我们已经学会了如何用PermissionPolicy保护插件后端 API 路由。对于绝大多数"返回数据用于展示"的路由(例如示例中的GET /todos),前端无需任何改动——后端会直接返回空列表或404,这本身已经足够安全。

但问题出在触发变更操作(mutative action)的 UI 元素上。以 todo-list 应用的 "Add" 按钮为例:用户点击后,前端会向后端/todos路由发起POST请求。如果一个用户没有创建 todo 的权限,他只能等到点下按钮、看到报错时才意识到自己被拒绝——这是非常糟糕的用户体验。更合理的做法是提前禁用这个按钮,甚至在权限不足时直接隐藏整个添加区块。

:::note 安全边界:前端鉴权永远不能替代后端鉴权 前端鉴权只能作为后端鉴权的补充,用于改善用户体验,绝不能作为安全防线本身。前端代码对用户完全可见、可篡改,即便你禁用了按钮或隐藏了组件,恶意用户仍然可以直接向后端路由发送请求。因此,务必先确保后端路由已置于鉴权保护之下,再在前端做这些优化。 :::

安装依赖

在插件前端做鉴权,需要两个包:@backstage/plugin-permission-react(提供 Hook 与组件)以及你插件自己的-common包(导出已声明的权限对象)。以 todo-list 插件为例:

$ yarn workspace @internal/plugin-todo-list \ add @backstage/plugin-permission-react @internal/plugin-todo-list-common

@backstage/plugin-permission-react包的核心导出非常精简,它从三个子模块统一对外暴露能力(见 plugins/permission-react/src/index.ts):

  • componentsRequirePermission组件;
  • hooksusePermissionHook;
  • apisPermissionApi接口与permissionApiRef(前端鉴权请求的统一入口)。

用 usePermission 禁用按钮

假设todoListCreatePermission是我们在@internal/plugin-todo-list-common中声明的普通(非资源型)权限。在plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx中引入 Hook 与权限对象:

import { alertApiRef, discoveryApiRef, fetchApiRef, useApi, } from '@backstage/core-plugin-api'; import { usePermission } from '@backstage/plugin-permission-react'; import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; function AddTodo({ onAdd }: { onAdd: (title: string) => any }) { const title = useRef(''); const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission, }); return ( <> <Typography variant="body1">Add todo</Typography> <Box component="span" alignItems="flex-end" display="flex" flexDirection="row" > <TextField placeholder="Write something here..." onChange={e => (title.current = e.target.value)} /> {!loadingPermission && ( <Button disabled={!canAddTodo} variant="contained" onClick={() => onAdd(title.current)} > Add </Button> )} </Box> </> ); }

关键点解读:

  • usePermission接收一个包含permission字段的参数对象,返回AsyncPermissionResult,其中loading表示鉴权请求是否仍在进行,allowed表示当前用户是否被允许执行该操作(plugins/permission-react/src/hooks/usePermission.ts)。
  • loadingPermissiontrue时不要渲染按钮(或渲染占位),避免出现"先可用、后禁用"的闪烁;加载完成后依据canAddTodo决定是否disabled

修改策略验证效果

改完前端代码后,把getting-started阶段创建的权限策略模块(CustomPolicy类)改为对创建 todo 的请求返回DENY,即可验证按钮是否被正确禁用:

if (isPermission(request.permission, todoListCreatePermission)) { return { result: AuthorizeResult.DENY, }; }

此时刷新页面,你会看到 "Add" 按钮处于禁用状态,用户不再被"点下去才报错"的糟糕交互所困扰。注意:策略模块的搭建、permission.enabled: true的配置以及默认 allow-all 策略的移除步骤,请参考 权限框架快速上手。

用 RequirePermission 隐藏组件

"禁用"是给用户的一个有用信号,但在某些场景下你可能更倾向于直接隐藏无权限的元素。这时可以使用RequirePermission组件:

import { RequirePermission } from '@backstage/plugin-permission-react'; import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; export const TodoListPage = () => { // ... return ( <Grid container spacing={3} direction="column"> <RequirePermission permission={todoListCreatePermission} errorPage={<></>}> <Grid item> <AddTodo onAdd={handleAdd} /> </Grid> </RequirePermission> <Grid item> <TodoList key={key} onEdit={setEdit} /> </Grid> </Grid> ); }; // AddTodo 内部不再需要 usePermission,恢复为普通按钮 function AddTodo({ onAdd }: { onAdd: (title: string) => any }) { const title = useRef(''); return ( <> <Typography variant="body1">Add todo</Typography> <Box component="span" alignItems="flex-end" display="flex" flexDirection="row" > <TextField placeholder="Write something here..." onChange={e => (title.current = e.target.value)} /> <Button variant="contained" onClick={() => onAdd(title.current)}> Add </Button> </Box> </> ); }

改动之后,没有权限的用户将完全看不到"添加 todo"的输入区。RequirePermission组件的errorPage属性可以自定义"无权限时渲染什么";如果不传,则会回退到应用默认的NotFoundErrorPage组件(plugins/permission-react/src/components/RequirePermission.tsx)。上面示例中传了errorPage={<></>},即无权限时什么都不渲染,实现纯隐藏。

用 RequirePermission 保护路由

RequirePermission同样可以包在路由元素外层,用来拦截整个页面的访问。以packages/app/src/App.tsx为例:

import { RequirePermission } from '@backstage/plugin-permission-react'; import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; const routes = ( <FlatRoutes> <Route path="/search" element={<SearchPage />}> {searchPage} </Route> <Route path="/settings" element={<UserSettingsPage />} /> <Route path="/todo-list" element={ {/* 更严谨的做法是为页面单独声明一个 "read" 权限,这里仅作示例 */} <RequirePermission permission={todoListCreatePermission}> <TodoListPage /> </RequirePermission> }> {/* ... */} </Route> </FlatRoutes> );

现在直接访问https://localhost:3000/todo-list,无权限用户会看到错误页面(默认是 Not Found 页,因为对无权用户隐藏路由的存在本身就是一种信息保护)。

源码级原理:前端鉴权到底怎么跑

了解上层 API 之后,深入@backstage/plugin-permission-react的源码,可以看到几个值得注意的实现细节:

1. usePermission:SWR 缓存 + 资源权限短路

usePermission内部通过useApi(permissionApiRef)拿到PermissionApi,再用useSWR对鉴权结果做缓存(plugins/permission-react/src/hooks/usePermission.ts)。源码注释明确指出,这里采用stale-while-revalidate策略,目的是避免根据allowed结果做条件渲染时出现 UI 闪烁。

另一个关键设计是资源权限的短路处理usePermission支持资源型权限(ResourcePermission)并接受可选的resourceRef。当传入的是资源权限但resourceRef尚未就绪(例如实体还在异步加载中)时,Hook 会直接返回DENY(plugins/permission-react/src/hooks/usePermission.ts),而不是发起一次注定失败的请求——这是为了配合"实体异步加载"场景特意做的安全默认值。相应地,此时allowed恒为false,UI 会表现为无权限状态,直到resourceRef就绪后重新评估。

2. IdentityPermissionApi:DataLoader 请求批处理

默认的PermissionApi实现是IdentityPermissionApi,它使用DataLoader同一微任务内发生的多次鉴权调用合并成一次 HTTP 请求(plugins/permission-react/src/apis/IdentityPermissionApi.ts)。这意味着一个页面上即使有十几个组件同时调用usePermission,后端也只会收到一个批量的authorize请求,性能开销极小。

3. RequirePermission:三态渲染

RequirePermission内部其实就是对usePermission的一层封装(plugins/permission-react/src/components/RequirePermission.tsx):

  • loadingtrue时:返回null,什么都不渲染;
  • allowedtrue时:渲染children
  • 否则:渲染errorPage(若提供),否则回退到app.getComponents()中的NotFoundErrorPage

所以它天然适合作为"路由级守卫"或"区块级守卫",把鉴权逻辑从业务组件中剥离出去。

测试验证:前端鉴权组件的行为契约

仓库为这两个前端鉴权 API 提供了完整的单元测试,可以作为理解其行为的"契约文档":

  • usePermission.test.tsx 验证了三件事:鉴权请求未返回时状态为loading;返回ALLOWallowed为真并渲染内容;返回DENY时不渲染内容。
  • RequirePermission.test.tsx 验证了五种行为:加载中不渲染子元素;有权限时渲染子元素;无权限时默认渲染 Not Found 页(测试中通过 "Reached NotFound Page" 断言);无权限时可用errorPage渲染自定义错误页;以及资源权限 +resourceRef的鉴权组合也能正常工作。

如果你的插件需要为前端鉴权写测试,可以参考这两个文件:用TestApiProvider注入 mock 的permissionApiRef,即可在测试环境中完全控制鉴权结果。

小结

给 Backstage 插件加前端鉴权,核心就是两条路:

  1. 禁用:用usePermission拿到loading/allowed,在加载完成后禁用无权限的操作按钮,保留可见性但明确"不可用";
  2. 隐藏/拦截:用RequirePermission包裹组件或路由,无权限时不渲染(可自定义errorPage)或显示 Not Found 页。

无论选择哪种方式,都请牢记:前端鉴权只是用户体验优化,真正的安全边界永远在后端路由。完整的权限策略编写与资源权限鉴权,可继续阅读 编写权限策略、添加基础权限检查 与 添加资源权限检查。

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询