Skip to main content

权限管理

前端权限管理本质是:根据用户身份控制可见/可操作的资源。权限数据来自后端,前端负责展示控制 + 路由拦截 + 接口鉴权兜底

一、权限模型

  • 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);
}
);

七、避坑指南

  1. 路由一定要懒加载() => import("@/views/User"),否则动态路由的代码分割失效
  2. 刷新页面后路由丢失:pinia/redux 状态从内存丢失,要么持久化、要么刷新时重新拉取菜单
  3. 按钮权限的 code 要统一管理:在 permissions.ts 集中定义,避免散落字符串
  4. 不要只依赖前端权限:用户绕过前端直接调接口,照样能拿到数据,后端必须做最终校验
  5. 路由 meta 设计:提前约定好 title / icon / roles / keepAlive / hideInMenu 等字段