前端权限控制的目标是确保不同角色用户只能看到并操作被授权的界面元素。路由级守卫解决了页面访问入口的问题,但一个页面内部往往包含新增、编辑、删除、发布等不同敏感操作。如果只依赖页面路由,普通编辑也可能看到并点击发布按钮,甚至通过浏览器控制台直接调用接口。RBAC模型把用户、角色、权限拆成独立实体,角色作为权限的集合分配给用户,权限编码再以扁平化列表的形式下发到前端。React则可以通过组件封装,把权限判断逻辑下沉到每一个按钮级别。

一、RBAC模型的数据结构与前端存储
RBAC全称是Role-Based Access Control,即基于角色的访问控制。核心思想是用户不直接绑定权限,而是通过角色间接获得权限。角色与权限是多对多关系,用户与角色也是多对多关系。一个用户可以同时拥有多个角色,一个角色也可以包含多个权限。登录成功后,后端通常不会只返回角色编码,而是会把角色关联的所有权限编码合并去重后,直接返回给前端。前端拿到的是类似 ["article:create", "article:edit", "article:publish"] 这样的权限数组,判断起来非常方便。
权限编码的命名规范建议采用“资源:操作”的形式,例如 article:create 表示文章创建权限,comment:delete 表示评论删除权限。这样做的好处是语义清晰,后续新增权限时不容易冲突。下面是一个典型的登录返回结构示例:
// 用户角色与权限类型定义
export interface UserInfo {
userId: string;
username: string;
roles: string[];
permissions: string[];
}
// 登录接口返回示例
const loginResponse = {
token: "eyJhbGciOiJIUzI1NiJ9...",
user: {
userId: "10001",
username: "zhangsan",
roles: ["editor"],
permissions: [
"article:create",
"article:edit",
"article:publish",
"comment:delete"
]
}
};
前端拿到权限列表后,需要存储在全局状态中。可以使用 Context、Redux 或 Zustand 等方案。关键点在于权限列表应该在登录成功后立即写入状态,并且配合持久化存储。刷新页面后,如果只保留了 token 而丢失了权限列表,就需要重新调用获取用户信息的接口;如果做了持久化,则可以直接从本地存储恢复,减少白屏和重复请求。无论哪种方式,都要保证全局状态中的权限列表和登录态同步更新,避免出现 token 有效但权限数组为空的情况。
二、路由级权限控制与登录态守卫
路由级权限控制通常通过一个包装组件实现。React Router 中可以在渲染目标路由前插入守卫逻辑,检查用户是否登录以及是否具备访问该路由所需的权限。如果未登录则跳转登录页,如果已登录但缺少权限则跳转 403 页面。这样的守卫组件可以复用,避免每个页面重复编写登录校验代码。
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
function ProtectedRoute({ requiredPermission, children }) {
const { token, permissions } = useAuth();
if (!token) {
return <Navigate to="/login" replace />;
}
if (requiredPermission && !permissions.includes(requiredPermission)) {
return <Navigate to="/403" replace />;
}
return children;
}
export default ProtectedRoute;
使用路由守卫可以防止用户通过地址栏直接输入 URL 进入未授权页面。但它的局限性也很明显:它只能控制页面级的访问,无法控制页面内部某个按钮的显示与隐藏。例如文章编辑页面可能同时包含保存、发布、删除等操作,路由守卫无法区分用户到底具备哪一个操作权限。哪怕用户没有删除权限,只要进入了编辑页,仍然可能看到删除按钮,甚至通过抓包构造请求来执行删除操作。
针对页面内细粒度控制的需求,还可以采用动态路由方案。前端根据权限列表动态生成可访问的路由表,只有具备对应权限时才注册该路由。这种方式在大型管理后台中比较常见,能减少静态路由硬编码。但动态路由仍然只解决了“进不进得去页面”的问题,按钮级控制还需要更轻量的组件或 Hook 来配合。
三、按钮级权限控制的实现方式对比
在 React 中实现按钮级权限控制,最常见的方式有三种:条件渲染、权限包装组件、以及在事件处理函数中调用权限判断 Hook。条件渲染最简单,直接写 {permissions.includes('article:edit') && <button>编辑</button>},但会导致权限判断逻辑分散在业务代码中,不易维护。权限包装组件则把判断逻辑集中到一个地方,使用声明式的方式包裹目标元素。自定义 Hook 则适合在事件处理过程中做权限兜底。
推荐使用权限包装组件的方式。下面通过自定义 Hook 和 Permission 组件来实现按钮级控制。Hook 负责从全局状态中读取权限列表,并提供一个判断函数。Permission 组件接收权限编码,根据判断结果决定渲染子元素还是兜底内容。
import { useAuth } from './AuthContext';
export function usePermission() {
const { permissions } = useAuth();
const hasPermission = (code) => {
if (Array.isArray(code)) {
return code.some((item) => permissions.includes(item));
}
return permissions.includes(code);
};
return { hasPermission };
}
export function Permission({ code, children, fallback = null }) {
const { hasPermission } = usePermission();
if (!hasPermission(code)) {
return fallback;
}
return <>{children}</>;
}
Permission 组件使用方式非常直观,把需要控制的按钮放在组件内部即可。如果当前用户没有对应权限,组件会返回 fallback 或者 null,按钮就不会渲染出来。这种写法类似于 Vue 中的自定义指令,虽然 React 本身没有指令系统,但借助组件封装可以达到同样的效果。下面是一个实际业务中的用法示例:
import { Permission } from './Permission';
function ArticleActionBar({ articleId, onEdit, onPublish, onDelete }) {
return (
<div className="action-bar">
<Permission code="article:edit">
<button onClick={() => onEdit(articleId)}>编辑</button>
</Permission>
<Permission code="article:publish">
<button onClick={() => onPublish(articleId)}>发布</button>
</Permission>
<Permission code="comment:delete">
<button className="danger" onClick={() => onDelete(articleId)}>删除评论</button>
</Permission>
</div>
);
}
除了控制显隐,按钮的点击事件里也应当做权限校验。因为隐藏按钮只是不给普通用户展示入口,但攻击者可以通过浏览器控制台直接调用组件内部方法或发送请求。所以在 onClick 处理函数中再次调用 hasPermission 做校验,可以防止前端被暴力绕过。如果权限不足,直接提示无操作权限并提前返回,不执行后续逻辑。这样按钮级指令既保证了界面整洁,又增加了操作层的安全屏障。
四、后端二次校验与边界优化
前端权限控制本质上是用户体验优化,不能作为安全手段。因为前端代码运行在用户浏览器中,权限判断逻辑可以被篡改或绕过。因此所有受限操作对应的后端接口都必须进行权限校验。前端在请求时携带登录凭证,后端根据用户角色或权限列表判断是否允许执行该操作。下面是一个 Node.js 后端的校验示例:
axios.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
app.post('/api/article/edit', authenticate, (req, res) => {
if (!req.user.permissions.includes('article:edit')) {
return res.status(403).json({ code: 403, message: '无操作权限' });
}
// 执行编辑逻辑
});
权限更新与缓存失效也是实践中容易出现问题的地方。当管理员修改了某个用户的角色后,该用户如果浏览器仍然保存着旧的权限列表,可能继续看到或操作已经失去权限的功能。解决方式有两种:一是在角色变更后强制用户重新登录,重新拉取最新权限;二是前端提供权限刷新机制,在关键操作前调用接口重新获取权限并更新全局状态。无论采用哪种方式,都要确保权限列表与实际权限保持同步。
另一个常见的误区是使用反向判断来控制权限。比如认为“没有禁止权限就显示按钮”,这种逻辑非常危险,一旦权限列表丢失或出现异常,就可能暴露不应该出现的操作入口。正确的做法是默认拒绝,只有明确拥有某个权限时才显示对应按钮。权限编码命名要保持一致,前后端统一使用同一套资源标识,避免出现前端判断 article:edit,后端却使用 edit_article 导致权限失效的情况。
RBAC 模型与按钮级权限控制指令相结合,可以让 React 应用的权限管理更加清晰和可控。路由守卫管住页面入口,Permission 组件管住页面内操作,后端接口校验管住最后一道防线。三层联动才能形成完整的权限闭环,真正避免越权操作和权限漏洞。