iT邦幫忙

2025 iThome 鐵人賽

DAY 13
2
Modern Web

Angular 進階實務 30天系列 第 13

Day 13:Router - Angular Guard

  • 分享至 

  • xImage
  •  

什麼是 Guard?為什麼需要它?

前面有稍微提到了守衛(Guard),它的名稱非常的符合它的功能,它確實就是擋在門口的衛兵,進去跟出來都得經過它的檢查,或是你要把它想成海關也可以。

在實作專案中,會遇到的情況如下:

  • 📱 社交媒體:只有登入的使用者才能查看個人資料頁
  • 🏪 電商網站:只有管理員能進入商品管理後台
  • 📝 線上編輯器:離開編輯頁面前,提醒使用者保存未完成的文章
  • 🏥 醫療系統:不同科別的醫生只能查看自己權限範圍內的病患資料

Angular Guards 就是為了處理這些「門禁控制」而設計的,它們在路由切換時執行檢查,決定是否允許使用者進入或離開特定頁面。

為什麼使用 Functional Guards?

Class-based Guard 還是可以用,不過從 Angular 14+ 開始,推薦使用 Functional Guards 取代 Class-based Guards,原因如下:

  • 更簡潔的程式碼
  • 更好的 Tree Shaking
  • 更容易測試
  • 符合現代 Angular 開發趨勢

最常用的三種場景

1. 登入檢查 (90% 的專案都會用到)

情境描述:在一個購物網站中,使用者想查看「我的訂單」頁面,但如果沒有登入,應該先引導到登入頁面。

// guards/auth.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  }

  // 可選:保存原本要前往的路徑
  router.navigate(['/login'], {
    queryParams: { returnUrl: router.url }
  });
  return false;
};

2. 防止使用者誤離頁面

情境描述:使用者在 it邦幫忙 後台寫了一篇長文,寫到一半時不小心點到瀏覽器的上一頁按鈕,或是誤觸了其他連結。沒有保護機制的話,辛苦打的文字就(可能)會全部消失,我不敢嘗試,有悲劇過的朋友歡迎分享。

或是你幫你的使用者做很長很長表單的時候,有些人並不是那麼熟悉科技,如果不多設幾道防線,可能會導致種種悲劇。

// guards/form.guard.ts
import { CanDeactivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { NzModalService } from 'ng-zorro-antd/modal';
import { map } from 'rxjs/operators';

// 定義組件必須實作的介面
export interface CanComponentDeactivate {
 hasUnsavedChanges(): boolean;
}

export const formGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
 if (component.hasUnsavedChanges && component.hasUnsavedChanges()) {
   return confirm('有未保存的內容,確定要離開嗎?');
 }
 return true;
};

// 進階版:使用 ng-zorro Modal
export const formGuardWithModal: CanDeactivateFn<CanComponentDeactivate> = (component) => {
 if (component.hasUnsavedChanges && component.hasUnsavedChanges()) {
   const modal = inject(NzModalService);
   
   return modal.confirm({
     nzTitle: '確認離開',
     nzContent: '有未保存的內容,確定要離開嗎?',
     nzOkText: '確定離開',
     nzOkType: 'primary',
     nzOkDanger: true,
     nzCancelText: '取消',
     nzClosable: false,
     nzMaskClosable: false,
     nzIconType: 'exclamation-circle'
   }).afterClose.pipe(
     map(result => !!result)
   );
 }
 return true;
};

3. 權限檢查 (管理系統常用)

情境描述:在一個企業內部系統中,一般員工可以查看自己的薪資單,但只有人資主管才能進入「薪資管理」頁面查看所有人的薪資資料。不同角色需要不同的存取權限。

// guards/admin.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

export const adminGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const user = authService.getCurrentUser();

  return user?.role === 'admin';
};

// 更靈活的權限檢查
export const roleGuard = (requiredRoles: string[]): CanActivateFn => {
  return () => {
    const authService = inject(AuthService);
    const user = authService.getCurrentUser();

    return user ? requiredRoles.includes(user.role) : false;
  };
};

實際使用方式 - 以電商網站為例

在路由中設定

情境描述:以下是一個電商網站的路由設計,包含了:

  • 🛒 會員專區:需要登入才能查看個人資料和訂單
  • ✏️ 商品評價:需要登入,離開前檢查是否有未送出的評價
  • 👑 管理後台:需要管理員權限
  • 📊 營運數據:需要管理員或經理權限
// app.routes.ts (Angular 17 standalone 寫法)
import { Routes } from '@angular/router';
import { authGuard, formGuard, adminGuard, roleGuard } from './guards';

export const routes: Routes = [
  {
    path: 'profile',
    loadComponent: () => import('./components/profile.component').then(c => c.ProfileComponent),
    canActivate: [authGuard]  // 👤 會員個人資料頁面
  },
  {
    path: 'write-review',
    loadComponent: () => import('./components/write-review.component').then(c => c.WriteReviewComponent),
    canActivate: [authGuard],
    canDeactivate: [formGuard]  // ✏️ 撰寫商品評價頁面
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(r => r.adminRoutes),
    canActivate: [authGuard, adminGuard]  // 👑 管理員後台
  },
  {
    path: 'analytics',
    loadComponent: () => import('./components/analytics.component').then(c => c.AnalyticsComponent),
    canActivate: [authGuard, roleGuard(['admin', 'manager'])]  // 📊 營運數據分析
  }
];

進階技巧 - 實際應用場景

1. 非同步檢查 (API 驗證)

情境描述:在大型系統中,Token 可能在伺服器端被撤銷(如管理員封鎖帳號),需要即時向後端驗證使用者狀態,而不只是檢查本地 Token。

// guards/async-auth.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

export const asyncAuthGuard: CanActivateFn = () => {
  const http = inject(HttpClient);
  const router = inject(Router);

  return http.get<{ valid: boolean }>('/api/check-auth').pipe(
    map(response => {
      if (response.valid) {
        return true;
      } else {
        router.navigate(['/login']);
        return false;
      }
    }),
    catchError((error) => {
      console.error('驗證失敗:', error);
      router.navigate(['/login']);
      return of(false);
    })
  );
};

2. 組合多個 Guards

情境描述:在醫療系統中,查看病患資料需要:1) 已登入 2) 具備醫護身分 3) 有權限查看該科別病患資料。多重檢查確保資料安全。

// guards/compound.guard.ts
export const protectedRouteGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  // 檢查登入狀態
  if (!authService.isLoggedIn()) {
    router.navigate(['/login']);
    return false;
  }

  // 檢查特定權限
  const requiredPermission = route.data?.['permission'];
  if (requiredPermission && !authService.hasPermission(requiredPermission)) {
    router.navigate(['/unauthorized']);
    return false;
  }

  return true;
};

3. 載入狀態處理

情境描述:使用者點擊「進入後台」按鈕後,系統需要向後端驗證權限,這個過程可能需要幾秒鐘。在等待期間顯示載入動畫,提升使用者體驗。

// guards/loading-auth.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { LoadingService } from '../services/loading.service';
import { finalize } from 'rxjs/operators';

export const loadingAuthGuard: CanActivateFn = () => {
  const http = inject(HttpClient);
  const router = inject(Router);
  const loadingService = inject(LoadingService);

  loadingService.show();

  return http.get<{ valid: boolean }>('/api/check-auth').pipe(
    map(response => {
      if (response.valid) {
        return true;
      } else {
        router.navigate(['/login']);
        return false;
      }
    }),
    catchError(() => {
      router.navigate(['/login']);
      return of(false);
    }),
    finalize(() => loadingService.hide())
  );
};

綜合實作範例 - 維護公告

維護守衛 MaintenanceGuard

情境描述:在電商網站進行系統升級時,一般使用者會看到維護頁面無法使用網站,但管理員仍可正常進入後台監控系統狀況,並在升級完成後進行測試,如測試無誤則結束維護模式,讓所有使用者恢復正常使用。這種機制常見於大型網站的版本更新或緊急修復期間。

程式流程上:當進入頁面的時候會呼叫API確認身分跟維護日期,如果不是管理者身分就會出現維護中的畫面,管理者的話就可以進入並查看,也可以終止維護時間

先編寫守衛邏輯

// guards/maintenance.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { Router } from '@angular/router';

interface MaintenanceStatus {
 isUnderMaintenance: boolean;
 maintenanceEndTime: string | null;
 userRole: 'admin' | 'user';
}

export const maintenanceGuard: CanActivateFn = () => {
 const http = inject(HttpClient);
 const router = inject(Router);

 return http.get<MaintenanceStatus>('/api/system/maintenance-status').pipe(
   map(response => {
     if (!response.isUnderMaintenance || response.userRole === 'admin') {
       return true;
     }
     router.navigate(['/maintenance']);
     return false;
   }),
   catchError(() => {
     router.navigate(['/maintenance']);
     return of(false);
   })
 );
};

設計維護頁面

// components/maintenance.component.ts
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzResultModule } from 'ng-zorro-antd/result';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-maintenance',
  standalone: true,
  imports: [CommonModule, NzResultModule, NzButtonModule],
  template: `
    <nz-result 
      nzStatus="warning" 
      nzTitle="系統維護中" 
      nzSubTitle="系統正在進行維護,請稍後再試">
      
      <div nz-result-extra *ngIf="isAdmin">
        <button nz-button nzType="primary" nzDanger 
                (click)="endMaintenance()" 
                [nzLoading]="loading">
          結束維護
        </button>
      </div>
      
      <div nz-result-extra *ngIf="!isAdmin">
        <button nz-button (click)="checkStatus()" [nzLoading]="loading">
          重新檢查
        </button>
      </div>
    </nz-result>
  `
})
export class MaintenanceComponent {
  private http = inject(HttpClient);
  isAdmin = false;
  loading = false;

  ngOnInit() {
    this.checkAdminStatus();
  }

  checkAdminStatus() {
    this.http.get<{userRole: string}>('/api/auth/user-info').subscribe({
      next: (response) => this.isAdmin = response.userRole === 'admin',
      error: () => this.isAdmin = false
    });
  }

  endMaintenance() {
    this.loading = true;
    this.http.post('/api/system/end-maintenance', {}).subscribe({
      next: () => window.location.href = '/',
      error: () => this.loading = false
    });
  }

  checkStatus() {
    this.loading = true;
    this.http.get<{isUnderMaintenance: boolean}>('/api/system/maintenance-status').subscribe({
      next: (response) => {
        if (!response.isUnderMaintenance) window.location.href = '/';
        this.loading = false;
      },
      error: () => this.loading = false
    });
  }
}

設定守衛

// 路由設定
export const routes: Routes = [
  {
    path: 'admin/dashboard',
    loadComponent: () => import('./admin/dashboard.component').then(c => c.DashboardComponent),
    canActivate: [maintenanceGuard]
  },
  {
    path: 'maintenance',
    loadComponent: () => import('./components/maintenance.component').then(c => c.MaintenanceComponent)
  }
];

總結

🔐 安全性考量

  1. 前端驗證不是萬能:Guard 只能防止一般使用者的誤操作,真正的安全檢查必須在後端進行
  2. 敏感資料保護:即使有 Guard 保護,敏感 API 仍需後端權限驗證
  3. Token 過期處理:定期檢查並更新 Token,避免使用者在操作中途被登出

💡 使用者體驗

  1. 載入狀態:非同步驗證時提供視覺回饋
  2. 友善提示:拒絕存取時給予明確的錯誤訊息和解決方案
  3. 保存工作進度:表單 Guard 搭配自動儲存功能

🛠️ 開發維護

  1. 優先使用 Functional Guards - 更簡潔、更現代
  2. 善用 inject() 函數 - 取代構造函數注入
  3. 適當的錯誤處理 - 特別是非同步 Guards
  4. 可重用性 - 建立可參數化的 Guards

常見問題 Q&A

Q: Guard 是在前端執行的,是否足以保護敏感資料?
A: 不夠喔,Guard 主要用於改善使用者體驗,真正的安全防護必須在後端 API 進行。

Q: 多個 Guard 的執行順序是什麼?
A: 按照在 canActivate 陣列中的順序執行,任何一個返回 false 就會停止後續檢查。


上一篇
Day 12:Router - 從點擊到頁面載入的完整生命週期
下一篇
Day 14:Angular 路由轉場動畫
系列文
Angular 進階實務 30天18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言