Vue 權限與路由守衛設計
後台系統裡,權限不是只寫一個 beforeEach。真正完整的權限設計通常包含:登入狀態、路由可訪問性、選單顯示、按鈕權限、API 權限、資料範圍、動態路由與安全邊界。
一句話回答:Vue 權限設計可以用 Router Guard 做登入攔截,用 route meta 描述路由權限,用 Pinia 保存使用者與權限資料,用動態路由或靜態路由過濾生成可訪問頁面;但前端權限只能改善體驗與控制入口,真正的安全必須由後端 API 權限與資料權限保證。
一、 必考觀念
1. 權限分很多層
常見權限層級:
| 層級 | 例子 | 前端能做什麼 |
|---|---|---|
| 登入權限 | 未登入不能進後台 | 路由守衛跳 login |
| 路由權限 | 不能進使用者管理頁 | 過濾路由、攔截導航 |
| 選單權限 | 不顯示某些 menu | 根據權限生成 menu |
| 按鈕權限 | 不能刪除、不能審核 | 隱藏或 disabled 按鈕 |
| API 權限 | 不能呼叫刪除 API | 後端驗證 |
| 資料權限 | 只能看自己部門資料 | 後端控制資料範圍 |
前端可以控制「看不看得到入口」,但不能當作安全邊界。
2. 前端權限不是安全本身
如果只是前端隱藏按鈕:
<button v-if="canDelete">
刪除
</button>使用者仍然可以直接呼叫 API。
所以資深回答一定要補:
前端權限主要是體驗與導航控制,後端仍要對每個 API 和資料範圍做權限驗證。
3. 權限設計要先定義權限模型
常見模型:
User -> Role -> Permission例如:
type Permission = 'user:list' | 'user:create' | 'user:delete'
type Role = {
id: string
name: string
permissions: Permission[]
}
type User = {
id: string
roles: Role[]
}前端通常拿到的是:
- 使用者資訊。
- 角色。
- 權限碼。
- 可訪問路由。
- 可顯示選單。
具體由後端回權限碼還是路由表,要看系統設計。
二、 登入攔截
1. 基本路由守衛
router.beforeEach(async to => {
const authStore = useAuthStore()
if (to.meta.public) {
return true
}
if (!authStore.isLogin) {
return {
path: '/login',
query: {
redirect: to.fullPath,
},
}
}
return true
})重點:
- public route 不需要登入。
- 未登入導到 login。
- 保留 redirect,登入後回原頁。
延伸讀:Vue 路由實現:Hash 路由與 History 路由原理。
2. 避免無限重導
常見錯誤是 login 頁也被守衛導回 login。
要明確標記:
const routes = [
{
path: '/login',
component: () => import('@/views/LoginPage.vue'),
meta: {
public: true,
},
},
]或在 guard 裡判斷:
if (to.path === '/login') {
return true
}3. 進入頁面前補使用者資料
使用者重新整理頁面時,store 可能還沒有 user profile,但 token 還在。
router.beforeEach(async to => {
const authStore = useAuthStore()
if (to.meta.public) {
return true
}
if (!authStore.token) {
return '/login'
}
if (!authStore.user) {
await authStore.fetchProfile()
}
return true
})要注意:
fetchProfile失敗要清 token 並回 login。- 多個導航同時觸發時要避免重複請求。
- loading 頁面要有體驗設計。
延伸讀:Vue 狀態管理:Pinia、Vuex 與狀態歸屬。
三、 Route Meta 權限
1. 用 meta 描述路由權限
const routes = [
{
path: '/users',
component: () => import('@/views/UserList.vue'),
meta: {
requiresAuth: true,
permissions: ['user:list'],
},
},
{
path: '/users/create',
component: () => import('@/views/UserCreate.vue'),
meta: {
requiresAuth: true,
permissions: ['user:create'],
},
},
]守衛中判斷:
router.beforeEach(to => {
const authStore = useAuthStore()
const requiredPermissions = to.meta.permissions as string[] | undefined
if (!requiredPermissions?.length) {
return true
}
const allowed = requiredPermissions.every(permission => {
return authStore.hasPermission(permission)
})
if (!allowed) {
return '/403'
}
return true
})2. meta 要型別化
可以擴充 Vue Router 的 RouteMeta:
declare module 'vue-router' {
interface RouteMeta {
public?: boolean
requiresAuth?: boolean
permissions?: string[]
roles?: string[]
title?: string
}
}這能避免 meta 欄位亂寫。
延伸讀:Vue 與 TypeScript 實戰。
3. roles 和 permissions 的差異
Role 是角色,例如:
admin
editor
viewerPermission 是具體能力,例如:
user:list
user:create
user:delete實務上通常更建議前端判斷 permission,而不是寫死 role。
原因是:
- 同一角色的權限可能變。
- 不同系統角色名稱可能不同。
- permission 更細粒度。
四、 動態路由
1. 靜態路由過濾
前端維護完整路由表,登入後根據權限過濾:
function filterRoutes(routes: AppRoute[], permissions: string[]) {
return routes
.filter(route => {
const required = route.meta?.permissions
if (!required?.length) {
return true
}
return required.every(permission => permissions.includes(permission))
})
.map(route => ({
...route,
children: route.children
? filterRoutes(route.children, permissions)
: undefined,
}))
}優點:
- 路由結構清楚。
- 前端可控。
- 型別與懶載入容易管理。
缺點:
- 每次新增頁面要發版。
- 權限配置彈性較低。
2. 後端返回路由配置
後端回:
[
{
"path": "/users",
"name": "UserList",
"component": "UserList",
"permissions": ["user:list"]
}
]前端映射 component:
const viewModules = import.meta.glob('../views/**/*.vue')
function resolveComponent(name: string) {
return viewModules[`../views/${name}.vue`]
}優點:
- 後端可配置菜單與路由。
- 不同租戶可有不同路由。
缺點:
- component mapping 要受控,不能任意載入。
- 型別較難。
- 前後端契約更複雜。
- 仍然不能取代後端 API 權限。
3. router.addRoute
登入後可以動態新增路由:
for (const route of allowedRoutes) {
router.addRoute(route)
}要注意:
- 重複 addRoute。
- 登出時清除動態路由。
- 404 路由順序。
- 首次進入目標路由時,路由還沒加完。
簡化處理:
if (!permissionStore.routesLoaded) {
await permissionStore.loadRoutes()
return to.fullPath
}重新返回 to.fullPath 可以讓 Router 用新加入的路由重新匹配。
五、 選單權限與按鈕權限
1. 選單由可訪問路由生成
常見方式:
const menus = computed(() => {
return allowedRoutes
.filter(route => !route.meta?.hidden)
.map(route => ({
title: route.meta?.title,
path: route.path,
icon: route.meta?.icon,
}))
})不要讓 menu 和 route 完全各自維護,否則很容易不同步。
2. 按鈕權限
Composable 寫法:
export function usePermission() {
const authStore = useAuthStore()
function can(permission: string) {
return authStore.permissions.includes(permission)
}
return {
can,
}
}使用:
<button v-if="can('user:delete')">
刪除
</button>Directive 寫法:
export const vPermission = {
mounted(el: HTMLElement, binding: { value: string }) {
const authStore = useAuthStore()
if (!authStore.permissions.includes(binding.value)) {
el.remove()
}
},
}Directive 適合簡單顯示控制;複雜互動通常用 Composable 更清楚。
延伸讀:Vue 復用手段選型:Composable、Directive、Plugin、Mixin。
3. disabled 還是 hide?
看業務:
| 做法 | 適合 |
|---|---|
| 隱藏 | 使用者不需要知道這個能力存在 |
| disabled | 使用者需要知道功能存在,但目前無權限或條件不足 |
| 顯示但提示 | 需要引導升級、申請權限 |
不要只用一種做法套全部。
六、 API 權限與資料權限
1. 前端攔截不能取代後端驗證
即使前端做了:
- 路由守衛。
- 選單過濾。
- 按鈕隱藏。
使用者仍然可以:
- 手動輸入 URL。
- 修改前端狀態。
- 直接呼叫 API。
所以每個敏感 API 都要後端驗證。
2. 資料權限通常由後端處理
例如:
- 只能看自己部門的訂單。
- 只能看自己建立的工單。
- 區域經理只能看某些地區資料。
這些不應該由前端 filter。
前端最多負責:
- 傳遞查詢條件。
- 顯示目前資料範圍。
- 根據後端回應呈現結果。
3. 401 和 403 要分清楚
| 狀態 | 意義 | 前端處理 |
|---|---|---|
| 401 | 未登入或登入過期 | 清狀態,導 login |
| 403 | 已登入但無權限 | 顯示 forbidden 或提示 |
不要把所有錯誤都導到 login。
七、 權限 Store 設計
1. Auth Store 和 Permission Store 可以分開
例如:
useAuthStore:token、user、login、logout
usePermissionStore:permissions、roles、allowedRoutes、menus好處:
- 登入狀態和授權資料分清楚。
- 權限刷新、路由生成可獨立管理。
- 測試更容易。
2. 權限要能刷新
權限可能改變:
- 管理員調整角色。
- 使用者登出再登入。
- token refresh。
- 租戶切換。
前端要能:
- 重新拉權限。
- 重建路由。
- 清除舊動態路由。
- 清除快取資料。
3. 登出要清乾淨
登出時要清:
- token。
- user。
- permissions。
- dynamic routes。
- menus。
- tabs / visited views。
- sensitive cache。
否則下一個使用者可能看到上一個使用者的狀態。
八、 追問題庫
Q1:Vue 後台權限怎麼做?
可以回答:
- 登入後取得 token、user、roles、permissions。
- Router Guard 根據登入狀態做攔截。
- 路由 meta 描述頁面需要的 permission。
- 根據 permissions 過濾路由或動態 addRoute。
- 根據可訪問路由生成 menu。
- 按鈕用
can(permission)或 directive 控制顯示。 - API 和資料權限由後端驗證。
Q2:動態路由是前端生成還是後端返回?
兩種都可以。
前端靜態路由過濾比較簡單、型別好、可維護;後端返回路由配置更靈活,但契約和 component mapping 更複雜。
不管哪種方式,後端 API 權限都不能省。
Q3:按鈕權限用 directive 還是 composable?
簡單顯示控制可以用 directive,例如 v-permission。
如果需要在邏輯裡判斷、組合條件、控制 disabled、顯示提示,用 composable can() 會更清楚。
Q4:權限資料放 localStorage 嗎?
要謹慎。
權限可能變更,長期存在 localStorage 容易過期。通常可以存 token,但 user/permissions 建議啟動時重新拉,或有明確失效策略。
敏感資料不要隨便持久化。
Q5:前端隱藏按鈕就安全了嗎?
不安全。
前端隱藏按鈕只是體驗。真正安全要靠後端 API 權限、資料權限和審計。
九、 實作題
題目:設計一個後台權限流程
需求:
- 未登入跳 login。
- 登入後拉 user 與 permissions。
- 根據 permissions 生成路由和 menu。
- 無權限進入 403。
- 按鈕可以判斷
can('user:delete')。
可以這樣設計:
router.beforeEach(async to => {
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
if (to.meta.public) {
return true
}
if (!authStore.token) {
return {
path: '/login',
query: {
redirect: to.fullPath,
},
}
}
if (!authStore.user) {
await authStore.fetchProfile()
}
if (!permissionStore.loaded) {
await permissionStore.loadPermissions()
await permissionStore.buildRoutes()
return to.fullPath
}
if (!permissionStore.canAccessRoute(to)) {
return '/403'
}
return true
})面試補充:
- 要處理 fetchProfile 失敗。
- 要避免重複 build routes。
- 登出要清除動態路由與權限。
- API 權限要後端驗證。
- 401/403 要分開處理。
十、 資深視角
1. 權限是前後端共同設計
前端負責:
- 導航體驗。
- menu 顯示。
- 按鈕顯示。
- 無權限頁面提示。
- 權限狀態管理。
後端負責:
- token 驗證。
- API 權限。
- 資料權限。
- 權限配置。
- 審計與安全。
資深回答不能只停在 Router Guard。
2. 權限模型要能演進
一開始可能只有 role:
admin / user後面可能會變成:
- RBAC。
- 資料權限。
- 租戶權限。
- 臨時授權。
- 功能開關。
- 審批流狀態權限。
如果前端到處寫死 role === 'admin',後面會很難改。
3. 權限要能觀測和除錯
大型後台常見問題是:「為什麼我看不到這個菜單?」
可以提供:
- 當前使用者 roles。
- 當前 permissions。
- route 需要哪些 permission。
- menu 來源。
- API 403 的錯誤碼。
這些對內部系統維護很有幫助。
總結
| 問題 | 回答重點 |
|---|---|
| 登入攔截 | Router Guard + token/user 狀態 |
| 路由權限 | route meta 或動態路由 |
| 選單權限 | 根據可訪問路由或後端 menu 生成 |
| 按鈕權限 | can(permission) 或 v-permission |
| API 權限 | 必須由後端驗證 |
| 資料權限 | 後端控制資料範圍 |
| 資深取捨 | 前端管體驗,後端管安全 |
一句完整的面試回答可以是:
Vue 後台權限我會分層設計。登入層用 Router Guard 判斷 token 和 user,未登入跳 login 並保留 redirect;路由層用 route meta 描述 permissions,或登入後根據後端權限動態 addRoute;選單根據可訪問路由生成;按鈕可以用
can(permission)或v-permission控制顯示。Auth Store 管 token/user,Permission Store 管 roles、permissions、allowedRoutes 和 menus。401 和 403 要分開處理,登出時要清掉權限、動態路由與敏感快取。最重要的是前端權限只是體驗與入口控制,API 權限和資料權限必須由後端保證。