Guard là một class được đánh dấu bằng decorator @Injectable(), implement interface CanActivate.

Guard chỉ có một trách nhiệm duy nhất. Chúng xác định liệu một request nhất định có được route handler xử lý hay không, dựa trên các điều kiện nhất định (như quyền hạn, vai trò, ACL, v.v.) có mặt tại thời điểm chạy. Việc này thường được gọi là phân quyền (authorization). Phân quyền (và người anh em của nó, xác thực (authentication), thứ mà nó thường phối hợp cùng) trong các ứng dụng Express truyền thống thường được xử lý bởi middleware. Middleware là lựa chọn tốt cho xác thực, vì những việc như kiểm tra token và gắn thuộc tính vào object request không gắn chặt với một route context cụ thể (và metadata của nó).
Nhưng middleware, do bản chất của nó, không biết gì về context. Nó không biết handler nào sẽ được thực thi sau khi gọi hàm next(). Mặt khác, Guard có quyền truy cập vào instance ExecutionContext, nên biết chính xác điều gì sẽ được thực thi tiếp theo. Chúng được thiết kế, giống như exception filter, pipe và interceptor, để bạn có thể chèn logic xử lý vào đúng thời điểm trong vòng đời request/response, theo cách khai báo (declarative). Điều này giúp code của bạn DRY và mang tính khai báo.
Gợi ý Guard được thực thi sau tất cả middleware, nhưng trước bất kỳ interceptor hoặc pipe nào.
Ghi chú người dịch Thứ tự này có lý do: guard chạy trước cả pipe (tức trước khi body/param được transform hay validate), nên nếu bạn cần chặn request càng sớm càng tốt để tránh tốn công xử lý sau đó, guard là chỗ phù hợp hơn interceptor. Điều này giống việc đặt middleware auth lên đầu chain trong Express hoặc middleware group trong Laravel.
Guard phân quyền (Authorization guard)
Như đã đề cập, phân quyền là một use case tuyệt vời cho Guard vì các route cụ thể chỉ nên khả dụng khi bên gọi (thường là một user đã xác thực cụ thể) có đủ quyền hạn. AuthGuard mà chúng ta sẽ xây dựng bây giờ giả định có một user đã xác thực (và do đó, một token đã được gắn vào header của request). Nó sẽ trích xuất và kiểm tra token, rồi dùng thông tin trích xuất được để quyết định request có được tiếp tục hay không.
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);
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthGuard {
async canActivate(context) {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
Gợi ý Nếu bạn muốn xem ví dụ thực tế về cách triển khai cơ chế xác thực trong ứng dụng của mình, hãy xem chương này. Tương tự, với ví dụ phân quyền phức tạp hơn, hãy xem trang này.
Logic bên trong hàm validateRequest() có thể đơn giản hoặc phức tạp tùy theo nhu cầu. Trọng tâm của ví dụ này là cho thấy guard hòa hợp thế nào vào vòng đời request/response.
Mọi guard đều phải implement một hàm canActivate(). Hàm này phải 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ề kết quả đồng bộ hoặc bất đồng bộ (qua Promise hoặc Observable). Nest dùng giá trị trả về để quyết định hành động tiếp theo:
- nếu trả về
true, request sẽ được xử lý. - nếu trả về
false, Nest sẽ từ chối request.
Ghi chú người dịch Khác với middleware Express, nơi bạn phải tự quyết định gọi next() hay tự trả lỗi (res.status(403).send(...)), guard chỉ cần trả về boolean — Nest tự lo phần throw exception và trả response 403 chuẩn. Nhờ vậy bạn không phải viết lặp lại logic response lỗi ở nhiều guard khác nhau.
Ngữ cảnh thực thi (Execution context)
Hàm canActivate() nhận một tham số duy nhất, là instance ExecutionContext. ExecutionContext kế thừa từ ArgumentsHost. Chúng ta đã thấy ArgumentsHost trước đó ở chương exception filter. Trong ví dụ trên, chúng ta dùng lại các helper method định nghĩa trên ArgumentsHost mà chúng ta đã dùng trước đó, để lấy tham chiếu đến object Request. Bạn có thể xem lại phần Arguments host của chương exception filters để biết thêm về chủ đề này.
Bằng cách kế thừa ArgumentsHost, ExecutionContext cũng bổ sung thêm một số helper method mới cung cấp chi tiết hơn về quá trình thực thi hiện tại. Những chi tiết này có thể hữu ích khi xây dựng các guard tổng quát hơn, có thể hoạt động trên nhiều controller, method và execution context khác nhau. Tìm hiểu thêm về ExecutionContext tại đây.
Xác thực dựa trên vai trò (Role-based authentication)
Hãy xây dựng một guard hữu dụng hơn, chỉ cho phép truy cập với user có vai trò (role) cụ thể. Chúng ta sẽ bắt đầu với một template guard cơ bản, rồi phát triển thêm ở các phần sau. Hiện tại, nó cho phép mọi request được tiếp tục:
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;
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class RolesGuard {
canActivate(context) {
return true;
}
}
Gắn (bind) guard
Giống như pipe và exception filter, guard có thể ở phạm vi controller, phạm vi method, hoặc phạm vi global. Dưới đây, chúng ta thiết lập một guard phạm vi controller bằng decorator @UseGuards(). Decorator này có thể nhận một tham số, hoặc một danh sách tham số cách nhau bởi dấu phẩy. Điều này giúp bạn dễ dàng áp dụng đúng bộ guard cần thiết chỉ với một khai báo.
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
Gợi ý Decorator @UseGuards() được import từ package @nestjs/common.
Ở trên, chúng ta truyền vào class RolesGuard (thay vì một instance), để việc khởi tạo được giao cho framework xử lý, qua đó cho phép dependency injection. Cũng như pipe và exception filter, chúng ta cũng có thể truyền vào một instance được tạo sẵn:
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
Cách viết trên gắn guard vào mọi handler được khai báo trong controller này. Nếu muốn guard chỉ áp dụng cho một method duy nhất, chúng ta áp dụng decorator @UseGuards() ở cấp method.
Để thiết lập một guard toàn cục (global), dùng phương thức useGlobalGuards() của instance ứng dụng Nest:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
Lưu ý Đối với ứng dụng hybrid, phương thức useGlobalGuards() mặc định không thiết lập guard cho gateway và microservice (xem Hybrid application để biết cách thay đổi hành vi này). Với ứng dụng microservice “chuẩn” (không phải hybrid), useGlobalGuards() vẫn gắn guard toàn cục như bình thường.
Global guard được sử dụng cho toàn bộ ứng dụng, áp dụng cho mọi controller và mọi route handler. Về mặt dependency injection, các global guard đăng ký từ bên ngoài bất kỳ module nào (bằng useGlobalGuards() như ví dụ trên) không thể inject dependency, vì việc này được thực hiện ngoài context của bất kỳ module nào. Để giải quyết vấn đề này, bạn có thể thiết lập guard trực tiếp từ một module bất kỳ theo cách sau:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
Gợi ý Khi dùng cách này để thực hiện dependency injection cho guard, lưu ý rằng bất kể
module nào được dùng để khai báo, guard này thực chất vẫn là guard toàn cục. Nên đặt ở đâu? Hãy chọn module
nơi guard (RolesGuard trong ví dụ trên) được định nghĩa. Ngoài ra, useClass không phải cách duy nhất để
đăng ký custom provider. Tìm hiểu thêm tại đây.
Gợi ý Bạn có thể đăng ký token APP_GUARD nhiều lần (trong cùng một module hoặc các module khác nhau) - mỗi guard đã đăng ký sẽ chạy cho mỗi request, theo đúng thứ tự đăng ký. Cũng lưu ý rằng APP_GUARD (giống các token APP_* khác) là một pseudo-provider được framework sử dụng trong quá trình bootstrap: nó không thể được lấy lại sau đó bằng app.get() hay inject ở nơi khác.
Thiết lập role cho từng handler
RolesGuard của chúng ta đã hoạt động, nhưng chưa thực sự “thông minh”. Chúng ta chưa tận dụng tính năng quan trọng nhất của guard - ngữ cảnh thực thi (execution context). Nó vẫn chưa biết gì về role, hay role nào được phép cho từng handler. Ví dụ, CatsController có thể có các sơ đồ quyền hạn khác nhau cho từng route. Một số route chỉ dành cho admin, số khác thì mở cho mọi người. Làm sao để ánh xạ role vào route một cách linh hoạt và tái sử dụng được?
Đây là lúc custom metadata phát huy tác dụng (tìm hiểu thêm tại đây). Nest cho phép gắn metadata tùy chỉnh vào route handler thông qua các decorator được tạo bằng phương thức tĩnh Reflector.createDecorator, hoặc decorator có sẵn @SetMetadata().
Ví dụ, hãy tạo decorator @Roles() bằng phương thức Reflector.createDecorator, decorator này sẽ gắn metadata vào handler. Reflector được framework cung cấp sẵn và được export từ package @nestjs/core.
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
Roles decorator ở đây là một hàm nhận một tham số kiểu string[].
Bây giờ, để sử dụng decorator này, chúng ta chỉ cần đánh dấu (annotate) handler với nó:
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Post()
@Roles(['admin'])
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
Ở đây chúng ta đã gắn metadata của decorator Roles vào method create(), cho biết chỉ user có role admin mới được phép truy cập route này.
Ngoài cách dùng Reflector.createDecorator, chúng ta cũng có thể dùng decorator có sẵn @SetMetadata(). Tìm hiểu thêm tại đây.
Kết hợp tất cả lại
Bây giờ hãy quay lại và ghép nối phần này với RolesGuard của chúng ta. Hiện tại, nó chỉ đơn giản trả về true trong mọi trường hợp, cho phép mọi request đi qua. Chúng ta muốn giá trị trả về phụ thuộc vào việc so sánh role được gán cho user hiện tại với các role thực sự được yêu cầu bởi route đang được xử lý. Để truy cập role(s) của route (custom metadata), chúng ta sẽ lại dùng class trợ giúp Reflector, như sau:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator.js';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
import { Injectable, Dependencies } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator.js';
@Injectable()
@Dependencies(Reflector)
export class RolesGuard {
constructor(reflector) {
this.reflector = reflector;
}
canActivate(context) {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
Gợi ý Trong thế giới node.js, một thông lệ phổ biến là gắn user đã xác thực vào object request. Vì vậy, trong đoạn code mẫu trên, chúng ta giả định rằng request.user chứa instance user và các role được phép. Trong ứng dụng của bạn, việc gán giá trị này thường được thực hiện trong authentication guard tùy chỉnh (hoặc middleware) của bạn. Xem chương này để biết thêm chi tiết về chủ đề này.
Ghi chú người dịch Cạm bẫy hay gặp: nếu RolesGuard chạy trước một AuthGuard đáng lẽ phải gắn request.user, thì user sẽ là undefined và dòng user.roles sẽ throw lỗi runtime chứ không phải một lỗi 403 rõ ràng. Khi khai @UseGuards(AuthGuard, RolesGuard), thứ tự tham số chính là thứ tự thực thi — tương tự thứ tự đăng ký middleware trong router.use() của Express.
Cảnh báo Logic bên trong hàm matchRoles() có thể đơn giản hoặc phức tạp tùy theo nhu cầu. Trọng tâm của ví dụ này là cho thấy guard hòa hợp thế nào vào vòng đời request/response.
Xem lại 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 hướng phụ thuộc ngữ cảnh (context-sensitive).
Khi một user không đủ quyền gọi đến một endpoint, Nest tự động trả về response sau:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
Lưu ý rằng bên dưới, khi một guard trả về false, framework sẽ ném (throw) một ForbiddenException. Nếu bạn muốn trả về một response lỗi khác, bạn nên tự throw exception cụ thể của riêng mình. Ví dụ:
throw new UnauthorizedException();
Bất kỳ exception nào được throw bởi guard đều sẽ được xử lý bởi tầng exception (global exceptions filter và bất kỳ exception filter nào được áp dụng cho context hiện tại).
Gợi ý Nếu bạn muốn xem ví dụ thực tế về cách triển khai phân quyền, hãy xem chương này.