Vue Router 導航守衛:守護您的應用程式頁面安全
在開發網頁應用時,我們常需要限制某些頁面的存取權限。導航守衛 (Navigation Guards) 就如同網頁的保安或海關,當使用者試圖切換路徑時,守衛會攔下請求並執行檢查,例如:檢查登入狀態、驗證權限,最後決定放行、取消,或是改派到其他頁面。
核心觀念:全域前置守衛 beforeEach
最常用的導航守衛是 router.beforeEach。它會在導航觸發後、進入目標頁面元件前被呼叫。您可以透過 to 參數取得目標路由的資訊。常用的情境如下:
- 驗證使用者是否已登入(Token 檢查)。
- 檢查使用者是否有特定職務權限(Role-based Access Control)。
- 頁面切換時的數據預載或進度條顯示。
前端守衛不等於安全
導航守衛保護的是 使用體驗,不是資料。前端的任何判斷都能被修改,真正的權限必須由 後端在每一支 API 驗證。守衛的價值在於讓使用者不會進到一個註定失敗的頁面。
檔案架構與內容
我們將新增一個 Login.vue 頁面來進行使用者登入。
src/
├─ router/
│ └─ index.js
├─ views/
│ ├─ member/
│ │ ├─ Profile.vue
│ │ └─ Orders.vue
│ ├─ Member.vue
│ ├─ Login.vue # 使用者登入
│ └─ ...
├─ App.vue
└─ main.js實作步驟:定義需要權限的路由
改寫程式碼
在導航守衛加入之前,我們需要改寫一下程式碼,讓之後的應用更方便。
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
略...
});
export default router;先把 createRouter 的結果 存成變數再匯出,之後才有 router 這個實體可以掛上守衛。這也是與 靜態路由 那篇直接 export default createRouter({...}) 的唯一差別。
指定需要定義權限的路由
首先,我們在路由表中使用 meta 欄位來標記哪些路徑需要經過驗證。將需要加入導航守衛的路由,設定 meta: { requiresAuth: true }。
略...
const router = createRouter({
略...,
routes: [
略...,
{
path: '/member',
component: () => import('@/views/Member.vue'),
meta: { requiresAuth: true },
children: [ 略... ],
},
略...
],
});
略...TIP
如果您有 巢狀路由,to.meta 會自動合併父層與子層的 meta 標籤,這讓您只需要在父層路由定義 requiresAuth: true,底下所有的子路由都會受到保護。
撰寫攔截邏輯
利用 router.beforeEach 檢查目標路由是否包含 requiresAuth,並確認 localStorage 中是否存在 token。
略...
const router = createRouter({
略...,
});
router.beforeEach((to) => {
// 1. 檢查目標路由是否需要登入權限
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token');
// 2. 如果有 token,放行進入頁面
if (token) {
return true;
}
// 3. 無 token,強制重新導向至登入頁
return {
path: '/login',
};
}
return true;
});
略...重要觀念
在實務專案中,僅檢查 token 是否存在是不夠的,通常還會呼叫 API 驗證 token 的有效性或解析 JWT 過期時間。
小心無限重導向
如果 登入頁自己也被標記了 requiresAuth,未登入時導向登入頁又會被守衛攔下,再導向登入頁,於是陷入無窮迴圈。除了確認登入頁沒有加上標記,也建議在導向前先判斷目標是不是已經是登入頁。
進階:登入後回到原本想去的頁面
使用者原本要看訂單卻被攔到登入頁,登入完卻被丟回首頁,體驗並不好。做法是把 原路徑記在 query 裡:
router.beforeEach((to) => {
if (!to.meta.requiresAuth) return true;
const token = localStorage.getItem('token');
if (token) return true;
// 把原本要去的完整路徑帶上,登入後才知道該回哪裡
return {
path: '/login',
query: { redirect: to.fullPath },
};
});登入成功後再讀出來跳回去,沒有這個參數時就走預設首頁:
const route = useRoute();
const router = useRouter();
function onLoginSuccess() {
router.push(route.query.redirect || '/');
}route 與 router 的分工可以參考 動態路由。
登入頁面實作
在登入元件中,當使用者成功登入後,我們將 token 存入瀏覽器儲存空間,並引導使用者前往受保護的頁面。
Login.vue
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
function setToken() {
localStorage.setItem("token", "user_access_token_sample");
router.push("/member");
}
</script>
<template>
<h1>Login</h1>
<input type="button" value="點我模擬登入" @click="setToken">
</template>實務上登入狀態通常不只存在 localStorage,也會同步到 Pinia 的 store,讓導覽列與各頁面都能直接讀取使用者資訊。
導航守衛的傳回值說明
在 Vue Router 4 中,beforeEach 的傳回值決定了導航的去向:
true:放行導航。false:取消導航,停留在目前頁面。{ path: '/login' }:路由物件,往指定的路徑。
不用再寫 next 了
Vue Router 3 時代習慣呼叫 next(),在 Vue Router 4 已改為 回傳值決定結果,不回傳任何值等同放行。兩種寫法混用容易造成導航卡住或執行兩次,新專案請統一使用回傳值。
與 Axios 攔截器的分工
守衛與 Axios 攔截器 常被誤認為重複,實際上是兩道不同時機的防線:
| 時機 | 負責的人 | 處理什麼 |
|---|---|---|
| 進入頁面前 | 導航守衛 | 沒有憑證就別讓使用者看到空白頁 |
| API 回應後 | Axios 攔截器 | Token 用到一半過期,清除憑證並導回登入 |
兩者一起使用,才能同時顧到 進得去 與 用得下去。
總結
透過 路由 Meta 設定 與 導航守衛 的結合,能以極精簡的程式碼管理整站的頁面存取權限。這不僅能防止未授權使用者進入敏感頁面,更能大幅提升 Web App 的使用者體驗。
常見問題
beforeEach 的回傳值代表什麼?
回傳 true 或不回傳任何值表示放行,false 表示取消導航停在原頁,回傳路由物件則改導向指定頁面。Vue Router 4 已不需要再呼叫 next。
為什麼守衛會造成無限重導向?
通常是登入頁本身也被標記為需要驗證,未登入時導向登入頁又被守衛攔下再導向登入頁。請確認登入頁沒有 requiresAuth,並在導向前判斷目標是否已是登入頁。
子路由需要每一頁都寫 meta 嗎?
不需要。to.meta 會自動合併父層與子層的設定,在父路由寫一次 requiresAuth 就能保護底下所有子頁面。
登入後要怎麼回到原本想去的頁面?
導向登入頁時把原路徑放進 query,例如:redirect 參數,登入成功後再讀出來 push 回去。沒有該參數時就導向預設首頁。
只檢查前端的 token 就安全了嗎?
不安全。導航守衛只是使用體驗上的攔截,任何人都能修改前端狀態。真正的權限必須由後端在每支 API 驗證,前端守衛只負責避免使用者看到無法使用的畫面。
導航守衛和 Axios 攔截器該用哪一個?
兩者互補。守衛在進入頁面前檢查,避免白畫面;Axios 攔截器 處理 API 回應 401 的情況,例如:Token 在使用中過期時清除憑證並導回登入頁。