权限管理
前端权限管理本质是:根据用户身份控制可见/可操作的资源。权限数据来自后端,前端负责展示控制 + 路由拦截 + 接口鉴权兜底。
一、权限模型
- RBAC(Role-Based Access Control):用户 → 角色 → 权限。最常用
- ABAC(Attribute-Based):基于属性(部门、时段、IP 等),更灵活但实现复杂
- PBAC(Policy-Based):策略引擎(OPA、Casbin),适合复杂业务
绝大多数前端场景用 RBAC 即可。
二、权限分类
| 类型 | 粒度 | 典型实现 |
|---|---|---|
| 菜单权限 | 路由级 | 动态路由 |
| 页面权限 | 页面级 | 路由守卫 + 403 兜底 |
| 按钮权限 | 元素级 | 指令/组件包裹 |
| 接口权限 | 请求级 | axios 拦截器 |
| 数据权限 | 行/列级 | 后端处理为主,前端辅助 |
三、菜单权限:动态路由
核心思路:登录后拉取该用户的菜单树 → 动态 addRoute 注册 → 渲染侧边栏。
// 1. 定义静态路由(无需权限)
export const constantRoutes = [
{ path: "/login", component: Login },
{ path: "/404", component: NotFound },
];
// 2. 动态路由(需要权限)
export const asyncRoutes = [
{
path: "/user",
component: Layout,
meta: { title: "用户管理", roles: ["admin"] },
children: [
{ path: "list", component: UserList, meta: { title: "用户列表" } },
],
},
// ...
];
// 3. 根据用户角色过滤路由
function filterRoutes(routes: Route[], roles: string[]) {
return routes.filter((r) => {
if (r.meta?.roles && !r.meta.roles.some((role) => roles.includes(role))) {
return false;
}
if (r.children) r.children = filterRoutes(r.children, roles);
return true;
});
}
// 4. 登录后动态注册
const accessRoutes = filterRoutes(asyncRoutes, user.roles);
accessRoutes.forEach((r) => router.addRoute(r));
四、按钮权限
封装一个权限组件 / 指令,避免到处写 v-if。
React 组件方式
import { useStore } from "@/store";
interface Props {
code: string; // 权限码
children: React.ReactNode;
}
export const AuthButton: React.FC<Props> = ({ code, children }) => {
const { user } = useStore();
if (!user.permissions.includes(code)) return null;
return <>{children}</>;
};
// 使用
<AuthButton code="user:delete">
<Button danger onClick={handleDelete}>删除</Button>
</AuthButton>
Vue 自定义指令
app.directive("auth", {
mounted(el, binding) {
const { value: code } = binding;
const permissions = useUserStore().permissions;
if (!permissions.includes(code)) {
el.parentNode?.removeChild(el);
}
},
});
// 使用
<el-button v-auth="'user:delete'">删除</el-button>
五、路由守卫
router.beforeEach((to, from, next) => {
const token = getToken();
if (to.path === "/login") return next();
if (!token) return next("/login");
if (!store.user.id) {
// 刷新页面后 user 状态丢失,重新拉取
return store.fetchUser().then(() => next());
}
// 权限校验
if (to.meta.roles && !to.meta.roles.includes(store.user.role)) {
return next("/403");
}
next();
});
六、接口权限兜底
前端权限是体验优化,真正的安全必须靠后端。但前端可以做兜底:
service.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
// token 过期
clearToken();
router.push("/login");
} else if (err.response?.status === 403) {
message.error("无权访问该资源");
}
return Promise.reject(err);
}
);
七、避坑指南
- 路由一定要懒加载:
() => import("@/views/User"),否则动态路由的代码分割失效 - 刷新页面后路由丢失:pinia/redux 状态从内存丢失,要么持久化、要么刷新时重新拉取菜单
- 按钮权限的 code 要统一管理:在
permissions.ts集中定义,避免散落字符串 - 不要只依赖前端权限:用户绕过前端直接调接口,照样能拿到数据,后端必须做最终校验
- 路由 meta 设计:提前约定好
title / icon / roles / keepAlive / hideInMenu等字段