权限设计不是单点技巧,而是一套“模型 → 同步 → 前端落点 → 后端兜底”的体系。本文结合实际项目,总结四种常见前端落点:路由守卫、按钮/组件权限、动态路由、接口权限控制,并给出各自优劣与实现要点。

一、先立模型:资源、动作与主体

  • 主体(Subject):用户、角色、组织、租户等。
  • 资源(Resource):页面、菜单、按钮、接口、文件等。
  • 动作(Action):view/create/update/delete/export…
  • 常见模型
    • RBAC:角色-权限映射,简单高效;粒度通常到菜单/按钮/接口。
    • ABAC:基于属性的策略更灵活(时间段、地理、部门级别等),实现与维护更复杂。

建议以 RBAC 起步,保留扩展位点(在权限项中附带资源/动作/条件)。前端仅做“展示与导航层过滤”,真正的安全必须后端校验。

二、路由权限控制(Route Guard)

— 页面级拦截入口。

  • 优点:统一把关,能有效阻止未授权页面访问;配合路由元信息,规则可读性强。
  • 局限:必须与后端权限同步,单靠前端不安全;需为每个受限路由显式声明。

1)Vue Router 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// router/permission.ts
import { router } from './router'
import { useAuthStore } from '@/stores/auth'

router.beforeEach((to, _from, next) => {
const auth = useAuthStore()
const requiresAuth = Boolean(to.meta?.requiresAuth)
if (!requiresAuth) return next()

if (!auth.user) {
return next({ name: 'login', query: { redirect: to.fullPath } })
}

const needRoles = (to.meta?.roles as string[] | undefined) ?? []
if (needRoles.length && !needRoles.some(r => auth.user!.roles.includes(r))) {
return next({ name: '403' })
}
next()
})

路由元信息示例:

1
2
3
4
5
6
{
path: '/admin',
name: 'admin',
component: () => import('@/pages/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] }
}

2)React Router 示例

1
2
3
4
5
6
7
8
9
10
11
12
import { Navigate } from 'react-router-dom'
import { useAuth } from './auth'

export function Guard({ roles, children }: { roles?: string[]; children: React.ReactNode }) {
const { user } = useAuth()
if (!user) return <Navigate to="/login" replace />
if (roles && !roles.some(r => user.roles.includes(r))) return <Navigate to="/403" replace />
return <>{children}</>
}

// 使用
// <Route path="/admin" element={<Guard roles={['admin']}><Admin /></Guard>} />

要点:路由层做“页面级”拦截,所有受限页面都需声明 meta.roles 或通过包装组件传入 roles

三、按钮/组件权限控制(Fine-grained UI)

— 细粒度的页面内显隐/禁用。

  • 优点:就地控制,用户体验最佳;权限变化可即时反映到交互元素。
  • 局限:需要在多个位置加判断,开发与维护成本较高;仅影响可见性,不提供真正的安全保证。

1)Vue 自定义指令

1
2
3
4
5
6
7
8
9
10
11
12
// directives/permission.ts
import { useAuthStore } from '@/stores/auth'

export default {
mounted(el: HTMLElement, binding: { value: string | string[] }) {
const auth = useAuthStore()
const need = ([] as string[]).concat(binding.value || [])
const has = need.some(code => auth.perms.includes(code))
if (!has) el.parentNode?.removeChild(el)
}
}
// <button v-permission="'user:delete'">删除用户</button>

2)React 组件封装

1
2
3
4
5
6
7
8
import { useAuth } from './auth'

export function Can({ perm, children }: { perm: string; children: React.ReactNode }) {
const { perms } = useAuth()
if (!perms?.includes(perm)) return null
return <>{children}</>
}
// <Can perm="user:delete"><Button danger>删除</Button></Can>

要点:UI 层只负责“显隐/禁用”,不要在前端做安全假设;真正的权限校验在后端接口。

四、动态路由加载(Login 后按权限注入)

— 登录后按权限注入路由与菜单。

  • 优点:减少无权限页面的代码与加载时间;与菜单/导航天然统一。
  • 局限:实现与状态同步更复杂,需要 404/403 兜底与缓存恢复策略。

1)Vue 动态注入

1
2
3
4
5
6
// 登录成功后
const { data: serverRoutes } = await api.get('/me/routes')
serverRoutes.forEach((r: any) => {
// 可按父子关系选择 addRoute(parentName, route)
router.addRoute(mapToVueRoute(r))
})

2)React Router 构建路由表

1
2
3
4
5
6
7
import { useMemo } from 'react'
import { useRoutes } from 'react-router-dom'

function AppRoutes({ rawRoutes }: { rawRoutes: any[] }) {
const routes = useMemo(() => buildRoutes(rawRoutes), [rawRoutes])
return useRoutes(routes)
}

要点:动态路由可减少无权限页面的打包体积与加载时间,但需要配合菜单构建、缓存恢复与 404/403 兜底处理。

五、接口权限控制(后端兜底、安全边界)

— 真正的安全边界在后端。

  • 优点:最安全、可审计、可细化到资源实例级;防止接口被恶意调用。
  • 局限:前端无法替代,需后端网关/服务配合与策略下发。

前端在请求中携带凭证(Cookie/JWT/Bearer token),后端网关/服务侧做鉴权与鉴定;前端只负责处理 401/403 与跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Axios 拦截器示例
axios.interceptors.request.use((cfg) => {
const token = getToken()
if (token) cfg.headers.Authorization = `Bearer ${token}`
return cfg
})

axios.interceptors.response.use(undefined, (err) => {
if (err.response?.status === 401) {
logout()
location.assign(`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`)
}
return Promise.reject(err)
})

后端建议:

  • 在 JWT 的 claims 中放入 roles/perms/tenant 等只读信息;关键写操作仍以服务端 ACL/Policy 为准。
  • 对敏感接口做二次校验(如资源拥有者校验、操作幂等签名、防重放)。

六、权限数据同步与一致性

  • 登录后获取 userInfo + roles + perms + menus/routes,并缓存到 Store;支持刷新恢复。
  • 变更时(角色变更、强制下线)通过推送/轮询刷新权限。
  • 前端每次进入受限页面二次校验(例如检查 token 是否过期、角色是否仍然匹配)。

七、统一的权限元信息约定

建议在路由/菜单项中统一描述权限:

1
2
3
4
5
type Meta = {
requiresAuth?: boolean
roles?: string[]
perms?: string[] // 细粒度按钮/接口能力声明(用于 UI 展示)
}

示例:

1
2
3
4
{
path: '/user/list',
meta: { requiresAuth: true, roles: ['admin','ops'], perms: ['user:list','user:export'] }
}

八、总结

  • 页面进入前由“路由守卫”兜底;页面内用“按钮/组件权限”做细粒度体验优化;配合“动态路由”减少无关代码;真正的安全由“接口权限控制”在后端完成。
  • 前端权限只负责“看得见/点得着”,不可替代后端鉴权;两端需共享统一的权限模型与数据。

Happy Coding!