Tổng quan – NestJS

Guards

Một Guard là một lớp được chú thích bằng decorator @Injectable(). Các Guards phải implement interface CanActivate.

Các Guards có một single responsibility. Họ xác định xem một request nhất định sẽ được xử lý bởi route handler hay không, tùy thuộc vào các điều kiện nhất định (như quyền/permissons, vai trò/roles, ACLs, v.v.) hiện có tại thời điểm chạy. Điều này thường được gọi là ủy quyền/authorization. Ủy quyền/Authorization (và người anh em họ của nó, xác thực/authentication, mà nó thường cộng tác) thường được xử lý bởi middleware trong các ứng dụng Express truyền thống. Middleware là một lựa chọn tốt để xác thực/authentication, vì những thứ như token validation thông báo và đính kèm thuộc tính vào request object không được kết nối chặt chẽ với ngữ cảnh route cụ thể (và metadata của nó).

Nhưng middleware, về bản chất, là ngu ngốc. Nó không biết handler nào sẽ được thực thi sau khi gọi hàm next(). Mặt khác, Guards có quyền truy cập vào instance ExecutionContext và do đó biết chính xác những gì sẽ được thực thi tiếp theo. Chúng được thiết kế, giống như exception filters, pipes và interceptors, để cho phép bạn sử dụng logic xử lý chính xác vào đúng điểm trong chu kỳ request/response và làm như vậy một cách khai báo. Điều này giúp giữ cho code của bạn DRY và dễ khai báo.

HINT Guards được thực thi sau mỗi middleware, nhưng trước bất kỳ interceptor hoặc pipe.

Authorization guard

Như đã đề cập, authorization/ủy quyền là một trường hợp sử dụng tuyệt vời cho các Guards vì các route cụ thể chỉ khả dụng khi người gọi (thường là người dùng được authenticated/xác thực cụ thể) có đủ quyền/permissions. AuthGuard mà chúng tôi sẽ xây dựng bây giờ giả định một người dùng đã được xác thực/authenticated (và do đó, một token được đính kèm với các request headers). Nó sẽ trích xuất và validate token, đồng thời sử dụng thông tin được trích xuất để xác định xem liệu request có thể tiếp tục hay không.

auth.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

HINT Nếu bạn đang tìm kiếm một ví dụ trong thế giới thực về cách triển khai cơ chế xác thực trong ứng dụng của mình, hãy vào chương này. Tương tự như vậy, đối với ví dụ ủy quyền phức tạp hơn, hãy kiểm tra trang này.

Logic bên trong hàm validateRequest() có thể đơn giản hoặc phức tạp nếu cần. Điểm chính của ví dụ này là chỉ ra cách các guards phù hợp với chu trình request/response.

Mọi guard phải implement một hàm canActivate(). Hàm này sẽ trả về một boolean, cho biết request hiện tại có được phép hay không. Nó có thể trả về response đồng bộ hoặc không đồng bộ (thông qua Promise hoặc Observable). Nest sử dụng giá trị trả về để kiểm soát hành động tiếp theo:

  • Nếu trả về true, request sẽ được thực hiện.
  • Nếu trả về false, Nest sẽ từ chối request.

Execution context

Hàm canActivate() nhận một đối số duy nhất là instance ExecutionContext. ExecutionContext kế thừa từ ArgumentsHost. Chúng ta đã thấy ArgumentsHost trước đây trong chương exception filters. Trong ví dụ trên, chúng tôi chỉ đang sử dụng cùng một phương thức helper được định nghĩa trên ArgumentsHost mà chúng tôi đã sử dụng trước đó, để nhận tham chiếu đến Request object. Bạn có thể tham khảo lại phần Arguments host của chương  exception filters để biết thêm về chủ đề này.

