访问控制实战:从RBAC到ABAC的权限设计与落地经验
为什么我又在折腾访问控制?
说实话,每次做新项目,访问控制这块总得重新纠结一遍。RBAC、ABAC、自定义中间件、路由守卫……方案一堆,但真用起来,坑也不少。最近一个后台系统要支持多角色+细粒度权限(比如“只能看自己部门的数据”),又把我逼回这个老问题上。折腾完一轮后,我觉得有必要把几个主流方案拉出来遛一遛,说说我到底踩了哪些坑,最后选了哪个。
谁更灵活?谁更省事?
先说结论:我一般优先选基于角色的访问控制(RBAC)搭骨架,再用属性或上下文补细节。纯 ABAC 太重,纯自定义又太乱。下面具体聊聊。
方案一:传统 RBAC(Role-Based Access Control)
这是最常见也最容易上手的。用户 → 角色 → 权限,三层结构。比如管理员有“删除文章”权限,编辑只有“发布文章”权限。
前端实现通常是在路由守卫里判断:
// Vue Router 示例
router.beforeEach((to, from, next) => {
const userRoles = store.getters.userRoles; // ['editor', 'viewer']
const requiredRoles = to.meta.roles || [];
if (requiredRoles.length && !requiredRoles.some(role => userRoles.includes(role))) {
next('/403');
} else {
next();
}
});
优点很明显:简单、直观、和后端对得上。大部分后台系统够用了。
但问题也来了——一旦需求变成“销售A只能看自己客户的订单”,RBAC 就不够看了。你不可能给每个销售建一个角色吧?这时候就得加料。
方案二:ABAC(Attribute-Based Access Control)
ABAC 是靠属性(用户属性、资源属性、环境等)动态判断权限。比如“用户部门 == 资源所属部门”。
前端如果要实现,可能得写个策略引擎:
function canAccess(resource, action, context) {
const { user, env } = context;
// 示例策略:普通用户只能看自己的数据
if (action === 'read' && resource.type === 'order') {
return user.id === resource.ownerId || user.role === 'admin';
}
// 更复杂的:工作时间才能导出报表
if (action === 'export' && env.time.getHours() < 9 || env.time.getHours() > 18) {
return false;
}
return true;
}
听起来很强大,对吧?但亲测有效但维护成本高。策略散落在各处,改一个权限逻辑可能要翻好几个文件。而且前端做 ABAC 其实不太合理——敏感权限判断应该在后端,前端只是做 UI 层的隐藏/跳转。所以我现在只在 UI 层用轻量版 ABAC,核心判断还是依赖 API 返回。
比如接口返回 canEdit: true,我就显示编辑按钮;否则藏掉。这样前后端职责清晰,也避免前端被绕过。
核心代码就这几行(但别小看它)
我现在实际项目中的做法,其实是混合体:RBAC 控大方向,ABAC 补细节,但权限数据全部来自后端。
登录后,我会调一次权限接口,拿到用户的“能力清单”(capabilities):
// 登录后获取权限
const res = await fetch('https://jztheme.com/api/user/permissions');
const permissions = await res.json(); // { canCreatePost: true, canDeleteUser: false, ... }
store.commit('SET_PERMISSIONS', permissions);
然后在组件里直接用:
<template>
<button v-if="permissions.canDeleteUser" @click="handleDelete">删除用户</button>
</template>
<script>
export default {
computed: {
permissions() {
return this.$store.state.permissions;
}
}
}
</script>
或者在路由守卫里结合角色 + 能力:
if (to.meta.requiresPermission && !store.getters.hasPermission(to.meta.requiresPermission)) {
next('/403');
}
这种方案的好处是:前端不用关心权限逻辑怎么来的,只负责展示。后端可以根据用户角色、部门、甚至时间动态计算权限,前端无感。而且即使有人篡改前端代码,因为 API 层有校验,照样删不了数据。
这里注意我踩过好几次坑:一开始图省事,前端自己根据角色硬编码权限,结果后来加了个“临时审核员”角色,又要改前端。现在统一走接口,后端改配置就行,前端完全不用动。
那些看似聪明实则坑爹的做法
有些团队喜欢在前端用 Vuex 或 Context 存一个巨大的权限树,然后写一堆 hasPermission('user.delete', scope) 这样的函数。听起来很酷,但<strong实际维护起来简直是噩梦。
比如你传个 scope 参数表示资源 ID,那每次调用都得带上当前页面的数据上下文,很容易漏。而且一旦后端权限模型变了,前端一堆地方要同步改。
还有人尝试用 JSON Schema 定义权限规则,前端解析执行。折腾半天发现,性能差不说,调试起来根本找不到哪条规则没生效。最后还是乖乖回退到“后端返回布尔值”的朴素方案。
所以我的建议是:前端权限控制只做两件事:隐藏按钮、拦截路由。别试图在前端实现完整的权限引擎。
我的选型逻辑
总结一下我的决策流程:
- 如果是标准后台系统(如 CMS、ERP):直接 RBAC + 路由守卫,够用又省事。
- 需要细粒度控制(如 SaaS 多租户):后端计算权限,前端只消费布尔值。页面级用 RBAC 拦截,按钮级用
canXXX字段控制。 - 绝对不干的事:在前端写复杂权限逻辑、用 localStorage 存权限、绕过后端做权限判断。
另外,权限变更后记得刷新权限数据。我们之前有个 bug:用户升级为管理员后,页面没刷新,还是看不到管理菜单。后来加了个机制:关键操作(如登出、切换账号)后强制拉取最新权限。
最后一点真心话
访问控制看起来是个小模块,但一旦设计不好,后期改起来牵一发动全身。我见过太多项目前期随便搞搞,后期为了加个“部门隔离”把整个权限体系推倒重来。
所以我的原则是:宁可前期多花半天和后端对清楚权限模型,也不要前端自己瞎猜。权限这东西,前端越“傻”,系统越稳。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案,比如用 Casbin 前端集成或者 GraphQL 的权限字段设计,欢迎评论区交流!

暂无评论