NestJS

9. NestJS 가드(Guards): 인증과 권한 부여 시스템 구현

오늘의하늘은 2025. 3. 31. 00:05

웹 애플리케이션에서 인증과 권한 부여는 필수적인 보안 요소입니다. 인증은 사용자가 자신이 주장하는 대로 맞는지 확인하는 과정이고, 권한 부여는 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다. NestJS는 이러한 기능을 쉽게, 그리고 체계적으로 구현할 수 있는 '가드(Guards)'라는 메커니즘을 제공합니다.

이 글에서는 NestJS 가드의 동작 원리와 가드를 활용하여 애플리케이션에 인증 및 권한 부여 시스템을 구현하는 방법을 자세히 살펴보겠습니다.

목차

  1. 가드의 개념과 역할
  2. 가드의 실행 컨텍스트
  3. 기본 인증 가드 구현하기
  4. JWT 기반 인증 시스템 구축하기
  5. 역할 기반 접근 제어(RBAC) 구현하기
  6. 정책 기반 접근 제어(PBAC) 구현하기
  7. 가드 조합하기
  8. 전역 가드 등록하기
  9. 가드와 다른 핸들러의 통합
  10. 모범 사례와 패턴

1. 가드의 개념과 역할

NestJS에서 가드는 특정 라우트 핸들러가 요청을 처리할 수 있는지 여부를 결정하는 역할을 합니다. 이는 미들웨어와 유사하지만, 더 강력하고 정교한 기능을 제공합니다. 가드는 다음과 같은 특성을 가집니다:

  1. 실행 컨텍스트에 접근: 가드는 ExecutionContext 인스턴스에 접근할 수 있어 현재 실행 중인 메서드에 대한 상세한 정보를 얻을 수 있습니다.
  2. 선언적 사용: 전역, 컨트롤러 또는 메서드 수준에서 선언적으로 적용할 수 있습니다.
  3. 조건부 실행: 가드는 canActivate() 메서드를 통해 요청의 처리 여부를 결정할 수 있습니다.
  4. 의존성 주입: NestJS의 의존성 주입 시스템을 활용할 수 있습니다.

가드의 주요 용도는 다음과 같습니다:

  • 인증: 요청한 사용자가 누구인지 확인합니다.
  • 권한 부여: 사용자가 특정 리소스에 접근할 권한이 있는지 확인합니다.
  • 속도 제한: 특정 기간 내 요청 수를 제한합니다.
  • 캐싱: 응답 캐싱 전략을 구현합니다.
  • 로깅: 특정 요청에 대한 로깅을 처리합니다.

2. 가드의 실행 컨텍스트

가드는 ExecutionContext를 통해 현재 요청의 다양한 측면에 접근할 수 있습니다. 이를 통해 더 정교한 조건부 로직을 구현할 수 있습니다.

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; // 역할이 지정되지 않은 경우 접근 허용
    }
    
    // HTTP 요청 컨텍스트로 전환
    const request = context.switchToHttp().getRequest();
    const user = request.user; // 요청에 포함된 사용자 정보
    
    if (!user) {
      return false; // 사용자 정보가 없으면 접근 거부
    }
    
    // 사용자가 필요한 역할을 가지고 있는지 확인
    return roles.some((role) => user.roles?.includes(role));
  }
}

ExecutionContext는 다음과 같은 메서드를 제공합니다:

  • getHandler(): 현재 처리 중인 핸들러(라우트 핸들러 메서드)를 반환합니다.
  • getClass(): 핸들러가 속한 컨트롤러 클래스를 반환합니다.
  • switchToHttp(): HTTP 컨텍스트로 전환하여 요청, 응답 객체에 접근할 수 있게 합니다.
  • switchToRpc(): RPC(마이크로서비스) 컨텍스트로 전환합니다.
  • switchToWs(): WebSocket 컨텍스트로 전환합니다.

또한 Reflector를 사용하면 컨트롤러나 핸들러에 설정된 메타데이터에 접근할 수 있어, 데코레이터를 통해 가드의 동작을 제어할 수 있습니다.

3. 기본 인증 가드 구현하기

가장 기본적인 형태의 인증 가드는 요청 헤더에서 API 키나 토큰을 확인하는 것입니다. 다음은 간단한 API 키 인증 가드의 예시입니다:

// api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private configService: ConfigService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    const apiKey = request.header('X-API-KEY');
    
    // API 키가 없거나 유효하지 않은 경우
    if (!apiKey || apiKey !== this.configService.get<string>('API_KEY')) {
      throw new UnauthorizedException('Invalid API key');
    }
    
    return true;
  }
}

가드 사용하기

이제 이 가드를 컨트롤러나 라우트 핸들러에 적용할 수 있습니다:

// products.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiKeyGuard } from '../guards/api-key.guard';
import { ProductsService } from './products.service';

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get()
  @UseGuards(ApiKeyGuard) // 이 라우트에만 가드 적용
  findAll() {
    return this.productsService.findAll();
  }
}

또는 컨트롤러 전체에 가드를 적용할 수도 있습니다:

@Controller('products')
@UseGuards(ApiKeyGuard) // 컨트롤러의 모든 라우트에 가드 적용
export class ProductsController {
  // ...
}

4. JWT 기반 인증 시스템 구축하기

JWT(JSON Web Token)는 현대 웹 애플리케이션에서 가장 널리 사용되는 인증 메커니즘 중 하나입니다. NestJS에서 JWT 기반 인증 시스템을 구축하는 방법을 살펴보겠습니다.

필요한 패키지 설치

npm install @nestjs/jwt passport passport-jwt @nestjs/passport
npm install -D @types/passport-jwt

JWT 모듈 설정

// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersModule } from '../users/users.module';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: {
          expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
        },
      }),
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

JWT 전략 구현

// strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private readonly configService: ConfigService,
    private readonly usersService: UsersService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    // 토큰의 페이로드에서 사용자 ID를 추출
    const user = await this.usersService.findById(payload.sub);
    
    if (!user) {
      throw new UnauthorizedException('User no longer exists');
    }
    
    // 이 객체는 request.user에 자동으로 할당됩니다
    return {
      id: user.id,
      email: user.email,
      roles: user.roles,
    };
  }
}

인증 서비스 구현

// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async validateUser(email: string, password: string) {
    const user = await this.usersService.findByEmail(email);
    
    if (!user) {
      return null;
    }
    
    const isPasswordValid = await bcrypt.compare(password, user.password);
    
    if (!isPasswordValid) {
      return null;
    }
    
    // 비밀번호 필드 제외하고 반환
    const { password: _, ...result } = user;
    return result;
  }

  async login(user: any) {
    const payload = {
      email: user.email,
      sub: user.id, // JWT subject 필드
    };
    
    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: user.id,
        email: user.email,
        roles: user.roles,
      },
    };
  }
}

JWT 가드 구현

이제 JWT 전략을 사용하는 가드를 구현합니다. NestJS의 Passport 모듈은 AuthGuard를 제공하므로 이를 확장하면 됩니다:

// jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

컨트롤러에 가드 적용

// auth.controller.ts
import { Controller, Post, UseGuards, Request, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    const user = await this.authService.validateUser(
      loginDto.email,
      loginDto.password,
    );
    
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    
    return this.authService.login(user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

이제 클라이언트가 /auth/login 엔드포인트를 통해 로그인하면 JWT 토큰을 받고, 이 토큰을 사용하여 보호된 엔드포인트에 접근할 수 있습니다.

5. 역할 기반 접근 제어(RBAC) 구현하기

역할 기반 접근 제어는 사용자의 역할에 따라 리소스에 대한 접근을 제어하는 방법입니다. NestJS에서는 가드와 메타데이터를 활용하여 RBAC를 쉽게 구현할 수 있습니다.

역할 메타데이터 데코레이터 생성하기

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

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

역할 기반 가드 구현하기

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { JwtAuthGuard } from './jwt-auth.guard';

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

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 먼저 JWT 인증 가드를 통과해야 함
    const isAuthenticated = await this.jwtAuthGuard.canActivate(context);
    
    if (!isAuthenticated) {
      return false;
    }
    
    // 메타데이터에서 필요한 역할 가져오기
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (!requiredRoles) {
      return true; // 역할이 지정되지 않은 경우 접근 허용
    }
    
    const { user } = context.switchToHttp().getRequest();
    
    // 사용자가 필요한 역할을 가지고 있는지 확인
    const hasRequiredRoles = requiredRoles.some(role => 
      user.roles?.includes(role)
    );
    
    if (!hasRequiredRoles) {
      throw new ForbiddenException(
        `User does not have sufficient permissions. Required roles: ${requiredRoles.join(', ')}`,
      );
    }
    
    return true;
  }
}

역할 기반 접근 제어 사용하기

// users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator';
import { UsersService } from './users.service';

@Controller('users')
@UseGuards(RolesGuard) // 컨트롤러 수준에서 가드 적용
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @Roles('ADMIN') // 관리자만 모든 사용자 목록에 접근 가능
  findAll() {
    return this.usersService.findAll();
  }

  @Get('profile')
  @Roles('USER', 'ADMIN') // 일반 사용자와 관리자 모두 프로필에 접근 가능
  getProfile(@Request() req) {
    return req.user;
  }
}

6. 정책 기반 접근 제어(PBAC) 구현하기

정책 기반 접근 제어는 역할 기반 접근 제어보다 더 세밀한 접근 제어를 제공합니다. 특정 조건이나 규칙을 기반으로 접근을 제어할 수 있습니다.

정책 인터페이스 정의하기

// interfaces/policy.interface.ts
import { Request } from 'express';

export interface Policy {
  canActivate(user: any, resource: any, request: Request): boolean | Promise<boolean>;
}

정책 구현하기

// policies/article-owner.policy.ts
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { ArticlesService } from '../../articles/articles.service';
import { Policy } from '../interfaces/policy.interface';

@Injectable()
export class ArticleOwnerPolicy implements Policy {
  constructor(private readonly articlesService: ArticlesService) {}

  async canActivate(user: any, resource: any, request: Request): Promise<boolean> {
    const articleId = request.params.id;
    
    if (!articleId) {
      return false;
    }
    
    const article = await this.articlesService.findOne(articleId);
    
    if (!article) {
      return false;
    }
    
    // 사용자가 자신의 글만 수정/삭제할 수 있도록 함
    return article.authorId === user.id;
  }
}

정책 데코레이터 생성하기

// decorators/policy.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const POLICY_KEY = 'policy';
export const UsePolicy = (policyClass: any) => SetMetadata(POLICY_KEY, policyClass);

정책 기반 가드 구현하기

// policy.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ModuleRef } from '@nestjs/core';
import { POLICY_KEY } from '../decorators/policy.decorator';
import { Policy } from '../interfaces/policy.interface';
import { JwtAuthGuard } from './jwt-auth.guard';

@Injectable()
export class PolicyGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private moduleRef: ModuleRef,
    private jwtAuthGuard: JwtAuthGuard,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 먼저 JWT 인증 가드를 통과해야 함
    const isAuthenticated = await this.jwtAuthGuard.canActivate(context);
    
    if (!isAuthenticated) {
      return false;
    }
    
    // 메타데이터에서 정책 클래스 가져오기
    const policyClass = this.reflector.get<any>(
      POLICY_KEY,
      context.getHandler(),
    );
    
    if (!policyClass) {
      return true; // 정책이 지정되지 않은 경우 접근 허용
    }
    
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest();
    const user = request.user;
    
    // 정책 인스턴스 생성
    const policy = await this.moduleRef.resolve<Policy>(policyClass);
    
    // 정책 검사
    const canActivate = await policy.canActivate(user, null, request);
    
    if (!canActivate) {
      throw new ForbiddenException('You do not have permission to perform this action');
    }
    
    return true;
  }
}

정책 기반 접근 제어 사용하기

// articles.controller.ts
import { Controller, Get, Put, Delete, Param, UseGuards } from '@nestjs/common';
import { PolicyGuard } from '../guards/policy.guard';
import { UsePolicy } from '../decorators/policy.decorator';
import { ArticleOwnerPolicy } from '../policies/article-owner.policy';
import { ArticlesService } from './articles.service';

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Get()
  findAll() {
    return this.articlesService.findAll();
  }

  @Put(':id')
  @UseGuards(PolicyGuard)
  @UsePolicy(ArticleOwnerPolicy) // 글 작성자만 수정 가능
  update(@Param('id') id: string, @Body() updateArticleDto: UpdateArticleDto) {
    return this.articlesService.update(id, updateArticleDto);
  }

  @Delete(':id')
  @UseGuards(PolicyGuard)
  @UsePolicy(ArticleOwnerPolicy) // 글 작성자만 삭제 가능
  remove(@Param('id') id: string) {
    return this.articlesService.remove(id);
  }
}

7. 가드 조합하기

NestJS에서는 여러 가드를 조합하여 더 복잡한 인증 및 권한 부여 시스템을 구축할 수 있습니다. 가드는 지정된 순서대로 실행되며, 모든 가드가 true를 반환해야 요청이 처리됩니다.

// 여러 가드 조합하기
@UseGuards(JwtAuthGuard, RolesGuard, PolicyGuard)
@Roles('ADMIN')
@UsePolicy(SomePolicy)
@Get('protected-resource')
getProtectedResource() {
  return 'This is a protected resource';
}

위 예시에서 요청이 처리되기 위해서는 다음 조건을 모두 만족해야 합니다:

  1. 유효한 JWT 토큰이 제공되어야 함(JwtAuthGuard)
  2. 사용자는 'ADMIN' 역할을 가지고 있어야 함(RolesGuard)
  3. 정책 검사를 통과해야 함(PolicyGuard, SomePolicy)

8. 전역 가드 등록하기

일부 가드는 애플리케이션 전체에 적용하는 것이 유용할 수 있습니다. NestJS에서는 전역 가드를 쉽게 등록할 수 있습니다:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 전역 가드 등록
  app.useGlobalGuards(new JwtAuthGuard());
  
  await app.listen(3000);
}
bootstrap();

그러나 useGlobalGuards()로 등록된 가드는 의존성 주입을 활용할 수 없다는 한계가 있습니다. 이 경우 다음과 같이 모듈 컨텍스트에서 가드를 제공해야 합니다:

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';

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

9. 가드와 다른 핸들러의 통합

NestJS의 가드는 파이프, 인터셉터, 예외 필터와 함께 작동할 수 있습니다. 각 핸들러는 특정 순서로 실행됩니다:

  1. 전역 미들웨어
  2. 모듈 및 라우트 미들웨어
  3. 전역 가드
  4. 컨트롤러 가드
  5. 라우트 가드
  6. 전역 인터셉터(컨트롤러 진입 전)
  7. 컨트롤러 인터셉터(컨트롤러 진입 전)
  8. 라우트 인터셉터(컨트롤러 진입 전)
  9. 전역 파이프
  10. 컨트롤러 파이프
  11. 라우트 파이프
  12. 라우트 매개변수 파이프
  13. 컨트롤러(라우트 핸들러)
  14. 서비스(라우트 핸들러에서 호출된 경우)
  15. 라우트 인터셉터(응답 부분)
  16. 컨트롤러 인터셉터(응답 부분)
  17. 전역 인터셉터(응답 부분)
  18. 예외 필터(예외가 발생한 경우)

인터셉터와 가드 함께 사용하기

// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const userId = request.user?.id; // JwtAuthGuard에 의해 설정된 사용자 정보
    
    console.log(`User ${userId} is accessing ${request.url}`);
    
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`Request completed in ${Date.now() - now}ms`)),
      );
  }
}

사용 예시:

@UseGuards(JwtAuthGuard)
@UseInterceptors(LoggingInterceptor)
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

10. 모범 사례와 패턴

세분화된 권한 관리

역할과 권한을 분리하여 더 세분화된 접근 제어를 구현할 수 있습니다:

// permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: string[]) => 
  SetMetadata(PERMISSIONS_KEY, permissions);
// permissions.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      PERMISSIONS_KEY,
      [context.getHandler(), context.getClass()],
    );
    
    if (!requiredPermissions) {
      return true;
    }
    
    const { user } = context.switchToHttp().getRequest();
    
    return requiredPermissions.every(permission => 
      user.permissions?.includes(permission)
    );
  }
}

인증 상태에 따른 선택적 가드

일부 엔드포인트는 인증된 사용자와 인증되지 않은 사용자 모두에게 접근 가능해야 할 수 있습니다. 이를 위한 선택적 인증 가드를 구현할 수 있습니다:

// optional-jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
  // 인증 실패 시 예외를 던지지 않고 요청을 계속 처리함
  handleRequest(err, user, info) {
    return user;
  }
}

리소스 소유자 확인 유틸리티

리소스 소유자를 확인하는 로직이 반복될 수 있습니다. 이를 재사용 가능한 유틸리티로 추출할 수 있습니다:

// resource-owner.util.ts
import { ForbiddenException } from '@nestjs/common';

export class ResourceOwnerUtil {
  static checkOwnership(userId: number, resourceOwnerId: number, errorMessage?: string) {
    if (userId !== resourceOwnerId) {
      throw new ForbiddenException(
        errorMessage || 'You do not have permission to access this resource'
      );
    }
    return true;
  }
}

사용 예시:

@Put(':id')
@UseGuards(JwtAuthGuard)
async update(@Param('id') id: string, @Body() updateDto, @Request() req) {
  const article = await this.articlesService.findOne(id);
  
  // 리소스 소유자 확인
  ResourceOwnerUtil.checkOwnership(
    req.user.id, 
    article.authorId,
    'You can only edit your own articles'
  );
  
  return this.articlesService.update(id, updateDto);
}

슈퍼 관리자 권한

모든 리소스에 접근할 수 있는 슈퍼 관리자 역할을 구현할 수 있습니다:

// 역할 가드 확장
canActivate(context: ExecutionContext): boolean {
  const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
    context.getHandler(),
    context.getClass(),
  ]);
  
  if (!requiredRoles) {
    return true;
  }
  
  const { user } = context.switchToHttp().getRequest();
  
  // 슈퍼 관리자는 항상 접근 가능
  if (user.roles?.includes('SUPER_ADMIN')) {
    return true;
  }
  
  // 일반적인 역할 확인 로직
  return requiredRoles.some(role => user.roles?.includes(role));
}

캐싱 및 성능 최적화

권한 확인 로직이 복잡하거나 데이터베이스 쿼리가 포함된 경우, 결과를 캐싱하여 성능을 향상시킬 수 있습니다:

// caching-policy.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Redis } from 'ioredis';

@Injectable()
export class CachingPolicyGuard implements CanActivate {
  constructor(@InjectRedis() private readonly redis: Redis) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const resourceId = request.params.id;
    
    // 캐시 키 생성
    const cacheKey = `policy:${user.id}:${resourceId}`;
    
    // 캐시에서 권한 결과 확인
    const cachedResult = await this.redis.get(cacheKey);
    
    if (cachedResult !== null) {
      return cachedResult === 'true';
    }
    
    // 캐시에 없는 경우 복잡한 권한 확인 로직 수행
    const hasPermission = await this.checkPermission(user, resourceId);
    
    // 결과 캐싱 (30분 유효)
    await this.redis.set(cacheKey, String(hasPermission), 'EX', 1800);
    
    return hasPermission;
  }
  
  private async checkPermission(user: any, resourceId: string): Promise<boolean> {
    // 복잡한 권한 확인 로직 (데이터베이스 쿼리 등)
    return true; // 예시
  }
}

동적 권한 부여 시스템

애플리케이션의 복잡성이 증가함에 따라 데이터베이스에 권한을 저장하고 동적으로 로드하는 것이 유용할 수 있습니다:

// dynamic-permission.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Permission } from './entities/permission.entity';
import { Role } from './entities/role.entity';

@Injectable()
export class PermissionService {
  constructor(
    @InjectRepository(Permission)
    private permissionRepository: Repository<Permission>,
    @InjectRepository(Role)
    private roleRepository: Repository<Role>,
  ) {}

  async getUserPermissions(userId: number): Promise<string[]> {
    const query = `
      SELECT DISTINCT p.code
      FROM permissions p
      JOIN role_permissions rp ON p.id = rp.permission_id
      JOIN user_roles ur ON rp.role_id = ur.role_id
      WHERE ur.user_id = ?
    `;
    
    const permissions = await this.permissionRepository.query(query, [userId]);
    return permissions.map(p => p.code);
  }
  
  async hasPermission(userId: number, permissionCode: string): Promise<boolean> {
    const userPermissions = await this.getUserPermissions(userId);
    return userPermissions.includes(permissionCode);
  }
}

오류 메시지 사용자 정의

가드가 접근을 거부할 때 더 명확한 오류 메시지를 제공하여 사용자 경험을 향상시킬 수 있습니다:

// custom-forbidden-exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class CustomForbiddenException extends HttpException {
  constructor(message: string = 'Access denied', details?: any) {
    super(
      {
        statusCode: HttpStatus.FORBIDDEN,
        error: 'Forbidden',
        message,
        details,
      },
      HttpStatus.FORBIDDEN,
    );
  }
}
// 역할 가드에서 사용
if (!hasRequiredRoles) {
  throw new CustomForbiddenException(
    `Access denied: insufficient permissions`,
    {
      requiredRoles,
      userRoles: user.roles,
      resource: context.getClass().name + '.' + context.getHandler().name,
    },
  );
}

결론

NestJS의 가드는 인증 및 권한 부여 시스템을 구현하기 위한 강력하고 유연한 메커니즘을 제공합니다. JWT 기반 인증, 역할 기반 접근 제어 및 정책 기반 접근 제어와 같은 다양한, 심지어 복잡한 보안 요구사항도 가드를 통해 우아하게 구현할 수 있습니다.

적절한 가드 구현을 통해 애플리케이션의 보안을 강화하고, 핵심 비즈니스 로직에서 인증 및 권한 부여 로직을 분리함으로써 코드의 가독성과 유지보수성을 향상시킬 수 있습니다. 또한 NestJS의 모듈식 아키텍처와 의존성 주입 시스템은 가드의 재사용성과 테스트 용이성을 높이는 데 크게 기여합니다.

다음 글에서는 NestJS의 인터셉터(Interceptors)에 대해 알아보고, 요청-응답 생명주기를 조작하여 로깅, 캐싱, 응답 변환 등의 기능을 구현하는 방법을 살펴보겠습니다.

반응형