웹 애플리케이션에서 인증과 권한 부여는 필수적인 보안 요소입니다. 인증은 사용자가 자신이 주장하는 대로 맞는지 확인하는 과정이고, 권한 부여는 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다. NestJS는 이러한 기능을 쉽게, 그리고 체계적으로 구현할 수 있는 '가드(Guards)'라는 메커니즘을 제공합니다.
이 글에서는 NestJS 가드의 동작 원리와 가드를 활용하여 애플리케이션에 인증 및 권한 부여 시스템을 구현하는 방법을 자세히 살펴보겠습니다.
목차
- 가드의 개념과 역할
- 가드의 실행 컨텍스트
- 기본 인증 가드 구현하기
- JWT 기반 인증 시스템 구축하기
- 역할 기반 접근 제어(RBAC) 구현하기
- 정책 기반 접근 제어(PBAC) 구현하기
- 가드 조합하기
- 전역 가드 등록하기
- 가드와 다른 핸들러의 통합
- 모범 사례와 패턴
1. 가드의 개념과 역할
NestJS에서 가드는 특정 라우트 핸들러가 요청을 처리할 수 있는지 여부를 결정하는 역할을 합니다. 이는 미들웨어와 유사하지만, 더 강력하고 정교한 기능을 제공합니다. 가드는 다음과 같은 특성을 가집니다:
- 실행 컨텍스트에 접근: 가드는 ExecutionContext 인스턴스에 접근할 수 있어 현재 실행 중인 메서드에 대한 상세한 정보를 얻을 수 있습니다.
- 선언적 사용: 전역, 컨트롤러 또는 메서드 수준에서 선언적으로 적용할 수 있습니다.
- 조건부 실행: 가드는 canActivate() 메서드를 통해 요청의 처리 여부를 결정할 수 있습니다.
- 의존성 주입: 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';
}
위 예시에서 요청이 처리되기 위해서는 다음 조건을 모두 만족해야 합니다:
- 유효한 JWT 토큰이 제공되어야 함(JwtAuthGuard)
- 사용자는 'ADMIN' 역할을 가지고 있어야 함(RolesGuard)
- 정책 검사를 통과해야 함(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의 가드는 파이프, 인터셉터, 예외 필터와 함께 작동할 수 있습니다. 각 핸들러는 특정 순서로 실행됩니다:
- 전역 미들웨어
- 모듈 및 라우트 미들웨어
- 전역 가드
- 컨트롤러 가드
- 라우트 가드
- 전역 인터셉터(컨트롤러 진입 전)
- 컨트롤러 인터셉터(컨트롤러 진입 전)
- 라우트 인터셉터(컨트롤러 진입 전)
- 전역 파이프
- 컨트롤러 파이프
- 라우트 파이프
- 라우트 매개변수 파이프
- 컨트롤러(라우트 핸들러)
- 서비스(라우트 핸들러에서 호출된 경우)
- 라우트 인터셉터(응답 부분)
- 컨트롤러 인터셉터(응답 부분)
- 전역 인터셉터(응답 부분)
- 예외 필터(예외가 발생한 경우)
인터셉터와 가드 함께 사용하기
// 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)에 대해 알아보고, 요청-응답 생명주기를 조작하여 로깅, 캐싱, 응답 변환 등의 기능을 구현하는 방법을 살펴보겠습니다.
'NestJS' 카테고리의 다른 글
8. NestJS 파이프(Pipes): 입력 유효성 검사와 변환 (0) | 2025.03.31 |
---|---|
7. NestJS 예외 필터(Exception Filters): 오류 처리 전략 (0) | 2025.03.30 |
6. NestJS 미들웨어: 요청 처리 파이프라인 이해하기 (0) | 2025.03.30 |
5. NestJS 모듈: 애플리케이션 구조화와 모듈간 의존성 관리 (0) | 2025.03.30 |
4. NestJS 프로바이더와 서비스: 비즈니스 로직 분리하기 (0) | 2025.03.30 |