Bằng cách mở rộng ArgumentsHost, ExecutionContext cũng bổ sung một số phương thức helper mới cung cấp chi tiết bổ sung về quy trình thực thi hiện tại. Những chi tiết này có thể hữu ích trong việc xây dựng nhiều guards chung hơn có thể hoạt động trên nhiều controllers, phương thức và execution contexts. Tìm hiểu thêm về ExecutionContext here.

Role-based authentication

Hãy xây dựng một biện pháp guard nhiều chức năng hơn chỉ cho phép truy cập vào những người dùng có vai trò cụ thể. Chúng tôi sẽ bắt đầu với một mẫu guard cơ bản và xây dựng dựa trên nó trong các phần tiếp theo. Hiện tại, nó cho phép tất cả các request tiếp tục:

roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

Binding guards

Giống như các pipes và exception filters, các guards có thể là phạm vi controller, phạm vi phương thức hoặc phạm vi toàn cục. Dưới đây, chúng tôi thiết lập bộ phận bảo vệ có phạm vi controller bằng cách sử dụng decorator @UseGuards(). Decorator này này có thể nhận một đối số duy nhất hoặc một danh sách các đối số được phân tách bằng dấu phẩy. Điều này cho phép bạn dễ dàng áp dụng nhóm guards thích hợp với một tuyên bố.

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

Cấu trúc bên trên gắn guard vào mọi handler do controller này khai báo. Nếu chúng ta muốn guard chỉ áp dụng cho một phương thức, chúng ta áp dụng decorator @UseGuards()cấp phương thức.

Để thiết lập bảo vệ toàn cục, hãy sử dụng phương thức useGlobalGuards() của instance ứng dụng Nest:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

NOTICE Trong trường hợp ứng dụng hybrid apps phương thức useGlobalGuards() không thiết lập guards cho gateways và micro services theo mặc định(see Hybrid application lấy nhều thông tin cách thay đổi hành vi này). Đối với “tiêu chuẩn:” (non-hybrid) microservice apps, useGlobalGuards() gắn kết các guards trên toàn cục.

Guards toàn cục được sử dụng trên toàn bộ ứng dụng, cho mọi controller và mọi route handler. Về mặt tiêm dependency injection, các guards toàn cầu được đăng ký từ bên ngoài của bất kỳ mô-đun nào (với useGlobalGuards() như trong ví dụ trên) không thể inject dependencies vì điều này được thực hiện bên ngoài ngữ cảnh của bất kỳ mô-đun nào. Để giải quyết vấn đề này, bạn có thể thiết lập bảo vệ trực tiếp từ bất kỳ mô-đun nào bằng cách sử dụng cấu trúc sau:

app.module.ts

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

HINT Khi sử dụng cách tiếp cận này để thực hiện dependency injection cho guard, hãy lưu ý rằng bất kể mô-đun nơi cấu trúc này được sử dụng, guard, trên fact, toàn cục. Điều này nên được thực hiện ở đâu? Chọn mô-đun nơi guard (RolesGuard trong ví dụ trên) được xác định. Ngoài ra, useClass không phải là cách duy nhất để giải quyết việc đăng ký provider tùy chỉnh. Tim hiểu thêm ở đây.

Setting roles per handler

RolesGuard của chúng tôi đang hoạt động, nhưng nó chưa thông minh lắm. Chúng tôi vẫn chưa tận dụng tính năng guard quan trọng nhất – execution context. Nó vẫn chưa biết về các roles, hoặc những roles nào được phép cho mỗi handler. Ví dụ, CatsController có thể có các sơ đồ quyền khác nhau cho các routekhác nhau. Một số có thể chỉ có sẵn cho người dùng quản trị và những cái khác có thể mở cho tất cả mọi người. Làm thế nào chúng ta có thể kết hợp các role với các routes một cách linh hoạt và có thể tái sử dụng?

Đây là nơi metadata tùy chỉnh phát huy tác dụng (tìm hiểu thêm tại đây). Nest cung cấp khả năng đính kèm metadata tùy chỉnh để route handlers thông qua decorator @SetMetadata(). Metadata này cung cấp dữ liệu vai trò còn thiếu của chúng tôi, mà một guard thông minh cần đưa ra quyết định. Hãy xem cách sử dụng @SetMetadata():

