Trong một ứng dụng web, chúng ta thường đối mặt với kịch bản một số page cho phép tất cả mọi người truy cập, ngược lại một số khác chỉ dành cho các user đã login vào hệ thống như admin system chẳng hạn. Để giải quyết vấn đề này chúng ta hoàn toàn có thể thực hiện dễ dàng trên server side. Tuy nhiên, nếu chúng ta xây dựng Single page application(SPA) như Angular thì phải làm thế nào. Các bạn đều biết, với SPA thì client side thường độc lập hoàn toàn với server, do vậy chúng ta cũng cần cơ chế để handle việc bảo vệ routes. Bài viết này tôi sẽ giới thiệu với các bạn về Guard trong angular và cách sử dụng nó.

1. Guard Types

Có bốn loại guards khác nhau chúng ta có thể sử dụng để bảo về routes của mình:

  • CanActivate: Quyết định việc một route được kich hoạt.
  • CanActivateChild: Quyết định việc children routes được kich hoạt
  • CanDeactivate: Quyết định việc một route hủy kích hoạt
  • CanLoad: Quyết định một module được lazy loading

Phụ thuộc trên cái gì chúng ta muốn làm, chúng ta có thể cần triển khai một hoặc một số guards nhất định, hoặc cũng có thể định nghĩa tất cả chúng. Hãy theo dõi cách làm thế nào để định nghĩa guards.

2. Định nghĩa Guards

Guards có thể được triển khai trong những cách khác nhau, nhưng sau tất cả nó sẽ là là function trả về một trong những kiểu sau Observable<boolean>, Promise<boolean> hoặc boolean

2.1. Kiểu function

Để đăng kí một guard chúng ta cần định nghĩa một token và guard function. Dưới đây là một định nghĩa cực kì đơn giản cho guard:

@NgModule({
  ...
  providers: [
    provide: 'CanActivateGuard',
    useValue: () => {
      return true;
    }
  ],
  ...
})
export class AppModule {}

Ở trên là định nghĩa một guard mà luôn trả về true, vì vậy chúng ta có thể include nó tới providers của AppModule luôn. Khi đã cấu hình nó trong AppModule chúng ta có thể sử dụng guard trong route config. Cấu hình route bên dưới có CanActivateGuard được đính kèm:

export const AppRoutes:RouterConfig = [
  { 
    path: '',
    component: SomeComponent,
    canActivate: ['CanActivateGuard']
  }
];

Như chúng ta có thể thấy, việc chúng ta cần làm là định nghĩa các guard tokens được gọi đến. Điều này cũng có nghĩa là chúng có thể triển khai nhiều guards để bảo vệ một route. Guards được thực thi theo thứ tự mà chúng được khai báo trên route.

2.2. Kiểu class

Đôi khi, một guard cần tương thích với dependency injection. Trong trường hợp này, nó có ý nghĩa để xác định guard như một class, bởi vì dependencies sau đó có thể đơn giản để injected. Chúng ta muốn để bảo vệ một route và user đã xác thực trước tiên. Chúng ta có thể muốn inject một AuthService để xác định liệu user đã xác thực hoặc chưa. Một class guard sẽ là lựa chọn hoàn hảo cho các vấn đề này.

Khi tạo một guard class, chúng ta implement hoặc CanActivate, CanDeactivate, hoặc CanActivateChild interfaces, điều này yêu cầu phải có một method canActivate(), canActivateChild(), hoặc canDeactivate() tương ứng. Những methods này là khá tương đương với một guard function trong kịch bản ở trên. Ví dụ bên dưới thể hiện một guard implementation đơn giản:

@Injectable()
export class AuthGuardService implements CanActivate, CanActivateChild {

    constructor(
        private authService: AuthService,
        private router: Router
    ) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
        return this.authService.authenticated().map((status) => {
            if (status) {
                return true;
            }
            this.router.navigate(["/admin/login"]);

            return false;
        });
    }

    canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
        return this.canActivate(route, state);
    }
}

Ở ví dụ trên chúng ta có thể thấy, AuthGuardService implement cả CanActivate, CanActivateChild, điều này hoàn toàn có thể thực hiện được. CanActivateChild đơn giản là sử dụng lại logic của CanActivate, tuy nhiên thực tế các bạn hoàn toàn có thể mở rộng logic này. Tiếp đến, chúng ta khai báo AuthGuardService với AppModule như bên dưới:

@NgModule({
  ...
  providers: [
    AuthService,
    AuthGuardService
  ]
})
export class AppModule {}

Bây giờ, chúng ta có thể dụng Guard trong routing config cho cả child route như ví dụ bên dưới:

RouterModule.forRoot([
    { path: "admin/login", component: LoginComponent },
    {
        path: "admin",
        component: AdminComponent,
        canActivate: [AuthGuardService],
        children: [
            {
                path: "",
                canActivateChild: [AuthGuardService],
                children: [
                    { path: "", redirectTo: "dashboard", pathMatch: "full" },
                    { path: "dashboard", component: DashboardComponent },
                    { path: "products", component: ProductComponent },
                    ...
                ]
            }
        ]
    ...

3. Deactivating Routes

Bây giờ chúng ta có thể thấy làm thế nào để CanActivate làm việc trong những kịch bản khác nhau, nhưng như đã đề cập trước đó, chúng ta có thêm một số guard interfaces có thể tận dụng. CanDeactivate cho chúng ta lựa chọn để quyết định rời route hay không. Điều này rất hữa dụng nếu chúng ta muốn ngăn chặn người dùng làm mất những thay đổi chưa được save khi điền dữ liệu vào form và không may click trên button cancel.

Việc implement một CanDeactivate guard là rất tương tụ với CanActivate. Tất cả những thứ cần làm là chúng ta tạo lại hoặc một function, hoặc một class cái mà implement CanDeactive interface. Chúng ta có thể implement đơn giản như bên dưới:

import { CanDeactivate } from '@angular/router';
import { CanDeactivateComponent } from './app/can-deactivate';

export class ConfirmDeactivateGuard implements CanDeactivate<CanDeactivateComponent> {

  canDeactivate(target: CanDeactivateComponent) {
    if(target.hasChanges()){
        return window.confirm('Do you really want to cancel?');
    }
    return true;
  }
}

Khai báo nó trong AppModule

@NgModule({
  ...
  providers: [
    ...
    ConfirmDeactivateGuard
  ]
})
export class AppModule {}

4. Thảo luận

Hy vọng bài viết cung cấp cho các bạn những thông tin hữa ích về việc bảo vệ route trong angular. Chúc các bạn thành công. Các bạn có thể tham khảo thêm về Resolve, CanLoad tại Route guard.