跳至主要內容
Skip to content

Vue 權限與路由守衛設計

後台系統裡,權限不是只寫一個 beforeEach。真正完整的權限設計通常包含:登入狀態、路由可訪問性、選單顯示、按鈕權限、API 權限、資料範圍、動態路由與安全邊界。

一句話回答:Vue 權限設計可以用 Router Guard 做登入攔截,用 route meta 描述路由權限,用 Pinia 保存使用者與權限資料,用動態路由或靜態路由過濾生成可訪問頁面;但前端權限只能改善體驗與控制入口,真正的安全必須由後端 API 權限與資料權限保證。


一、 必考觀念

1. 權限分很多層

常見權限層級:

層級例子前端能做什麼
登入權限未登入不能進後台路由守衛跳 login
路由權限不能進使用者管理頁過濾路由、攔截導航
選單權限不顯示某些 menu根據權限生成 menu
按鈕權限不能刪除、不能審核隱藏或 disabled 按鈕
API 權限不能呼叫刪除 API後端驗證
資料權限只能看自己部門資料後端控制資料範圍

前端可以控制「看不看得到入口」,但不能當作安全邊界。

2. 前端權限不是安全本身

如果只是前端隱藏按鈕:

vue
<button v-if="canDelete">
  刪除
</button>

使用者仍然可以直接呼叫 API。

所以資深回答一定要補:

前端權限主要是體驗與導航控制,後端仍要對每個 API 和資料範圍做權限驗證。

3. 權限設計要先定義權限模型

常見模型:

text
User -> Role -> Permission

例如:

typescript
type Permission = 'user:list' | 'user:create' | 'user:delete'

type Role = {
  id: string
  name: string
  permissions: Permission[]
}

type User = {
  id: string
  roles: Role[]
}

前端通常拿到的是:

  • 使用者資訊。
  • 角色。
  • 權限碼。
  • 可訪問路由。
  • 可顯示選單。

具體由後端回權限碼還是路由表,要看系統設計。


二、 登入攔截

1. 基本路由守衛

typescript
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。

要明確標記:

typescript
const routes = [
  {
    path: '/login',
    component: () => import('@/views/LoginPage.vue'),
    meta: {
      public: true,
    },
  },
]

或在 guard 裡判斷:

typescript
if (to.path === '/login') {
  return true
}

3. 進入頁面前補使用者資料

使用者重新整理頁面時,store 可能還沒有 user profile,但 token 還在。

typescript
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 描述路由權限

typescript
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'],
    },
  },
]

守衛中判斷:

typescript
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

typescript
declare module 'vue-router' {
  interface RouteMeta {
    public?: boolean
    requiresAuth?: boolean
    permissions?: string[]
    roles?: string[]
    title?: string
  }
}

這能避免 meta 欄位亂寫。

延伸讀:Vue 與 TypeScript 實戰

3. roles 和 permissions 的差異

Role 是角色,例如:

text
admin
editor
viewer

Permission 是具體能力,例如:

text
user:list
user:create
user:delete

實務上通常更建議前端判斷 permission,而不是寫死 role。

原因是:

  • 同一角色的權限可能變。
  • 不同系統角色名稱可能不同。
  • permission 更細粒度。

四、 動態路由

1. 靜態路由過濾

前端維護完整路由表,登入後根據權限過濾:

typescript
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. 後端返回路由配置

後端回:

json
[
  {
    "path": "/users",
    "name": "UserList",
    "component": "UserList",
    "permissions": ["user:list"]
  }
]

前端映射 component:

typescript
const viewModules = import.meta.glob('../views/**/*.vue')

function resolveComponent(name: string) {
  return viewModules[`../views/${name}.vue`]
}

優點:

  • 後端可配置菜單與路由。
  • 不同租戶可有不同路由。

缺點:

  • component mapping 要受控,不能任意載入。
  • 型別較難。
  • 前後端契約更複雜。
  • 仍然不能取代後端 API 權限。

3. router.addRoute

登入後可以動態新增路由:

typescript
for (const route of allowedRoutes) {
  router.addRoute(route)
}

要注意:

  • 重複 addRoute。
  • 登出時清除動態路由。
  • 404 路由順序。
  • 首次進入目標路由時,路由還沒加完。

簡化處理:

typescript
if (!permissionStore.routesLoaded) {
  await permissionStore.loadRoutes()
  return to.fullPath
}

重新返回 to.fullPath 可以讓 Router 用新加入的路由重新匹配。


五、 選單權限與按鈕權限

1. 選單由可訪問路由生成

常見方式:

typescript
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 寫法:

typescript
export function usePermission() {
  const authStore = useAuthStore()

  function can(permission: string) {
    return authStore.permissions.includes(permission)
  }

  return {
    can,
  }
}

使用:

vue
<button v-if="can('user:delete')">
  刪除
</button>

Directive 寫法:

typescript
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 可以分開

例如:

text
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 後台權限怎麼做?

可以回答:

  1. 登入後取得 token、user、roles、permissions。
  2. Router Guard 根據登入狀態做攔截。
  3. 路由 meta 描述頁面需要的 permission。
  4. 根據 permissions 過濾路由或動態 addRoute。
  5. 根據可訪問路由生成 menu。
  6. 按鈕用 can(permission) 或 directive 控制顯示。
  7. 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')

可以這樣設計:

typescript
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:

text
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 權限和資料權限必須由後端保證。

延伸閱讀