前端如何来做权限管理?
Category(分类): Browser, Security Status: 已更新
原文只保留了一个权限管理资料链接。这里补齐原文想讨论的“角色、页面、菜单、按钮和接口权限”内容,并补充现代 Web 应用中必须明确的边界:前端权限控制主要改善用户体验,真正的授权必须由服务端在每一次受保护请求中执行。
原文资料:前端如何来做权限管理?
一、先区分认证和授权
很多文章把“登录”和“权限”混在一起,但它们解决的是两个问题:
- 认证(Authentication,AuthN):你是谁?例如账号密码、Passkey、企业 SSO 登录。
- 授权(Authorization,AuthZ):你可以对哪个资源执行什么操作?例如用户能否编辑某篇文章、管理员能否导出报表。
登录成功只代表服务端识别了主体(subject),不代表主体可以访问所有资源。一次授权判断通常至少包含:
主体 subject + 操作 action + 资源 resource + 上下文 context
例如:
用户 alice + update + 文章 42 + 所属租户 tenant-a
授权结果应该由服务端依据可信数据和策略计算,不能由浏览器传来的 role=admin、隐藏按钮或路由参数决定。
二、权限模型:从角色到策略
1. RBAC:基于角色的访问控制
RBAC(Role-Based Access Control)适合组织结构比较稳定的后台系统:
用户(User) -> 角色(Role) -> 权限(Permission)
alice -> editor -> article:read
article:create
article:update
一个权限最好是稳定的动作和资源组合,而不是把大量页面 URL 当成权限本身:
article:read
article:update
article:delete
report:export
user:invite
RBAC 的优点是管理简单、容易审计;缺点是角色数量可能随业务条件爆炸。例如“只能编辑自己部门、工作日、指定地区的文章”,仅靠角色很难表达。
2. ABAC:基于属性的访问控制
ABAC(Attribute-Based Access Control)根据主体、资源、动作和环境属性判断:
允许:
subject.department == resource.department
&& subject.permissions 包含 "article:update"
&& resource.status != "locked"
ABAC 更灵活,但策略设计、调试、审计和性能成本更高。实际项目常采用“RBAC 分配基础权限 + ABAC 处理资源归属和上下文”的组合。
3. 资源关系和租户边界
协作产品还可能使用 ReBAC(Relationship-Based Access Control):
用户 alice 是文档 42 所属项目的 editor
多租户系统必须把 tenantId、组织、项目和资源归属纳入授权判断。只在前端切换租户下拉框、只在 URL 中传 tenantId 都不能构成安全隔离。
三、前端权限通常分为四层
| 层级 | 前端可以做什么 | 服务端必须做什么 |
|---|---|---|
| 页面/路由 | 未登录跳转登录,未授权展示 403 页面 | 校验页面对应 API 和资源权限 |
| 菜单 | 根据权限隐藏或禁用导航项 | 不因菜单不可见就放开接口 |
| 按钮/操作 | 隐藏“删除”“导出”等操作,减少误操作 | 再次校验具体动作 |
| 数据/字段 | 隐藏不应显示的列和敏感字段 | 查询时过滤字段,禁止越权读取 |
隐藏按钮不是安全措施。 用户可以修改 JavaScript、直接调用 API、重放请求或绕过前端路由。OWASP 将访问控制列为服务端必须执行的安全职责,前端检查只能作为 UX 层的提前提示。
四、推荐的整体流程
- 用户通过登录、Passkey、企业 SSO 或 OAuth/OIDC 完成认证;
- 服务端创建会话或签发符合协议的令牌;
- 浏览器访问
/api/me,获取当前用户的非敏感资料、租户和展示用权限摘要; - 前端据此生成菜单、路由提示和按钮状态;
- 每个业务 API 在服务端重新加载主体、资源和策略并做授权;
- 权限变更、登出、封禁或会话失效后,服务端立即拒绝后续请求;
- 前端遇到
401重新认证,遇到403展示无权限,而不是无限重试。
示例响应可以是:
{
"user": {
"id": "u_42",
"displayName": "Ada",
"tenantId": "tenant-a"
},
"permissions": [
"article:read",
"article:update"
],
"policyVersion": 12
}
权限列表只用于前端展示,不应包含密码、私钥、完整会话令牌或“前端相信即可”的管理员标志。复杂资源权限应由服务端通过 API 返回可执行能力,或在业务接口中直接判断。
五、Vue Router 中的路由守卫
路由守卫可以改善导航体验,但不能替代 API 授权:
const routes = [
{
path: '/articles',
component: () => import('./pages/Articles.vue'),
meta: { requiresAuth: true, permission: 'article:read' }
},
{
path: '/articles/new',
component: () => import('./pages/ArticleEditor.vue'),
meta: { requiresAuth: true, permission: 'article:create' }
}
]
router.beforeEach(async to => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return {
name: 'login',
query: { redirect: to.fullPath }
}
}
if (to.meta.permission && !auth.can(to.meta.permission)) {
return { name: 'forbidden' }
}
})
注意事项:
- 路由
meta是客户端代码,不能存放秘密,也不能作为后端权限配置的唯一来源; - 认证状态初始化完成前,不要因为默认值
false把已登录用户误导向登录页; redirect参数必须限制为站内路径,避免开放重定向;- 懒加载组件失败、会话过期和权限变更都要有明确的错误状态;
- 如果权限变更后仍在旧页面,应让下一次 API 请求返回
403,并刷新权限摘要。
六、菜单和按钮权限
1. 组件函数
<script setup lang="ts">
const auth = useAuthStore()
</script>
<template>
<button
v-if="auth.can('article:delete')"
type="button"
@click="deleteArticle"
>
删除文章
</button>
</template>
更复杂的场景可以封装 Can 组件或 v-permission 指令,但它们本质上都是展示层逻辑。不要因为按钮被 v-if 移除,就认为接口安全。
2. 动态菜单和路由
如果菜单由服务端返回,前端仍要:
- 校验返回数据的结构,避免把任意字符串当作组件名或 HTML 执行;
- 使用本地允许的路由映射,不根据服务端字符串动态执行任意代码;
- 对无法匹配的路径显示 404,而不是任意
import(); - 在服务器端继续校验每个菜单所代表的业务 API。
const routeComponents = {
articles: () => import('./pages/Articles.vue'),
reports: () => import('./pages/Reports.vue')
} as const
for (const item of menuFromServer) {
const component = routeComponents[item.key as keyof typeof routeComponents]
if (!component) continue
router.addRoute({
name: item.key,
path: item.path,
component,
meta: { permission: item.permission }
})
}
动态注册路由要处理重复注册、刷新恢复、登出清理和权限变化。它解决的是导航组织问题,不是授权问题。
七、服务端授权的最小示例
下面是 Express 风格的示意代码。真实项目应使用成熟的会话中间件、数据库事务、策略引擎和集中日志:
const requirePermission = permission => async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ code: 'UNAUTHENTICATED' })
}
const allowed = await policy.can({
subject: req.user,
action: permission,
resource: req.resource
})
if (!allowed) {
return res.status(403).json({ code: 'FORBIDDEN' })
}
return next()
}
app.patch(
'/api/articles/:id',
requireAuthenticated,
loadArticle,
requirePermission('article:update'),
updateArticle
)
状态码语义通常是:
401 Unauthorized:当前请求没有通过认证,通常没有有效会话或令牌;403 Forbidden:服务器识别了主体,但该主体没有执行操作的权限;404 Not Found:某些资源系统会对无权资源故意返回 404,避免泄露资源是否存在;这应作为整体信息泄露策略设计,而不是机械替换所有 403。
最容易被忽略的是对象级越权(IDOR/BOLA):
// 错误:只根据 URL id 查询并返回
const unsafeArticle = await db.article.findById(req.params.id)
// 正确思路:查询和租户/主体约束一起执行
const article = await db.article.findFirst({
where: {
id: req.params.id,
tenantId: req.user.tenantId,
// 或通过策略确认 req.user 对该资源有 read/update 权限
}
})
不要先查询全部数据再在浏览器过滤;敏感字段和不属于当前租户的数据根本不应出现在响应中。
八、Cookie、Token 和 CSRF 的关系
权限管理经常和登录凭证一起讨论,但凭证放在哪里不是授权模型本身:
- 服务端会话常把不可猜的 session ID 放在
HttpOnly; Secure; SameSite=Lax/StrictCookie 中; - Cookie 会自动随符合规则的请求发送,因此需要 CSRF 防护;
- 把访问令牌放在
localStorage可以避免浏览器自动发送,但 XSS 能读取它,不能因此称为“绝对安全”; - HttpOnly 也不能消除 XSS,攻击脚本仍可能借助当前页面发起已认证操作;
- 跨站请求、CORS、SameSite 和 CSRF Token 应按具体部署组合设计。
前端权限控制不能靠“把 Token 放到更隐蔽的变量”解决。凭证生命周期、撤销、轮换、权限变更和异常会话都需要服务端策略。
九、常见错误和改进
错误 1:只做前端路由拦截
攻击者可以直接请求 /api/admin/users。改进:每个服务端接口都做认证、资源加载和授权,并默认拒绝。
错误 2:相信前端传来的角色
role=admin、isAdmin=true、前端 localStorage 中的权限都可被修改。改进:角色和权限来自服务端可信存储,服务端从会话或令牌确认主体。
错误 3:把菜单当作权限系统
菜单只是导航,不应决定数据库查询范围。改进:资源归属、动作和租户约束放进服务端策略。
错误 4:权限变更只刷新浏览器
用户被撤销权限后,旧页面可能仍显示按钮。改进:服务端立即拒绝;前端处理 401/403,并使权限摘要失效。
错误 5:把“加密前端权限配置”当成保护
浏览器必须拿到用于展示的代码和数据,用户可以查看或调试它。加密配置无法替代服务端授权;真正的秘密不应下发到浏览器。
错误 6:忽略审计和高风险操作
删除、导出、转账、修改权限等操作应记录主体、资源、结果、请求 ID、时间和必要的风险信号;高风险操作可要求重新认证、二次确认或 WebAuthn。
十、权限系统设计清单
- 默认拒绝,采用最小权限原则;
- 认证、授权、资源归属和租户隔离分开设计;
- 前端检查只服务于 UX,服务端检查才是安全边界;
- 使用稳定的权限命名和策略版本,避免散落字符串;
- 对角色、权限、资源和策略变更建立审计记录;
- 防范对象级越权、批量赋值、路径穿越、开放重定向和 CSRF;
- 不在前端暴露密钥、私钥、数据库凭据或完整敏感用户资料;
- 会话过期、撤销、登出、并发登录和权限变更有明确行为;
- 对
401和403做不同处理,不要无限重试; - 用自动化授权测试覆盖允许、拒绝、跨租户和资源所有权场景;
- 对高风险操作实施重新认证、二次审批或不可抵赖的审计机制。
总结
- 认证回答“你是谁”,授权回答“你能对哪个资源做什么”;
- RBAC 适合稳定的角色体系,ABAC/ReBAC 适合资源归属和上下文更复杂的业务;
- 页面、菜单、按钮和前端路由都可以做体验层提示,但不能作为安全边界;
- 服务端必须在每个受保护请求中验证主体、动作、资源和租户;
- Cookie、Token、SSO 和 OAuth 是身份凭证或认证协议,不会自动替代授权策略;
- 默认拒绝、最小权限、对象级授权、审计和撤销能力是可维护权限系统的基础。