cats.controller.ts

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

HINT SetMetadata() decorato được nhập từ gói @nestjs/common.

Với cách xây dựng ở trên, chúng tôi đã đính kèm metadata roles (roles là một khóa, trong khi [‘admin’] là một giá trị cụ thể) vào phương thức create(). Mặc dù điều này hoạt động, nhưng thực tế không tốt nếu sử dụng @SetMetadata() trực tiếp trong các routes của bạn. Thay vào đó, hãy decorator của riêng bạn, như được hiển thị bên dưới:

roles.decorator.ts

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Cách tiếp cận này rõ ràng hơn và dễ đọc hơn, và được kiểu mạnh mẽ. Bây giờ chúng ta đã có một decorator @Roles() tùy chỉnh, chúng ta có thể sử dụng nó để decorator cho phương thức create().

cats.controller.ts

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

Putting it all together

Bây giờ chúng ta hãy quay lại và liên kết điều này với RolesGuard của chúng tôi. Hiện tại, nó chỉ trả về true trong mọi trường hợp, cho phép mọi request được tiến hành. Chúng tôi muốn đặt giá trị trả về có điều kiện dựa trên việc so sánh các vai trò được chỉ định cho người dùng hiện tại với các vai trò thực tế được yêu cầu bởi route hiện tại đang được xử lý. Để truy cập (các) vai trò của route (metadata tùy chỉnh), chúng tôi sẽ sử dụng lớp helper Reflector, được cung cấp bên ngoài bởi framework và được lấy từ gói @nestjs/core.

roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

HINT Trong thế giới node.js, việc đính kèm người dùng được ủy quyền/authorized với request object. Do đó, trong mã mẫu của chúng tôi ở trên, chúng tôi giả định rằng request.user chứa instance của user và các roles được phép. Trong ứng dụng của mình, bạn có thể sẽ tạo liên kết đó theo tùy chỉnh authentication guard của mình (or middleware). Kiểm tra chương này để có nhiều thông tin hơn về chủ đề này.

WARNING logic bên trong hàm matchRole() có thể đơn giản hoặc phức tạp nếu cần. Điểm chính của ví dụ này là chỉ ra cách các guards phù hợp với chu trình Request/response.

Tham khảo phần Reflection and metadata của chương  Execution context để biết thêm chi tiết về cách sử dụng Reflector theo cách nhạy cảm với ngữ cảnh.

Khi người dùng không có đủ đặc quyền yêu cầu một endpoint. Nest sẽ tự động trả về response sau:

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

Lưu ý rằng đằng sau hậu trường, khi một guard trả về false, framework sẽ ném một ForbiddenException. Nếu bạn muốn trả về một response lỗi khác, bạn nên đưa ra ngoại lệ cụ thể của riêng mình. Ví dụ:

throw new UnauthorizedException();

Bất kỳ ngoại lệ nào được đưa ra bởi một guard sẽ được xử lý bởi lớp ngoại lệ  exceptions layer  (bộ lọc ngoại lệ chung và bất kỳ bộ lọc ngoại lệ nào được áp dụng cho ngữ cảnh hiện tại).

HINT Nếu bạn đang tìm kiếm một ví dụ trong thế giới thực về cách triển khai ủy quyền, hãy kiểm tra this chapter.

(Nguồn https://docs.nestjs.com/guards)

One Comment on “Tổng quan – NestJS

  1. Nếu một interceptor handle() được gọi ở bất kỳ đâu trên đường đi, phương thức create() sẽ không được thực thi. => Nếu một interceptor handle() “”không”” được gọi ở bất kỳ đâu trên đường đi, phương thức create() sẽ không được thực thi.

Leave a Reply

%d bloggers like this: