前面有稍微提到了守衛(Guard),它的名稱非常的符合它的功能,它確實就是擋在門口的衛兵,進去跟出來都得經過它的檢查,或是你要把它想成海關也可以。
在實作專案中,會遇到的情況如下:
Angular Guards 就是為了處理這些「門禁控制」而設計的,它們在路由切換時執行檢查,決定是否允許使用者進入或離開特定頁面。
Class-based Guard 還是可以用,不過從 Angular 14+ 開始,推薦使用 Functional Guards 取代 Class-based Guards,原因如下:
情境描述:在一個購物網站中,使用者想查看「我的訂單」頁面,但如果沒有登入,應該先引導到登入頁面。
// 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;
};
情境描述:使用者在 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;
};
情境描述:在一個企業內部系統中,一般員工可以查看自己的薪資單,但只有人資主管才能進入「薪資管理」頁面查看所有人的薪資資料。不同角色需要不同的存取權限。
// 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'])] // 📊 營運數據分析
}
];
情境描述:在大型系統中,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);
})
);
};
情境描述:在醫療系統中,查看病患資料需要: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;
};
情境描述:使用者點擊「進入後台」按鈕後,系統需要向後端驗證權限,這個過程可能需要幾秒鐘。在等待期間顯示載入動畫,提升使用者體驗。
// 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())
);
};
情境描述:在電商網站進行系統升級時,一般使用者會看到維護頁面無法使用網站,但管理員仍可正常進入後台監控系統狀況,並在升級完成後進行測試,如測試無誤則結束維護模式,讓所有使用者恢復正常使用。這種機制常見於大型網站的版本更新或緊急修復期間。
程式流程上:當進入頁面的時候會呼叫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)
}
];
Q: Guard 是在前端執行的,是否足以保護敏感資料?
A: 不夠喔,Guard 主要用於改善使用者體驗,真正的安全防護必須在後端 API 進行。
Q: 多個 Guard 的執行順序是什麼?
A: 按照在 canActivate
陣列中的順序執行,任何一個返回 false
就會停止後續檢查。