很多人包括之前的我写前端权限,基本停在按钮的隐藏和路由的拦截等。
但如果只做到这里,本质上只是"写了一些 if 判断"。
那么前端权限系统到底在解决什么问题?
实际上,前端权限不只是安全系统,而是视图层的权限投影(Projection)
后端才是真正的"裁决者",前端只是"渲染结果",要攻击就攻后端,跟我前端无关(甩锅.png)。
这一点在很多实践里是共识:
- 前端负责 UI 显隐、交互优化
- 后端负责最终鉴权,大部分逻辑
如果前端越界去做"安全判断",那就是架构问题,而不是实现问题。
一、从模型开始:权限其实是一个函数
权限系统可以抽象成一个函数:
js
function canAccess(user, action, resource, context) {
return boolean
}
这其实就是经典模型:
text
Who + What + Resource + Condition → Allow / Deny
换句话说:
权限系统,本质是一个策略决策引擎(Policy Engine)
这点很多文章会提到,但很少有人继续往下拆。
二、RBAC:本质是"映射压缩"
RBAC(Role-Based Access Control)不是权限本身,而是:
一种"降维手段"
先看最原始模型:
text
User → Permission
如果直接这么做:
text
100 用户 × 100 权限 = 10000 条关系
这是不可维护的。
所以 RBAC 引入中间层:
text
User → Role → Permission
从数据结构角度看,本质就是:
js
const userRoles = ['admin']
const rolePermissions = {
admin: ['user:add', 'user:delete']
}
function getPermissions(userRoles) {
return userRoles.flatMap(r => rolePermissions[r])
}
RBAC 做的事情只有一个:
把 N×M 的关系压缩成 N+M
这也是为什么它在工程上非常好用
前端 RBAC 的真实实现
很多人写 RBAC,只写一句:
vue
v-if="permissions.includes('user:add')"
但一个完整实现,其实至少包含三层:
1. 权限初始化(登录阶段)
js
// store/modules/auth.js
state = {
roles: [],
permissions: []
}
async function fetchUserInfo() {
const res = await api.getUserInfo()
state.roles = res.roles
state.permissions = res.permissions
}
2. 路由过滤(结构控制)
js
function filterRoutes(routes, roles) {
return routes.filter(route => {
if (!route.meta?.roles) return true
return roles.some(r => route.meta.roles.includes(r))
})
}
配合:
js
router.beforeEach((to, from, next) => {
if (!hasRoutePermission(to)) {
next('/403')
} else {
next()
}
})
这是最典型的实现路径
3. 组件级权限(视图控制)
很多人直接写:
vue
<button v-if="permissions.includes('user:add')" />
但更工程化的写法是指令化:
js
app.directive('permission', {
mounted(el, binding) {
const { value } = binding
if (!store.permissions.includes(value)) {
el.parentNode?.removeChild(el)
}
}
})
使用:
vue
<button v-permission="'user:add'">新增</button>
4. 更进一步:统一入口(避免散落 if)
如果写过大型项目,会发现一个问题:
text
v-if="xxx" 到处都是
这时应该抽象:
js
function hasPermission(code) {
return store.permissions.includes(code)
}
甚至:
js
function can(action, resource) {
return permissionMap[resource]?.includes(action)
}
三、RBAC 的核心问题
它无法表达"条件"
例如:
text
只能编辑自己的数据
只能在工作时间操作
只能访问本部门数据
RBAC 做不到。
如果你强行用 RBAC:
text
editor_self
editor_department
editor_time_limited
这就是经典问题:
角色爆炸
四、ABAC:本质是"运行时求值"
ABAC(Attribute-Based Access Control)解决的不是结构问题,而是:
决策逻辑问题
从源码角度看,它其实就是:
js
function canAccess({ user, resource, env }) {
return (
user.role === 'finance' &&
resource.ownerId === user.id &&
env.time < 18
)
}
也就是说:
权限不再是"查表",而是"执行表达式"
示例:策略引擎(Policy Engine)
js
const policies = [
{
action: 'edit',
resource: 'order',
condition: (ctx) =>
ctx.user.id === ctx.resource.ownerId
},
{
action: 'delete',
resource: 'order',
condition: (ctx) =>
ctx.user.role === 'admin'
}
]
function can(ctx) {
return policies.some(p =>
p.action === ctx.action &&
p.resource === ctx.resource.type &&
p.condition(ctx)
)
}
五、为什么前端不能纯 ABAC
从理论上看,ABAC 很优雅。
但在前端,会出现三个问题:
1. 状态来源不完整
前端拿不到:
- 全量数据
- 实时上下文
2. 性能问题
js
列表 100 条 × 每条 ABAC 判断
成本很高。
3. 可维护性问题
js
condition: (ctx) => ...
如果散落在各个组件里:
完全不可控
六、真实做法:不是选模型,而是"分层执行"
我们不应该问:
用 RBAC 还是 ABAC?
而是:
在哪一层用哪种模型
一个典型分层
1. 路由层 → RBAC(结构)
js
meta: {
roles: ['admin']
}
决定:
能不能进入页面
2. 组件层 → RBAC(功能)
vue
<button v-permission="'user:add'" />
决定:
能不能看到按钮
3. 数据层 → ABAC(核心)
js
function canEdit(item) {
return item.ownerId === user.id
}
决定:
能不能操作数据
4. API 层 → 最终裁决
text
后端再次校验(必须存在)
七、很多人忽略的关键点:权限其实是"数据流问题"
你可以从另一个角度理解权限系统:
text
后端 → 返回权限数据
↓
前端 → 投影到 UI
这就是:
权限 = 状态 + 渲染
而不是:
权限 = 判断逻辑
八、再抽象一层:权限系统就是一个"解释器"
如果再往源码层抽象:
RBAC:
js
permission = lookup(role)
ABAC:
js
permission = eval(policy)
统一之后:
js
permission = resolve(policy, context)
这其实就是:
一个 DSL(权限描述语言)解释器
九、一个更工程化的最终形态
我们可以把整个权限系统收敛成这样:
js
// policy.js
export const policies = {
'user:add': (ctx) => ctx.user.role === 'admin',
'order:edit': (ctx) =>
ctx.user.id === ctx.resource.ownerId
}
// permission.js
export function can(key, ctx) {
const fn = policies[key]
return fn ? fn(ctx) : false
}
组件中:
html
<button v-if="can('order:edit', { user, resource: item })">
十、最后收敛
RBAC 解决的是"权限如何组织",
ABAC 解决的是"权限如何计算"。
而前端权限系统真正做的,是:
把后端的权限决策结果,稳定地映射到 UI 上
它不单是安全系统,而是:
一个受约束的策略执行 + 视图投影系统
实际上如果做过一些比较大的后台系统,就知道权限不单只有菜单,页面,按钮,还有接口访问,数据等等一系列细分的权限。