Guards trong NestJS
Guards là một công cụ để xác định liệu một request có nên được xử lý bởi route handler hay không. Chúng chủ yếu được sử dụng cho authentication và authorization. Một guard là một class implement interface CanActivate với một method canActivate().
Khái Niệm Guard
Guard có một trách nhiệm duy nhất: xác định liệu một request có được phép truy cập resource hay không. Guard được thực thi sau middleware nhưng trước pipes.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// Logic xác thực
return true; // true = cho phép, false = từ chối
}
}
Request Processing Pipeline (with Guards)
Request
↓
Global Middleware
↓
Module Middleware
↓
→ Guards (xác thực, kiểm tra quyền)
↓
Interceptors (before)
↓
Pipes (validation)
↓
Controller
↓
Interceptors (after)
↓
Response
ExecutionContext
ExecutionContext cung cấp thông tin chi tiết về request hiện tại:
@Injectable()
export class CustomGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// Lấy HTTP context
const http = context.switchToHttp();
const request = http.getRequest();
const response = http.getResponse();
// Lấy handler class và method
const handler = context.getHandler();
const classRef = context.getClass();
// Lấy arguments của handler
const args = context.getArgs();
return true;
}
}
Built-in Guards
NestJS cung cấp một số built-in guards thông qua @nestjs/passport:
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
import { LocalAuthGuard } from './local-auth.guard';
@Controller('users')
@UseGuards(AuthGuard('jwt'))
export class UsersController {
@Get()
findAll() {
return [];
}
}
Cơ Bản về Guards
1. Simple Authentication Guard
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class SimpleAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.headers.authorization;
if (!authHeader) {
throw new UnauthorizedException('Missing authorization header');
}
const [scheme, token] = authHeader.split(' ');
if (scheme !== 'Bearer' || !token) {
throw new UnauthorizedException('Invalid authorization header');
}
// Validate token (simplified)
if (token === 'invalid-token') {
throw new UnauthorizedException('Invalid token');
}
// Attach user to request
(request as any).user = { id: 1, email: 'user@example.com' };
return true;
}
}
// Sử dụng
@Controller('users')
@UseGuards(SimpleAuthGuard)
export class UsersController {
@Get()
findAll() {
return [];
}
}
2. Roles Guard
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
// Custom decorator để chỉ định required roles
export const Roles = Reflector.createDecorator<string[]>();
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
// Nếu không có roles được chỉ định, cho phép truy cập
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const user = (request as any).user;
if (!user) {
throw new ForbiddenException('User not found');
}
if (!roles.includes(user.role)) {
throw new ForbiddenException(
`User with role "${user.role}" is not allowed to access this resource`,
);
}
return true;
}
}
// Sử dụng
@Controller('admin')
@UseGuards(SimpleAuthGuard, RolesGuard)
export class AdminController {
@Get()
@Roles(['admin'])
getDashboard() {
return { data: 'Admin dashboard' };
}
@Post('users')
@Roles(['admin', 'moderator'])
createUser() {
return { message: 'User created' };
}
}
Ví Dụ Guards Thực Tế
1. JWT Authentication Guard
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Missing token');
}
try {
const payload = this.jwtService.verify(token);
(request as any).user = payload;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// Sử dụng
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
@Get()
getProfile(@Req() req: Request) {
const user = (req as any).user;
return { user };
}
}
2. Optional JWT Guard
import { Injectable, ExecutionContext } from '@nestjs/common';
@Injectable()
export class OptionalJwtAuthGuard extends JwtAuthGuard {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
return true; // Token tùy chọn
}
try {
const payload = this.jwtService.verify(token);
(request as any).user = payload;
} catch (error) {
// Nếu token invalid, vẫn cho phép truy cập
return true;
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// Sử dụng - user có thể optional
@Controller('posts')
@UseGuards(OptionalJwtAuthGuard)
export class PostsController {
@Get()
findAll(@Req() req: Request) {
const user = (req as any).user;
// Nếu user login, hiển thị nội dung đặc biệt
// Nếu không, hiển thị nội dung công khai
return { authenticated: !!user };
}
}
3. API Key Guard
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class ApiKeyGuard implements CanActivate {
private readonly validApiKeys = [
'sk_test_abc123',
'sk_test_def456',
];
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const apiKey = request.headers['x-api-key'] as string;
if (!apiKey) {
throw new UnauthorizedException('Missing API key');
}
if (!this.validApiKeys.includes(apiKey)) {
throw new UnauthorizedException('Invalid API key');
}
return true;
}
}
// Sử dụng
@Controller('api/webhooks')
@UseGuards(ApiKeyGuard)
export class WebhooksController {
@Post('process')
processWebhook(@Body() payload: any) {
return { success: true };
}
}
4. Owner Guard (Resource Ownership)
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Request } from 'express';
import { UsersService } from '../users/users.service';
@Injectable()
export class OwnerGuard implements CanActivate {
constructor(private usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const user = (request as any).user;
const resourceId = request.params.id;
if (!user) {
throw new ForbiddenException('User not found');
}
// Kiểm tra xem user có quyền sở hữu resource này không
const resource = await this.usersService.getUserResource(resourceId);
if (resource.userId !== user.id) {
throw new ForbiddenException('You do not have permission to access this resource');
}
return true;
}
}
// Sử dụng
@Controller('posts')
@UseGuards(JwtAuthGuard, OwnerGuard)
export class PostsController {
@Put(':id')
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
return { message: 'Post updated' };
}
@Delete(':id')
delete(@Param('id') id: string) {
return { message: 'Post deleted' };
}
}
5. Time-based Guard
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
@Injectable()
export class TimeBasedGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const now = new Date();
const hour = now.getHours();
// Chỉ cho phép truy cập từ 9 AM - 5 PM
if (hour < 9 || hour >= 17) {
throw new ForbiddenException('This resource is only available during business hours');
}
return true;
}
}
6. IP Whitelist Guard
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class IpWhitelistGuard implements CanActivate {
private readonly whitelistedIps = [
'127.0.0.1',
'192.168.1.100',
'203.0.113.0',
];
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const clientIp = request.ip;
if (!this.whitelistedIps.includes(clientIp)) {
throw new ForbiddenException(`IP address ${clientIp} is not whitelisted`);
}
return true;
}
}
// Sử dụng
@Controller('admin')
@UseGuards(IpWhitelistGuard)
export class AdminController {
@Get('sensitive-data')
getSensitiveData() {
return { data: 'Sensitive information' };
}
}
7. Permission-based Guard
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
export const Permission = Reflector.createDecorator<string[]>();
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.get(Permission, context.getHandler());
if (!requiredPermissions) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const user = (request as any).user;
if (!user) {
throw new ForbiddenException('User not found');
}
const hasPermission = requiredPermissions.every((permission) =>
user.permissions?.includes(permission),
);
if (!hasPermission) {
throw new ForbiddenException('You do not have the required permissions');
}
return true;
}
}
// Sử dụng
@Controller('users')
@UseGuards(JwtAuthGuard, PermissionGuard)
export class UsersController {
@Post()
@Permission(['users:create'])
create(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}
@Delete(':id')
@Permission(['users:delete'])
delete(@Param('id') id: string) {
return { message: 'User deleted' };
}
}
8. Rate Limiting Guard
import { Injectable, CanActivate, ExecutionContext, TooManyRequestsException } from '@nestjs/common';
import { Request } from 'express';
interface RateLimit {
count: number;
resetTime: number;
}
@Injectable()
export class RateLimitGuard implements CanActivate {
private requests: Map<string, RateLimit> = new Map();
private readonly maxRequests = 100;
private readonly timeWindow = 60000; // 1 minute
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const ip = request.ip;
const now = Date.now();
const record = this.requests.get(ip);
if (!record || now > record.resetTime) {
this.requests.set(ip, { count: 1, resetTime: now + this.timeWindow });
return true;
}
if (record.count >= this.maxRequests) {
throw new TooManyRequestsException(
`Rate limit exceeded. Max ${this.maxRequests} requests per ${this.timeWindow / 1000}s`,
);
}
record.count++;
return true;
}
}
9. Custom Metadata Guard
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
// Custom decorator
export const Public = Reflector.createDecorator<void>();
export const RequireFeature = Reflector.createDecorator<string>();
@Injectable()
export class FeatureGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Nếu marked as @Public, cho phép truy cập
const isPublic = this.reflector.getAllAndOverride(Public, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
// Kiểm tra required features
const requiredFeature = this.reflector.get(RequireFeature, context.getHandler());
if (!requiredFeature) {
return true; // Không có yêu cầu feature cụ thể
}
const request = context.switchToHttp().getRequest();
const user = (request as any).user;
// Giả sử user có array features được enabled
return user?.enabledFeatures?.includes(requiredFeature) ?? false;
}
}
// Sử dụng
@Controller('api')
export class ApiController {
@Get('public')
@Public()
getPublic() {
return { data: 'public' };
}
@Get('premium')
@RequireFeature('premium_feature')
getPremium() {
return { data: 'premium' };
}
}
Guards at Different Scopes
Method-level
@Controller('users')
export class UsersController {
@Get()
@UseGuards(JwtAuthGuard)
findAll() {
return [];
}
@Get('public')
findPublic() {
return [];
}
}
Class-level
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
// Tất cả methods trong controller này được protect
@Get()
getDashboard() {
return { data: 'admin' };
}
}
Global-level
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new JwtAuthGuard());
await app.listen(3000);
}
bootstrap();
Module-level (Provider)
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
Async Guards
Guards có thể async:
@Injectable()
export class AsyncAuthGuard implements CanActivate {
constructor(private usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Missing token');
}
try {
const payload = await this.verifyToken(token);
const user = await this.usersService.findOne(payload.sub);
if (!user || !user.isActive) {
return false;
}
(request as any).user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
private verifyToken(token: string): Promise<any> {
// Implementation
return Promise.resolve({});
}
}
Guards vs Other Features
| Feature | Guards | Interceptors | Middleware | Pipes |
|---|---|---|---|---|
| Xử lý HTTP | ✓ | ✓ | ✓ | ✓ |
| Execution Context | ✓ | ✓ | ✗ | ✓ |
| Có thể ngăn chặn request | ✓ | ✗ | ✓ | ✗ |
| Dùng cho authorization | ✓ | ✗ | ✗ | ✗ |
| Dùng cho authentication | ✓ | ✓ | ✓ | ✗ |
| Dùng cho validation | ✗ | ✗ | ✗ | ✓ |
Best Practices
1. Tách Biệt Concerns
// ❌ Sai - Quá nhiều logic trong một guard
@Injectable()
export class BadGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// Auth logic
// Logging logic
// Permission logic
// Rate limiting logic
return true;
}
}
// ✅ Đúng - Từng guard một trách nhiệm
@UseGuards(AuthGuard, PermissionGuard, RateLimitGuard)
export class ProtectedController {}
2. Reusable Guards
// ✅ Tạo generic guard có thể reuse
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private requiredRoles: string[]) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = (request as any).user;
return this.requiredRoles.includes(user.role);
}
}
3. Proper Error Handling
// ✅ Throw appropriate exceptions
@Injectable()
export class ProperErrorGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
throw new UnauthorizedException('Missing token');
}
if (!this.isValidToken(request.headers.authorization)) {
throw new UnauthorizedException('Invalid token');
}
return true;
}
private isValidToken(token: string): boolean {
// Token validation
return true;
}
}
4. Use Reflector cho Metadata
// ✅ Sử dụng Reflector để đọc custom metadata
export const Roles = Reflector.createDecorator<string[]>();
@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; // No roles required
const request = context.switchToHttp().getRequest();
const user = (request as any).user;
return roles.includes(user.role);
}
}
5. Guard Order Matters
// ✅ Đúng - Auth trước, sau đó kiểm tra quyền
@UseGuards(AuthGuard, PermissionGuard)
export class ProtectedController {}
// ❌ Sai - Kiểm tra quyền trước auth (sẽ lỗi)
@UseGuards(PermissionGuard, AuthGuard)
export class ProtectedController {}
Complete Example
// decorators/roles.decorator.ts
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
// guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Missing token');
}
try {
const payload = this.jwtService.verify(token);
(request as any).user = payload;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
return true;
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// guards/roles.guard.ts
@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<Request>();
const user = (request as any).user;
if (!user) {
throw new ForbiddenException('User not found');
}
return roles.includes(user.role);
}
}
// controllers/admin.controller.ts
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
@Roles(['admin'])
getDashboard() {
return { data: 'Admin dashboard' };
}
@Post('users')
@Roles(['admin', 'moderator'])
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}
@Delete('users/:id')
@Roles(['admin'])
deleteUser(@Param('id') id: string) {
return { message: 'User deleted' };
}
}
// app.module.ts
@Module({
imports: [JwtModule.register({ secret: 'secret' })],
controllers: [AdminController],
providers: [JwtAuthGuard, RolesGuard],
})
export class AppModule {}
Kết Luận
Guards là công cụ quan trọng để:
- Xác thực users (authentication)
- Kiểm tra quyền (authorization)
- Kiểm soát truy cập (access control)
- Bảo vệ resources khỏi truy cập không được phép
Sử dụng Guards đúng cách giúp bạn:
- Xây dựng ứng dụng secure
- Kiểm soát quyền truy cập một cách tập trung
- Tái sử dụng logic authentication/authorization
- Giảm boilerplate code
- Tạo ứng dụng maintainable và scalable