آموزش انگولار – جلسه ۱۸: Route Guards و CanActivate در Angular 17
· mytag_6166
در این جلسه از دوره آموزش Angular 17 قصد داریم با یکی از مهمترین قابلیتهای Angular Router یعنی Route Guards در Angular آشنا شویم.
در جلسات قبل یاد گرفتیم چگونه با استفاده از RouterLink و RouterOutlet بین صفحات جابهجا شویم، پارامترهای URL را دریافت کنیم و از Child Routes و Lazy Loading استفاده کنیم.
اما یک سؤال مهم وجود دارد: آیا همه کاربران باید بتوانند به تمام صفحات برنامه دسترسی داشته باشند؟
فرض کنید در پروژه فروشگاهی خودمان یک صفحه مدیریت کاربران یا پنل ادمین داریم. طبیعتاً نمیخواهیم کاربری که وارد حسابش نشده بتواند به این صفحات دسترسی پیدا کند.
برای مثال، مسیرهای زیر را در نظر بگیرید:
/login
/dashboard
/admin
/users
صفحه Login باید برای کاربران مهمان در دسترس باشد؛ اما مسیرهای Dashboard و Admin نیاز به بررسی وضعیت ورود و سطح دسترسی دارند.
اینجاست که Route Guard وارد عمل میشود.
در ادامه یاد میگیریم Route Guard چیست، چه انواعی دارد و چگونه با استفاده از Functional Guards در Angular 17.3 از مسیرهای پروژه محافظت کنیم.
Route Guard در Angular چیست؟
Route Guard قابلیتی در Angular Router است که به ما اجازه میدهد هنگام مسیریابی، شرایط مشخصی را بررسی کنیم.
به زبان ساده، Guard مانند یک نگهبان عمل میکند.
وقتی کاربر قصد ورود به یک صفحه را دارد، Angular میتواند قبل از فعالسازی آن مسیر، تابع Guard مربوط به آن را اجرا کند.
برای مثال:
- اگر کاربر وارد حساب شده باشد، اجازه ورود به Dashboard را بدهیم.
- اگر کاربر وارد حساب نشده باشد، او را به صفحه Login هدایت کنیم.
- اگر کاربر نقش Admin نداشته باشد، دسترسی او به پنل مدیریت را محدود کنیم.
- اگر کاربر فرمی با تغییرات ذخیرهنشده دارد، قبل از خروج از صفحه از او تأیید بگیریم.
البته باید توجه داشت که Route Guard بهتنهایی یک مکانیزم امنیتی کامل نیست و کنترل دسترسی اصلی باید در Backend انجام شود.
انواع Route Guards در Angular
Angular چند نوع Guard در اختیار ما قرار میدهد که هرکدام کاربرد متفاوتی دارند.
| Guard | کاربرد |
|---|---|
| CanActivate | بررسی اجازه ورود به یک مسیر |
| CanActivateChild | بررسی اجازه ورود به مسیرهای فرزند |
| CanDeactivate | بررسی اجازه خروج از یک صفحه |
| CanMatch | تعیین امکان انتخاب یک Route در مرحله تطبیق مسیر |
در این جلسه تمرکز اصلی ما روی CanActivate و Functional Guard است. در ادامه با CanActivateChild و CanMatch نیز آشنا میشویم.
CanActivate در Angular چیست؟
CanActivate برای بررسی اجازه فعالسازی یک مسیر استفاده میشود.
فرض کنید مسیر زیر را داریم:
http://localhost:4200/dashboard
میخواهیم قبل از ورود کاربر به این صفحه بررسی کنیم که آیا کاربر لاگین کرده است یا خیر.
اگر لاگین کرده باشد، مسیر فعال شود. در غیر این صورت کاربر به صفحه Login منتقل شود.
برای این کار یک Guard میسازیم.
ساخت Functional Guard در Angular 17
در Angular 17 میتوانیم Guardها را به صورت تابع تعریف کنیم.
این روش با معماری Standalone هماهنگ است و بهجای پیادهسازی کلاسهای قدیمی CanActivate از نوع CanActivateFn استفاده میکند.
برای ساخت Guard میتوانیم از Angular CLI کمک بگیریم:
ng generate guard core/guards/auth
یا به صورت کوتاه:
ng g guard core/guards/auth
هنگام ساخت Guard، نوع CanActivate را انتخاب میکنیم.
بعد از ایجاد فایل، میتوانیم منطق موردنظر را داخل آن بنویسیم.
ساختار پیشنهادی پروژه:
src/app/
├── core/
│ ├── guards/
│ │ ├── auth.guard.ts
│ │ └── role.guard.ts
│ └── services/
│ └── auth.service.ts
├── pages/
│ ├── login/
│ ├── dashboard/
│ ├── admin/
│ └── forbidden/
└── app.routes.ts
ساخت AuthService برای مدیریت وضعیت ورود
قبل از پیادهسازی Guard، به یک سرویس نیاز داریم که وضعیت ورود کاربر را مشخص کند.
این همان مفهومی است که در جلسات Service و Dependency Injection آموزش دادیم.
برای ساده نگه داشتن مثال، فعلاً ورود کاربر را شبیهسازی میکنیم.
فایل auth.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loggedIn = false;
private role: 'user' | 'admin' = 'user';
login(role: 'user' | 'admin' = 'user'): void {
this.loggedIn = true;
this.role = role;
}
logout(): void {
this.loggedIn = false;
}
isLoggedIn(): boolean {
return this.loggedIn;
}
hasRole(requiredRole: string): boolean {
return this.isLoggedIn() && this.role === requiredRole;
}
}
توضیح AuthService
در این سرویس دو متغیر خصوصی داریم:
private loggedIn = false;
private role: 'user' | 'admin' = 'user';
متغیر loggedIn مشخص میکند که کاربر وارد حساب شده یا نه.
متغیر role نقش کاربر را نگه میدارد.
متد login() وضعیت ورود و نقش کاربر را تغییر میدهد.
متد logout() وضعیت ورود را غیرفعال میکند.
متد isLoggedIn() بررسی میکند که کاربر لاگین کرده است یا خیر.
و در نهایت متد hasRole() بررسی میکند که کاربر وارد حساب شده و نقش موردنیاز را دارد یا خیر.
نکته: این سرویس فقط برای آموزش است. اطلاعات ورود صرفاً در حافظه نگه داشته میشوند و با بارگذاری مجدد صفحه از بین میروند. در پروژه واقعی باید احراز هویت به Backend و مدیریت Session یا Token متصل شود.
ساخت AuthGuard با CanActivateFn
حالا نوبت پیادهسازی Guard است.
فایل auth.guard.ts:
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: {
returnUrl: state.url
}
});
};
در این مثال چند مفهوم مهم وجود دارد که در ادامه بررسی میکنیم.
CanActivateFn چیست؟
export const authGuard: CanActivateFn = (route, state) => {
CanActivateFn نوع یک تابع Guard در Angular Router است.
این تابع هنگام بررسی فعالسازی مسیر اجرا میشود و میتواند با توجه به شرایط برنامه، اجازه ادامه مسیر را بدهد یا مسیریابی را متوقف و هدایت کند.
استفاده از inject در Functional Guard
const authService = inject(AuthService);
const router = inject(Router);
از آنجایی که Guard ما یک تابع است، با استفاده از inject() سرویسهای موردنیاز را از سیستم Dependency Injection دریافت میکنیم.
در این مثال:
AuthService برای بررسی وضعیت ورود و Router برای ساخت مسیر تغییر جهت استفاده میشود.
خروجی true در CanActivate
if (authService.isLoggedIn()) {
return true;
}
اگر کاربر لاگین کرده باشد، Guard مقدار true برمیگرداند و Angular میتواند مسیر را فعال کند.
انتقال کاربر به Login با UrlTree
return router.createUrlTree(['/login'], {
queryParams: {
returnUrl: state.url
}
});
اگر کاربر لاگین نکرده باشد، یک UrlTree برمیگردانیم.
Angular با دریافت این مقدار، مسیریابی فعلی را لغو کرده و انتقال به آدرس Login را آغاز میکند.
در اینجا از returnUrl هم استفاده کردیم تا آدرس صفحهای که کاربر قصد ورود به آن را داشت، نگه داریم.
مثلاً:
/dashboard
به این آدرس تبدیل میشود:
/login?returnUrl=%2Fdashboard
این قسمت ارتباط مستقیمی با مبحث Query Parameters در جلسه قبل دارد.
چرا از UrlTree بهجای router.navigate استفاده کنیم؟
ممکن است این سؤال پیش بیاید که چرا داخل Guard مستقیماً از navigate() استفاده نکردیم.
در Guard بهتر است نتیجه مسیریابی را به Router برگردانیم.
به همین دلیل به جای این الگو:
router.navigate(['/login']);
return false;
مینویسیم:
return router.createUrlTree(['/login']);
این روش باعث میشود Angular Router خودش عملیات تغییر مسیر را مدیریت کند.
ثبت AuthGuard در app.routes.ts
بعد از ساخت Guard باید مشخص کنیم که روی کدام Route اجرا شود.
فایل app.routes.ts:
import { Routes } from '@angular/router';
import { LoginComponent } from './pages/login/login.component';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { authGuard } from './core/guards/auth.guard';
export const routes: Routes = [
{
path: 'login',
component: LoginComponent
},
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
}
];
در اینجا:
canActivate: [authGuard]
مشخص میکند که قبل از ورود به صفحه Dashboard باید تابع authGuard بررسی شود.
اگر مقدار true برگردد، ورود مجاز خواهد بود.
اگر UrlTree برگردد، کاربر به مسیر مشخصشده منتقل میشود.
برای اجرای این مثال فرض میکنیم LoginComponent و DashboardComponent را به صورت Standalone ساختهایم.
پیادهسازی صفحه Login برای تست Guard
برای اینکه عملکرد Guard را به صورت عملی بررسی کنیم، یک صفحه Login ساده میسازیم.
ng g c pages/login --standalone
فایل login.component.ts:
import { Component, inject } from '@angular/core';
import {
ActivatedRoute,
Router
} from '@angular/router';
import { AuthService } from '../../core/services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {
private authService = inject(AuthService);
private router = inject(Router);
private route = inject(ActivatedRoute);
login(role: 'user' | 'admin'): void {
this.authService.login(role);
const returnUrl =
this.route.snapshot.queryParamMap.get('returnUrl');
const isInternalUrl =
returnUrl?.startsWith('/') &&
!returnUrl.startsWith('//');
const destination = isInternalUrl
? returnUrl!
: '/dashboard';
this.router.navigateByUrl(destination);
}
}
فایل login.component.html:
<h2>Login Page</h2>
<button (click)="login('user')">
Login as User
</button>
<button (click)="login('admin')">
Login as Admin
</button>
در این مثال دو دکمه داریم.
دکمه اول ورود را با نقش User شبیهسازی میکند.
دکمه دوم ورود را با نقش Admin انجام میدهد.
پس از ورود، اگر Query Parameter مربوط به returnUrl وجود داشته باشد و یک مسیر داخلی معتبر در قالب مورد انتظار باشد، به همان مسیر منتقل میشویم؛ در غیر این صورت Dashboard باز میشود.
در پروژه واقعی، مقصد بازگشت باید با قواعد امنیتی مناسب اعتبارسنجی شود و عملیات Login نیز باید توسط سرور تأیید شود.
تست عملکرد CanActivate
حالا برنامه را اجرا کنید:
ng serve
و آدرس زیر را در مرورگر باز کنید:
http://localhost:4200/dashboard
چون مقدار اولیه loggedIn برابر با false است، Guard اجازه ورود نمیدهد.
در نتیجه به صفحه Login منتقل میشویم.
بعد از کلیک روی Login as User، مقدار loggedIn برابر true میشود و امکان ورود به Dashboard فراهم خواهد شد.
این دقیقاً همان کاری است که از یک AuthGuard انتظار داریم.
پیادهسازی Role Guard در Angular
تا اینجا فقط وضعیت ورود کاربر را بررسی کردیم.
اما در پروژههای واقعی گاهی لاگین بودن کافی نیست.
مثلاً میخواهیم تنها کاربران دارای نقش Admin بتوانند وارد صفحه مدیریت شوند.
برای این کار میتوانیم یک Role Guard بسازیم.
فایل role.guard.ts:
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService } from '../services/auth.service';
export const roleGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isLoggedIn()) {
return router.createUrlTree(['/login'], {
queryParams: {
returnUrl: state.url
}
});
}
const requiredRole = route.data['role'] as string;
if (authService.hasRole(requiredRole)) {
return true;
}
return router.createUrlTree(['/forbidden']);
};
Route Data چیست؟
در Angular میتوانیم اطلاعات ثابتی را داخل تنظیمات Route تعریف کنیم.
مثلاً:
{
path: 'admin',
component: AdminComponent,
canActivate: [roleGuard],
data: {
role: 'admin'
}
}
در این مثال مشخص کردهایم که برای ورود به صفحه Admin، نقش admin موردنیاز است.
داخل Guard این مقدار را میخوانیم:
const requiredRole = route.data['role'] as string;
سپس بررسی میکنیم:
authService.hasRole(requiredRole)
اگر نقش کاربر مناسب باشد، اجازه ورود داده میشود.
در غیر این صورت او را به مسیر /forbidden منتقل میکنیم.
برای این مسیر میتوانید یک کامپوننت ساده به نام ForbiddenComponent بسازید که پیام عدم دسترسی را نمایش دهد.
اضافه کردن Role Guard به مسیرها
نمونه تکمیلشده app.routes.ts:
import { Routes } from '@angular/router';
import { LoginComponent } from './pages/login/login.component';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { AdminComponent } from './pages/admin/admin.component';
import { ForbiddenComponent } from './pages/forbidden/forbidden.component';
import { authGuard } from './core/guards/auth.guard';
import { roleGuard } from './core/guards/role.guard';
export const routes: Routes = [
{
path: 'login',
component: LoginComponent
},
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
},
{
path: 'admin',
component: AdminComponent,
canActivate: [roleGuard],
data: {
role: 'admin'
}
},
{
path: 'forbidden',
component: ForbiddenComponent
}
];
در این مرحله میتوانیم سه وضعیت را تست کنیم.
| وضعیت کاربر | مسیر | نتیجه |
|---|---|---|
| لاگین نکرده | /dashboard | انتقال به Login |
| لاگین با نقش User | /dashboard | اجازه ورود |
| لاگین با نقش User | /admin | انتقال به Forbidden |
| لاگین با نقش Admin | /admin | اجازه ورود |
این مثال نشان میدهد که Guardها علاوه بر بررسی Login، برای کنترل دسترسی مبتنی بر نقش نیز کاربرد دارند.
CanActivateChild در Angular چیست؟
در جلسه قبل با Child Routes آشنا شدیم.
گاهی یک Route والد داریم که چندین مسیر فرزند داخل آن تعریف شده است.
مثلاً:
/users
/users/1
/users/2
اگر بخواهیم از تمام مسیرهای فرزند محافظت کنیم، میتوانیم از CanActivateChild استفاده کنیم.
ابتدا Guard مربوطه را میسازیم:
import { inject } from '@angular/core';
import {
CanActivateChildFn,
Router
} from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authChildGuard: CanActivateChildFn = (
route,
state
) => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isLoggedIn()
? true
: router.createUrlTree(['/login'], {
queryParams: {
returnUrl: state.url
}
});
};
حالا آن را روی مسیر والد قرار میدهیم:
{
path: 'users',
canActivateChild: [authChildGuard],
children: [
{
path: '',
component: UserListComponent
},
{
path: ':id',
component: UserCardComponent
}
]
}
در این مثال، مسیرهای فرزند از طریق authChildGuard محافظت میشوند.
این قابلیت برای ساخت پنلهای مدیریتی با چندین صفحه کاربرد زیادی دارد.
اگر بخواهیم خود مسیر والد و کامپوننت آن را نیز کنترل کنیم، باید تنظیمات Guard را متناسب با ساختار آن مسیر در نظر بگیریم.
تفاوت CanActivate و CanMatch در Angular
یکی دیگر از Guardهای مهم Angular، CanMatch است.
در جلسه قبل درباره Lazy Loading صحبت کردیم. حالا میتوانیم ارتباط این مفهوم با CanMatch را بررسی کنیم.
CanActivate هنگام بررسی فعالسازی Route اجرا میشود.
اما CanMatch مشخص میکند آیا یک Route هنگام بررسی آدرس میتواند انتخاب شود یا خیر.
برای مثال:
{
path: 'admin',
canMatch: [adminMatchGuard],
loadComponent: () =>
import('./pages/admin/admin.component')
.then(c => c.AdminComponent)
}
در اینجا adminMatchGuard از نوع CanMatchFn خواهد بود.
نکته مهم این است که اگر CanMatch مقدار false برگرداند، Angular میتواند سراغ سایر Routeهای منطبق بر آدرس برود. بنابراین این رفتار با CanActivate کاملاً یکسان نیست.
برای شروع مدیریت دسترسی صفحات، معمولاً CanActivate مثال سادهتر و واضحتری است.
CanDeactivate چیست؟
برخلاف CanActivate که ورود به یک مسیر را بررسی میکند، CanDeactivate برای کنترل خروج از یک صفحه استفاده میشود.
فرض کنید کاربر وارد صفحه ویرایش پروفایل شده و اطلاعات فرم را تغییر داده است.
اگر قبل از ذخیره اطلاعات بخواهد صفحه را ترک کند، میتوانیم یک پیام تأیید نمایش دهیم.
مثلاً:
تغییرات شما ذخیره نشده است.
آیا مطمئن هستید که میخواهید از صفحه خارج شوید؟
برای این سناریو از CanDeactivateFn استفاده میکنیم.
در پروژههایی که Reactive Forms دارند، این قابلیت بسیار مفید است.
آیا Route Guard باعث امنیت کامل برنامه میشود؟
خیر.
این نکته یکی از مهمترین مواردی است که هنگام استفاده از Guard باید بدانیم.
Route Guard در سمت Angular اجرا میشود و وظیفه اصلی آن کنترل فرآیند مسیریابی و بهبود تجربه کاربری است.
اما کاربر میتواند درخواست HTTP را مستقیماً برای Backend ارسال کند، حتی اگر Angular اجازه ورود به یک صفحه را ندهد.
بنابراین کنترل دسترسی واقعی باید در Backend انجام شود.
برای مثال اگر API زیر مخصوص مدیر سیستم است:
GET /api/admin/users
سرور باید مستقل از Angular بررسی کند که درخواستکننده مجوز دریافت اطلاعات را دارد یا خیر.
در نتیجه:
Route Guard جایگزین Authentication و Authorization در سمت سرور نیست.
نکته مهم درباره Route Guards و SSR در Angular 17
اگر پروژه شما از Server-Side Rendering یا SSR استفاده میکند، باید هنگام پیادهسازی سرویس احراز هویت دقت کنید.
در SSR، بخشی از کد Angular ممکن است در محیط سرور اجرا شود.
به همین دلیل نمیتوان بدون بررسی محیط اجرا مستقیماً از APIهای مخصوص مرورگر مانند localStorage استفاده کرد.
همچنین وضعیت Login در یک پروژه SSR واقعی باید به شکلی مدیریت شود که سمت سرور بتواند وضعیت نشست کاربر را به صورت امن تشخیص دهد؛ برای مثال از طریق Cookieهای مناسب و اعتبارسنجی سمت سرور.
مثال AuthService این جلسه برای آموزش مفهوم Guard نوشته شده و یک سیستم احراز هویت کامل SSR نیست.
اشتباهات رایج هنگام استفاده از Route Guards
فراموش کردن ثبت Guard
ساختن فایل Guard کافی نیست.
باید آن را داخل تنظیمات Route قرار دهیم:
canActivate: [authGuard]
استفاده از localStorage بدون توجه به SSR
اگر پروژه SSR داشته باشد، دسترسی مستقیم به localStorage در زمان اجرای سمت سرور میتواند باعث خطا شود.
استفاده از Guard بهعنوان تنها لایه امنیت
Guard صرفاً محافظ مسیریابی سمت برنامه است. Backend باید دسترسی کاربران را مستقلاً بررسی کند.
استفاده از مسیر تغییر جهت نامناسب
اگر Guard کاربر را به مسیری هدایت کند که خودش دوباره همان Guard را فعال میکند، ممکن است حلقه تغییر مسیر ایجاد شود.
بنابراین بهتر است صفحات Login، Forbidden و سایر مسیرهای عمومی با دقت تنظیم شوند.
سوالات متداول درباره Route Guards در Angular
Route Guard در Angular چه کاربردی دارد؟
Route Guard برای کنترل فرآیند مسیریابی استفاده میشود. با آن میتوانیم شرایط ورود یا خروج از یک صفحه را بررسی کنیم و در صورت نیاز کاربر را به مسیر دیگری منتقل کنیم.
CanActivate در Angular چیست؟
CanActivate یکی از قابلیتهای Angular Router است که پیش از فعالسازی یک مسیر، اجازه دسترسی کاربر را بررسی میکند.
تفاوت AuthGuard و CanActivate چیست؟
AuthGuard معمولاً نامی است که توسعهدهندگان برای Guard مربوط به احراز هویت انتخاب میکنند. CanActivate نوع قابلیت Angular Router برای کنترل فعالسازی مسیر است.
Functional Guard چیست؟
Functional Guard روشی برای تعریف Guard به شکل تابع است. در Angular 17 میتوانیم از انواعی مانند CanActivateFn به همراه inject() استفاده کنیم.
آیا میتوان چند Guard را برای یک Route تعریف کرد؟
بله. میتوان چند Guard را داخل آرایه canActivate قرار داد. برای ادامه مسیریابی لازم است شرایط تمام Guardهای مربوطه تأیید شوند. در Guardهای غیرهمزمان نباید فرض کنیم یکی حتماً قبل از شروع دیگری تمام میشود.
تفاوت CanActivate و CanActivateChild چیست؟
CanActivate برای کنترل فعالسازی یک Route استفاده میشود؛ اما CanActivateChild برای کنترل ورود به مسیرهای فرزند کاربرد دارد.
آیا Route Guard در Angular 17 با Standalone Components سازگار است؟
بله. میتوانیم Functional Guard را تعریف کنیم و مستقیماً در فایل app.routes.ts روی مسیرهای موردنظر قرار دهیم.
جمعبندی آموزش Route Guards و CanActivate در Angular 17
در این جلسه از دوره آموزش انگولار یاد گرفتیم چگونه با استفاده از Route Guards در Angular فرآیند دسترسی به صفحات مختلف برنامه را کنترل کنیم.
ابتدا با مفهوم Route Guard و انواع آن آشنا شدیم.
سپس با استفاده از CanActivateFn یک AuthGuard ساختیم تا فقط کاربران واردشده بتوانند صفحه Dashboard را مشاهده کنند.
در ادامه یاد گرفتیم چگونه با Role Guard، دسترسی به پنل مدیریت را براساس نقش کاربر محدود کنیم.
همچنین کاربردهای CanActivateChild، CanMatch و CanDeactivate را بررسی کردیم و درباره اهمیت اعتبارسنجی دسترسی در Backend صحبت کردیم.
با یادگیری این مفاهیم میتوانیم ساختار مسیریابی پروژههای Angular را حرفهایتر طراحی کنیم و برای بخشهای مختلف برنامه، قوانین دسترسی مشخصی داشته باشیم.
دسترسی به ویدیوهای کامل دوره
برای مشاهده ویدیوهای کامل هر جلسه، دریافت تمرینها، فایلها و پشتیبانی، میتوانید دوره کامل را تهیه کنید.