[NestJS | Docs] Guard 알아보기 (feat. athentication & athorization: 인증과 인가)
2024.02.14- -
1. Guards
guard는 NestJS에서 @Injectable() 데코레이터가 붙은 클래스로 CanActivate 인터페이스를 구현합니다.
이 친구도 역시 단일 책임을 갖는데요. 런타임에 존재하는 특정 조건(permissions, roles, ACL 등)에 따라 특정 요청이 라우트 핸들러에 의해 처리될지 여부를 결정하는 책임을 갖습니다. 이는 일반적으로 authorization을 처리하기 위해 사용합니다.
권한부여-authorization(그리고 일반적으로 함께 사용되는 사촌격인 authentication)는 일반적으로 기존 Express 애플리케이션의 미들웨어가 처리해 왔습니다. 토큰 유효성 검사(token validation) 및 요청 객체에 속성을 추가하는 작업은 특정 route 컨텍스트(및 해당 metadata)와 밀접하게 연결되어 있는 것이 아니기 때문에 미들웨어는 인증에 꽤 적합한 선택이었습니다.
하지만 미들웨어는 본질적으로 멍청하다는 문제가 있습니다. 미들웨어는 next() 함수를 호출한 후 어떤 핸들러가 실행될지 모르기 때문입니다.
반면에 guard는 ExecutionContext 인스턴스에 액세스할 수 있기 때문에 다음에 실행될 내용에 대해 정확히 알 수 있습니다.
exception filter, pipe, interceptor와 마찬가지로 요청/응답 주기의 정확한 지점에 프로세스 로직을 삽입할 수 있도록 설계되어 있으며 이를 선언적으로 처리할 수 있습니다. 따라서 코드를 간결하고 선언적으로 유지하는 데 도움이 됩니다.
guard는 모든 미들웨어 이후에 실행되며, interceptor나 pipe보다는 이전에 실행됩니다.
1-1. Authorization guard
앞서 언급했듯, authorization(권한 부여)은 호출자(일반적으로 인증된 특정 사용자)에게 충분한 permission이 있는 경우에만 특정 라우트를 사용할 수 있도록 해야하기 때문에 이는 guard의 매우 유용한 사례 중 하나라고 할 수 있습니다.
이제 만들어 볼 AuthGuard는 인증된 사용자(요청 헤더에 유효 토큰이 첨부된 사용자)를 가정합니다. 토큰을 추출해서 유효성 검사를 하고 이 정보를 바탕으로 요청을 계속해서 진행할 수 있는지에 대한 여부를 판단합니다.
// 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);
}
}
vaidateRequest() 함수 내부 로직은 필요에 따라 간단할 수도 혹은 정교할 수도 있습니다. 이 예제에서의 요점은 guard가 요청/응답 주기에서 어떻게 들어맞는지를 보는 것입니다.
모든 guard는 canActivate() 함수를 구현해야 합니다. 이 함수는 현재 요청의 허용 가능 여부를 나타내는 boolean을 반환합니다. 이 함수는 응답을 동기 및 비동기(Promise or Observable)식으로 반환이 가능합니다. Nest는 반환값을 사용하여 다음과 같이 작업을 제어하게 될 것입니다.
- true 반환 시, 요청이 진행된다.
- false 반환 시, Nest가 해당 요청을 거부한다.
1-2. Execution Context
canActivate() 함수는 단일 인자인 ExecutionContext 인스턴스 하나만을 받습니다. ExecutionContext는 ArgumentsHost로부터 상속됩니다. 위의 예제에서는 이전에 사용한 것과 동일한 헬퍼 메서드를 ArgumentsHost에 정의하여 요청 객체에 대한 참조를 가져오고 있습니다.
ArgumentsHost를 extend함으로써 ExecutionContext는 현재 실행 프로세스에 대한 추가 세부 정보를 제공하는 몇 가지 새로운 헬퍼 메서드를 추가적으로 사용할 수 있게 됩니다. 이러한 세부 정보들은 광범위한 컨트롤러 및 메서드, 실행 컨텍스트에서 작동될 수 있도록 도와주는 보다 범용적인 guard를 구축하는 데 유용하게 사용됩니다.
1-3. Role-based authentication (역할 기반 인증)
특정 역할을 가진 사용자에게만 액세스를 허용하는 보다 유용한 guard를 구축해 보겠습니다. 기본 guard 템플릿으로 시작하여 다음 섹션에서 이를 기반으로 구축해 나가는 식으로 진행하겠습니다.
우선 지금은 모든 요청이 그냥 진행되도록 허용합니다:
// role.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; // 그냥 허용
}
}
1-4. Binding guards (guard 바인딩)
guard는 pipe나 exception filter와 마찬가지로 컨트롤러 범위, 메서드 범위 또는 전역 범위에서 적용할 수 있습니다.
아래에서는 @UseGuards() 데코레이터를 사용하여 컨트롤러 범위의 guard로 설정해 주었습니다. 이 데코레이터는 적용하고자 하는 guard에 대한 단일 인자를 받거나 쉼표로 구분된 guard 인자의 리스트를 받을 수 있습니다. 이를 통해 하나의 선언만으로도 적절한 guard 리스트를 쉽게 적용할 수 있습니다.
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
위 코드를 보면 인스턴스 대신 RolesGuard라는 클래스명을 전달하여 인스턴스화에 대한 책임을 프레임워크에게 맡기는 의존성 주입을 활용한 것을 보실 수 있습니다.
물론 pipe나 exception filter와 마찬가지로 인자에서 직접 인스턴스를 생성하는 in-place 인스턴스를 통해 전달할 수도 있습니다. 아래와 같이 말이죠.
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
이렇게 @UseGuard()를 사용하면 이 컨트롤러에서 선언된 모든 핸들러에게 지정한 guard를 적용하게 됩니다.
만약 global guard(전역 guard)를 사용하려면 main.ts 파일에서 Nest 애플리케이션 인스턴스의 useGlobalGuards() 메서드를 사용하면 됩니다:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
항상 전역적으로 무언가 적용을 할 때는 의존성 주입을 반드시 고려해주어야 합니다.
global guard는 전체 애플리케이션의 모든 컨트롤러와 모든 라우트 핸들러에 대해서 사용됩니다. 하지만 코드 상에서는 의존성 주입 관점에서 봤을 때, 모듈 외부에서 등록된 global guard이기 때문에 모든 모듈의 context에서 벗어나 수행되는 코드이기 때문에 의존성을 주입할 수가 없는 상황인 것입니다.
그래서 이를 해결하려면 다음과 같은 구성을 사용하여 특정 모듈에서 직접 guard를 설정해야 합니다:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
다만, 이러한 방식을 사용할 때는 guard에 대한 의존성 주입을 수행할 때, 이것이 사용되는 모듈과는 관계없이 guard는 실제로 전역이라는 점에 유의해야 합니다(즉 AppModule이 아니어도 특정 모듈에서 등록된 guard는 전역 guard임).
그러면 이 guard는 어느 모듈에 추가해주어야 할까요? guard가 정의된 범위의 모듈을 선택하면 됩니다.
- 또한 위의 예처럼 useClass 만이 custom provider를 처리하는 유일한 방법은 아닙니다. 이에 대한 내용은 여기에서 자세히 알아보실 수 있습니다.
1-5. Setting roles per handler (핸들러별 역할 설정)
우리가 만든 RolesGuard는 잘 작동은 하지만 무조건 허용하고 있기 때문에 아직 어떤 기능을 하는 것은 아닙니다.
guard가 가지고 있는 것 중에 가장 강력한 execution context를 활용하지 못하고 있습니다. 그렇기 때문에 아직 role(역할)이나 각 핸들러에 허용되는 역할에 대해서는 무지한 상태입니다.
예를 들어, CatsController는 라우트마다 다른 permission 체계를 가지도록 하고 싶다고 합시다. 즉, 일부 라우트는 'admin' 사용자만 허용되도록 하고, 다른 일부는 모든 사용자에게 개방되게끔 하고 싶은 것입니다.
그러면 어떻게 해야지 유연하고 재사용 가능한 방식으로 이러한 role들을 라우트와 매칭시킬 수 있을까요?
여기서 custom metatdata가 중요한 역할을 합니다(여기를 참고하세요). Nest는 Reflector#createDecorator 정적 메서드를 통해 생성된 데코레이터나, 기본으로 제공되는 @SetMetadata() 데코레이터를 통해 custom metatdata를 라우트 핸들러에 첨부할 수 있는 기능을 제공합니다.
예를 들어, 핸들러에 메타데이터를 첨부하는 Reflector#createDecorator 메서드를 사용하여 @Roles()라는 커스텀 데코레이터를 만들어 보겠습니다. Reflector는 프레임워크에서 @nestjs/core 패키지를 통해 기본 제공됩니다.
// roles.decorator.ts
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
여기서 구현한 Roles 데코레이터는 string[] 타입의 단일 인자를 받는 함수입니다. 이제 이 데코레이터를 사용하려면 핸들러 부분에 '@' (어노테이션)와 해당 데코레이터의 이름을 달아주기만 하면 됩니다.
// cats.controller.ts
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
'admin' 역할이 있는 사용자만 이 라우트에 액세스할 수 있도록 하기 위해 create() 메서드에 @Roles() 데코레이터 메타데이터를 통해 'admin'을 지정한 것을 볼 수 있습니다.
이에 대한 대안으로는 Reflector#createDecorator 메서드를 사용하는 대신 기본으로 제공되는 @SetMetadata() 데코레이터를 사용할 수 있고 실제로 이 방식이 더 권장되는 방식이라고 합니다. 이에 대한 사용은 아래에서 다시 보도록 하겠습니다.
1-6. Putting it all together
이제 다시 돌아가서, 이 Role데[코레이터가 제공하는 데이터와 RolesGuard와 연결해 보도록 합시다. 아직까지도 단순히 모든 경우에 true를 반환하기 때문에 모든 요청이 진행되도록 허용되고 있습니다.
여기서 우리는 현재 사용자에게 할당된 role('admin')과 현재 처리 중인 라우트가 실제로 요구하는 role을 비교하여 반환 값을 조건에 따라 true/false를 반환하도록 만들고 싶습니다. 그러기 위해선 현재 처리 중인 라우트가 요구하는 role과 사용자의 role을 비교하여 허용할지 말지를 판단하는 부분을 구현해야 합니다
먼저, 라우트가 요구하는 role을 가져오기 위해서 다음과 같이 Reflector 헬퍼 클래스를 다시 사용하겠습니다:
// role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';
@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);
}
}
node.js 생태계에서는 권한이 부여된 사용자(authenticated user)를 request 객체에 첨부하는 것이 일반적입니다. 따라서 위의 예제 코드에서는 request.user에 사용자 인스턴스와 더불어 허용된 역할에 대한 값(allowed roles)이 포함되어 있다고 가정합니다.
애플리케이션에서 custom authentication guard(또는 미들웨어)가 이러한 연결을 만들어 줍니다.
권한이 부족한 사용자가 엔드포인트를 요청하면 nest는 자동으로 다음과 같은 응답을 반환하게 됩니다.
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
이러한 결과가 나오는 그 이면에는 guard가 false를 반환할 때, 프레임워크가 ForbiddenException을 던진다는 사실을 인지해야 합니다. 만약 다른 error 응답을 반환하고 싶다면 직접 Error 인스턴스를 반환하도록 하면 됩니다:
throw new UnauthorizedException();
guard에서 던지는 모든 예외는 exception layer(global exception filter 및 현재 컨텍스트에 적용되는 모든 exception filter)에서 처리됩니다.
2. Athentication (인증)
athentication(인증)은 대부분의 애플리케이션 구축에 있어서 필수적인 부분입니다. 로그인/회원가입이 없는 서비스는 요즘 거의 없기 때문이죠.
인증을 처리하기 위한 다양한 접근 방식과 전략들이 존재합니다. 하지만 특정 프로젝트에 적용되는 접근 방식은 특정 애플리케이션 요구 사항에 따라 모두 다를 것입니다. 이번 장에서는 다양한 요구 사항에 맞게 적용할 수 있는 인증(authentication)에 대한 몇 가지 접근 방식에 대해서 알아보고자 합니다.
먼저 요구 사항을 정리하면 다음과 같습니다. 클라이언트는 username과 password를 통해 인증을 시작합니다. 인증이 완료되면 서버에서는 인증을 증명하기 위해서 클라이언트가 후속 요청을 할 때 인증 헤더에 bearer token 형태로 전송될 수 있는 JWT(JSON Web Token)를 발급합니다. 또한 유효한 JWT가 포함된 요청에만 액세스할 수 있는 보호된 라우트를 생성합니다.
첫 번째 요구 사항인 사용자 인증부터 시작하겠습니다. 이후 JWT를 발행하여 이를 확장하고 마지막으로 요청에 대해 유효한 JWT를 갖는지 확인하는 보호된 라우트(protected route)를 생성합니다.
2-1. Creating an authentication module (인증 모듈 생성하기)
먼저 AuthModule을 생성하고 그 안에 AuthService와 AuthController를 생성합니다. AuthService를 사용하여 인증 로직을 구현할 것이고, AuthController를 사용하여 인증 엔드포인트를 제공할 것입니다.
$ nest g module auth
$ nest g controller auth
$ nest g service auth
AuthService를 구현하면서도, user와 관련된 작업은 UsersService에 캡슐화하는 것이 유용하기 때문에 user와 관련된 모듈과 서비스 또한 생성해 줍시다.
$ nest g module users
$ nest g service users
이렇게 생성된 파일의 기본 내용을 아래 코드와 같이 작성해줍니다. 예제 애플리케이션에서는, UsersService는 단순히 하드코딩된 인메모리 user 리스트와 username으로 사용자를 검색하는 find 메서드를 유지보수합니다. 물론 실제 앱에서는 원하는 라이브러리(ex. TypeORM, Sequealize, Mongoosse 등)를 사용하여 user model과 persistence layer를 구축하는 것이 일반적입니다.
// users/users.service.ts
import { Injectable } from '@nestjs/common';
// This should be a real class/interface representing a user entity
export type User = any;
@Injectable()
export class UsersService {
private readonly users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'maria',
password: 'guess',
},
];
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
UserModule에서 수정해야 할 것은 해당 모듈의 외부에서도 UsersService에 접근할 수 있도록 @Module 데코레이터의 exports 배열에 UsersService를 추가하여 해당 프로바이더를 내보내는 것입니다.
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
2-2. Implementing the "Sign in" endpoint (로그인 엔드포인트 구현하기)
AuthService는 UsersService를 통해 사용자를 가져오고 비밀번호를 확인하는 작업을 수행합니다. 이를 위한 signIn() 메서드를 생성합니다.
아래 코드에서는 ES6의 문법인 스프레드 연산자(spread operator)를 사용하여 편리하게 user 객체로부터 비밀번호 속성과 그 나머지 속성을 의미하는 result와 분리시켜 result만 반환하도록 하였습니다. 이는 user 객체를 반환할 때 비밀번호나 기타 보안 키와 같은 민감한 필드는 노출하지 않기 위한 일반적인 관행입니다.
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async signIn(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const { password, ...result } = user;
// TODO: Generate a JWT and return it here
// instead of the user object
return result;
}
}
WARNING
물론 실제 애플리케이션에서는 비밀번호를 일반 텍스트로 저장하지 않습니다. 대신 salted one-way hash algorithm이 포함된 bcrypt와 같은 라이브러리를 사용합니다. 이 접근 방식을 사용하면 해시된 비밀번호만 저장한 다음 저장된 비밀번호를 수신 비밀번호의 해시된 버전과 비교하므로 사용자 비밀번호가 일반 텍스트로 저장되거나 노출되지 않습니다. 예제 앱은 단순하도록 유지하기 위해 이러한 절대적인 의무를 위반하여 일반 텍스트를 사용한 것이므로 실제 앱에서는 절대 이렇게 하면 안됩니다.
이제 AuthModule을 다음과 같이 UsersModule을 import하도록 수정합니다.
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
이제 AuthController에 signIn() 핸들러를 추가해 보겠습니다. 이 메서드는 클라이언트에서 사용자를 인증하기 위해 호출됩니다. 이 메서드는 요청 body에서 username과 password를 받고, 사용자가 인증되면 JWT 토큰을 반환합니다.
// auth/auth.controller.ts
import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
}
이상적으로는 Record<string, any> 유형을 사용하는 대신 DTO 클래스(ex. SignInDto)를 사용하여 요청 body의 모습을 정의하는 것이 좋습니다.
2-3. JWT token
이제 인증 시스템의 JWT 부분으로 넘어갈 준비가 되었습니다. 다시 한 번 요구 사항을 검토하고 이를 구체화해 보겠습니다:
사용자가 username/password로 인증할 수 있도록 허용하여 보호된 API 엔드포인트에 대해 후속 호출에서 계속 사용할 수 있는 JWT를 발급하여 사용자에게 반환해 주어야 합니다.
이러한 요구 사항을 충족하기 위해 진행했던 선행 작업들은 순조롭게 진행되었습니다. 이를 마무리하기 위해 우선 JWT를 발급하는 코드를 작성합니다.
bearer token으로 유효한 JWT인지의 유무를 판단하여 보호되는 API 라우트를 만들어야 합니다. JWT 요구 사항을 지원하기 위해 하나의 추가적인 패키지를 설치해줍니다:
npm install --save @nestjs/jwt
service를 깔끔하게 모듈화하기 위해 authService에서 JWT 생성을 처리하겠습니다. auth 폴더에서 auth.service.ts 파일 만들고 열어서 JwtService를 삽입한 다음 아래와 같이 JWT 토큰을 생성하도록 signIn 메서드를 수정합니다.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async signIn(
username: string,
pass: string,
): Promise<{ access_token: string }> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const payload = { sub: user.userId, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}
user 객체 속성의 하위 집합에서 JWT를 생성하는 signAsync() 함수를 @nestjs/jwt 라이브러리를 사용하여 단일 access_token 속성을 가진 간단한 객체로 반환합니다.
- 참고: JWT 표준과 일관성을 유지하기 위해 sub라는 속성 이름을 선택하여 userId 값을 보유합니다.
이제 새 의존성을 가져오기 위해 AuthModule을 업데이트하고 JwtModule을 구성해야 합니다.
먼저 auth 폴더에 constants.ts를 만들고 다음 코드를 추가합니다:
export const jwtConstants = {
secret: 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};
이는 JWT 서명 및 확인(sign & verify) 단계 간에 key를 공유할 수 있게됩니다.
WARNING
키는 공개적으로 노출하면 안됩니다. 여기서는 코드가 수행하는 작업을 명확히 하기 위해 공개했지 실제 서비스 운영 단계에서는 비밀 저장소, 환경 변수 또는 구성 서비스와 같은 적절한 방법을 사용하여 이 키를 숨겨야 합니다.
이제 auth 폴더에서 auth.module.ts 파일을 다음과 같이 수정합니다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
위와 같이 더 편리한 작업을 위해 JwtModule을 글로벌로 등록할 수 있습니다. 이렇게 함으로써 애플리케이션의 다른 곳에서 JwtModule을 import 하지 않아도 됩니다.
register()을 사용하여 구성(configuration) 객체를 전달하여 JwtModule을 구성합니다.
이제 cURL을 사용하여 해당 라우트를 다시 테스트해 보겠습니다. UsersService에 하드코딩되었던 사용자 리스트 중 한 사용자로 테스트해보면 다음과 같이 결과가 나옵니다.
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
$ # Note: 위의 JWT는 너무 길어 잘라서 표현된 것입니다.
2-4. Implementing the authentication guard (인증 가드 구현하기)
이제 마지막 요구 사항인 요청에서 유효한 JWT를 요구함으로써 엔드포인트를 보호하는 문제를 구현해봅시다.
그러기 위해서 라우트를 보호하는 데 사용할 수 있는 AuthGuard를 구현합니다.
// auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
이제 보호하고자 하는 라우트에 AuthGuard를 등록하여 해당 라우트를 보호할 수 있습니다.
auth.controller.ts 파일을 열고 아래 내용과 같이 업데이트합니다:
// auth/auth.controller.ts
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
방금 생성한 AuthGuard를 GET /profile 라우트에 적용하여 보호하였습니다.
앱이 실행 중인지 확인하고 cURL을 사용하여 경로를 테스트해봅시다.
$ # GET /profile
$ curl http://localhost:3000/auth/profile
{"statusCode":401,"message":"Unauthorized"}
$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."}
$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/auth/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
{"sub":1,"username":"john","iat":...,"exp":...}
AuthModule에서 JWT의 만료가 60초가 되도록 구성했습니다. 이는 사실 매우 짧은 만료 시간이지만, 토큰 만료 및 새로 고침에 대한 세부 사항을 다루는 것은 이 문서의 범위를 벗어나기에 임의로 설정한 값입니다.
그럼에도 이렇게 짧게 설정한 이유는 JWT의 중요한 특성을 보여주기 위해서 입니다. 인증 후 60초를 기다렸다가 GET /auth/profile 요청을 다시 시도하면 401 "Unauthorized" 응답을 받게 되는 것을 확인하실 수 있을 겁니다. 이는 @nestjs/jwt가 자동으로 JWT의 만료 시간을 확인하기 때문에 애플리케이션에서 이를 확인해야 하는 수고를 덜어주게 됩니다.
이로써 JWT 인증 구현을 완료했습니다. 이제 JavaScript 클라이언트(예: Angular/React/Vue) 및 기타 JavaScript 앱이 인증하고 안전하게 API 서버와 통신할 수 있습니다.
2-5. Enable authentication globally (전역적인 인증 구현하기)
그런데 사실 대부분의 서비스에서는 거의 모든 기능이 사용자 인증이 있어야지 사용할 수 있는 기능입니다. 그렇기 때문에 대부분의 엔드포인트들은 default로 보호되어야 하는 경우가 일반적인데요.
이 경우에 각 컨트롤러 위에 @UseGuards() 데코레이터를 일일히 달아주는 것이 아니라 authentication guard를 gloabl(전역) gaurd로 등록하되, 간단한 플래그를 사용해서 특정 라우트들에만 public으로 지정하여 guard 없이 액세스 할 수 있도록 하는 방식을 사용할 수 있습니다.
먼저 다음과 같은 구성을 사용하여 AuthGuard를 global guard로 등록합니다.(아무 모듈에서 등록 가능하지만, 아래 예에서는 AuthModule에 등록함)
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
이 설정이 완료되면, Nest는 자동으로 모든 엔드포인트에 AuthGuard를 바인딩합니다.
그렇다면 이제는 선언적으로 특정 라우트를 public으로 만드는 메커니즘을 구현해야 합니다. 이를 위해 SetMetadata 데코레이터 팩토리 함수를 사용하여 custom decorator를 만들 수가 있습니다.
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
위 코드를 보면 두 개의 상수를 export 하고있는 것을 볼 수 있습니다. 하나는 IS_PUBLIC_KEY라는 메타데이터 키이고, 다른 하나는 Public이라고 부르는 우리가 만들 데코레이터 그 자체를 의미하는 변수입니다(프로젝트 성격에 맞도록 이름을 SkipAuth 또는 AllowAnon으로 지정할 수도 있습니다).
이제 사용자 정의 @Public() 데코레이터를 만들었으므로 다음과 같이 특정 메서드를 데코레이션하는 데 사용할 수 있습니다:
@Public()
@Get()
findAll() {
return [];
}
마지막으로, AuthGuard에서 "isPublic" 메타데이터가 발견되면 true를 반환하여 요청이 허용되도록 수정해야 합니다. 이 메타데이터를 가져오기 위해선 앞서 guard에서 본 것과 같이 Reflector 클래스를 사용합니다.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService, private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
// 💡 즉시 true를 반환합니다.
return true;
}
// 만약 isPublic 키 값이 존재하지 않는다면 똑같이 토큰 인증을 거칩니다.
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
// 💡 payload를 라우트 핸들러에서 접근할 수 있도록
// 해당 값을 request object에 할당하고 있습니다.
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
2-7. Passport integration
Passport는 커뮤니티에서 잘 알려져 있고 많은 프로덕션 애플리케이션에서 성공적으로 사용되고 있는는 가장 인기 있는 node.js 인증 라이브러리입니다.
이 라이브러리를 Nest 애플리케이션과 통합하는 방법은 @nestjs/passport 모듈을 사용하면 간단합니다. 이에 대한 내용은 다른 포스팅에서 따로 다뤄보도록 하겠습니다.
3. Athorization (권한 부여)
authorization은 사용자가 수행할 수 있는 작업을 결정하는 프로세스를 일컫는 말입니다. 예를 들어, admin 사용자는 글을 작성하거나 편집, 삭제 등을 할 수 있을 것이지만, admin이 아닌 사용자는 오직 게시물을 읽을 수 있는 권한만 부여되는 상황을 말할 수 있겠습니다.
authorization에는 authentication 메커니즘을 필요로 합니다.
권한 부여를 처리하기 위하여 다양한 접근 방식과 전략이 존재합니다. 특정 프로젝트에 적용되는 접근 방식은 특정 애플리케이션 요구 사항에 따라 모두 다릅니다. 이번 장에서는 다양한 요구 사항에 맞게 적용할 수 있는 인증에 대한 몇 가지 접근 방식에 대해서 소개해보고자 합니다.
3-1. Basic RBAC implementation
역할 기반 액세스 제어(Role-based access control); RBAC는 역할(role)과 권한(privileges)을 중심으로 정의된 정책 중릭적인 액세스 제어 메커니즘입니다. 이 장에서는 Nest guard를 사용하여 아주 기본적인 RBAC 메커니즘을 구현해 볼 것입니다.
먼저 시스템에서 역할을 나타내는 역할 enum을 정의해 보겠습니다.
export enum Role {
User = 'user',
Admin = 'admin',
}
보다 정교한 시스템에서는 데이터베이스 내에 이 역할들을 저장하거나 외부 인증 provider로부터 role을 가져오기도 합니다.
이 enum을 기반으로 @Roles() 데코레이터를 만들 수 있습니다. 이 데코레이터를 사용하면 특정 리소스에 액세스하는 데 필요한 role을 명시적으로 지정할 수 있습니다.
// roels.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
이제 사용자 지정 @Roles() 데코레이터가 있으니, 이를 사용하여 모든 라우트 핸들러를 decorate 할 수 있습니다.
@Post()
@Roles(Role.Admin)
create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
마지막으로, 현재 사용자에게 할당된 역할을 현재 처리 중인 라우트가 요구하는 실제 역할과 비교하는 RolesGuard 클래스를 생성합니다.
라우트의 역할(사용자 정의 메타데이터)을 가져오기 위해 역시나 프레임워크에서 기본 제공하는 Reflector 헬퍼 클래스를 사용합니다.
// role.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 requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
이 예제는 라우트 핸들러 수준에서만 role이 있는지 확인하기 때문에 "가장 기본 RBAC"라고 명명했습니다. 실제 애플리케이션에서는 여러 작업을 포함하는 엔드포인트/핸들러가 있을 수 있으며, 각 작업에는 특정 권한 집합이 필요할 수 있습니다.
그러나 이 경우 비즈니스 로직 내 어딘가에 role을 확인하는 메커니즘을 제공해야 하며, 중앙화된 장소에서 권한을 특정 작업과 연결하는 것이 아니기 때문에 유지 보수가 다소 어렵다는 문제가 있습다.
이 예에서는 request.user에 사용자 인스턴스와 허용된 역할(roles 속성 아래)이 포함되어 있다고 가정했습니다. 앱에서는 2장에서 살펴보았던 custom authentication guard를 통해 이러한 연결을 만들 수 있습니다.
이 예제가 제대로 작동하려면 User 클래스가 다음과 같이 되어야 합니다.
class User {
// ...other properties
roles: Role[];
}
마지막으로, 컨트롤러 수준에서 혹은 전역적으로 RolesGuard를 등록하는 예시에 대해서 살펴보겠습니다:
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
권한이 부족한 사용자가 엔드포인트를 요청하면 Nest는 자동으로 다음과 같은 응답을 반환합니다:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
다른 오류 응답을 반환하려면 boolean 값을 반환하는 대신 고유한 특정 예외 인스턴스를 생성하여 던져야 합니다.
3-2. Claims-based authorization (클레임 기반 권한 부여)
ID가 생성되면 신뢰할 수 있는 당사자가 발급한 하나 이상의 claim이 할당될 것입니다. claim은 현재 요청을 한 주체가 무엇을 할 수 있는지를 나타내는 이름-값 쌍으로, 해당 주체가 어떤 존재인지가 아니라 무엇을 할 수 있는지를 나타내게 됩니다.
nest에서 클레임 기반 인가를 구현하려면 앞선 RBAC 섹션에서 설명한 것과 동일한 단계를 따르면 되지만 한 가지 중요한 차이점이 있습니다.
특정 역할을 확인하는 대신 권한(permission)을 비교해야 한다는 점입니다.
모든 사용자에게는 일련의 permission이 할당됩니다. 물론 마찬가지로 각 리소스/엔드포인트에도 액세스하기 위해 필요한 permission을 정의합니다.(ex. @RequirePermission() 데코레이터를 사용)
@Post()
@RequirePermissions(Permission.CREATE_CAT)
create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
위의 예에서 Permission(RBAC 섹션에서 설명한 역할과 유사)은 시스템에서 사용 가능한 모든 권한이 포함된 TypeScript enum 타입입니다.
3-3. Integrating CASL (CASL과 같이 사용하기)
CASL은 특정 클라이언트가 액세스할 수 있는 리소스에 제한을 거는 동형 권한 부여(isomorphic authorization) 라이브러리입니다. 점층적으로 채택할 수 있도록 설계되었으며 단순한 클레임 기반과 완전한 기능을 갖춘 주제 및 속성 기반 권한 부여 사이에서 쉽게 확장할 수 있습니다.
시작하려면 먼저 @casl/ability 패키지를 설치하세요:
$ npm i @casl/ability
이 예제에서는 CASL을 선택했지만 기본 설정과 프로젝트 요구 사항에 따라 accesscontrol이나 acl과 같은 다른 라이브러리를 사용할 수 있습니다.
설치가 완료되면 CASL의 메커니즘을 설명하기 위해 두 개의 엔티티 클래스를 정의하겠습니다: User및 Article.
class User {
id: number;
isAdmin: boolean;
}
User 클래스는 고유한 사용자 식별자인 id와 사용자에게 admin 권한이 있는지의 여부를 나타내는 isAdmin의 두 가지 속성으로 구성됩니다.
class Article {
id: number;
isPublished: boolean;
authorId: number;
}
Article 클래스에는 각각 id, isPublished, authorId라는 세 가지 속성이 있습니다. id는 고유한 Article 개체의 식별자이고, isPublished는 아티클이 이미 게시되었는지 여부를 나타내며, authorId는 해당 문서를 작성한 사용자의 ID입니다.
이제 이 예제에 대한 요구 사항을 검토하고 구체화해 보겠습니다:
- admin는 모든 엔티티를 관리(생성/읽기/업데이트/삭제)할 수 있습니다.
- User는 모든 항목에 읽기 전용 액세스 권한이 있습니다.
- User는 문서를 업데이트할 수 있습니다(article.authorId === userId).
- 이미 게시된 문서는 삭제할 수 없습니다(article.isPublished === true).
이를 염두에 두고 사용자가 엔티티로 수행할 수 있는 모든 가능한 동작을 나타내는 Action enum을 만들어줍니다:
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
Manage는 CASL에서 모든 action에 대한 표현을 의미하는 특수 키워드입니다.
CASL 라이브러리를 캡슐화하기 위해 이제 CaslModule과 CaslAbilityFactory를 생성해 보겠습니다.
$ nest g module casl
$ nest g class casl/casl-ability.factory
이렇게 하면 CaslAbilityFactory에서 createForUser() 메서드를 정의할 수 있습니다. 이 메서드는 지정된 사용자에 대한Ability 객체를 생성하는 기능을 수행합니다:
type Subjects = InferSubjects<typeof Article | typeof User> | 'all';
export type AppAbility = Ability<[Action, Subjects]>;
@Injectable()
export class CaslAbilityFactory {
createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder<
Ability<[Action, Subjects]>
>(Ability as AbilityClass<AppAbility>);
if (user.isAdmin) {
can(Action.Manage, 'all'); // read-write access to everything
} else {
can(Action.Read, 'all'); // read-only access to everything
}
can(Action.Update, Article, { authorId: user.id });
cannot(Action.Delete, Article, { isPublished: true });
return build({
// Read https://casl.js.org/v6/en/guide/subject-type-detection#use-classes-as-subject-types for details
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
- 'all'은 CASL에서 '모든 주체(any subject)'를 나타내는 특수 키워드입니다.
- Ability, AbilityBuilder, AbilityClass, ExtracSubjectTyp 클래스 는 @casl/ability 패키지로부터 가져옵니다.
- detectedSubjetType 옵션을 사용하면 CASL이 객체에서 subject의 타입을 어떻게 가져오는지 이해할 수 있도록 합니다.
위의 예시에서는 AbilityBuilder 클래스를 사용하여 Ability 인스턴스를 만들었습니다. 짐작하셨겠지만, can과 cannot은 다른 의미를 갖는 동일한 인자이며, can은 특정 주체에 특정 행동을 '할 수 있게 '하는 것이고 cannot은 '하지 못하도록 하는' 것입니다.
마지막으로 CaslModule 모듈 정의에서 provider 및 export 배열에 CaslAbilityFactory를 추가해야 합니다:
import { Module } from '@nestjs/common';
import { CaslAbilityFactory } from './casl-ability.factory';
@Module({
providers: [CaslAbilityFactory],
exports: [CaslAbilityFactory],
})
export class CaslModule {}
이렇게 하면 호스트 컨텍스트에서 CaslModule을 import하기만 하면 표준 생성자 기반 주입을 사용하여 모든 클래스에 CaslAbilityFactory를 주입할 수 있습니다:
constructor(private caslAbilityFactory: CaslAbilityFactory) {}
그러고 나서 클래스 안에서 이를 다음과 같이 사용할 수 있습니다:
const ability = this.caslAbilityFactory.createForUser(user);
if (ability.can(Action.Read, 'all')) {
// "user" has read access to everything
}
예를 들어 admin이 아닌 사용자가 있다고 가정해 보면, 이 경우 해당 사용자는 게시글(article)을 읽을 수는 있지만 새로운 article을 만들거나 기존의 article을 삭제하는 것은 금지되어야 합니다.
const user = new User();
user.isAdmin = false;
const ability = this.caslAbilityFactory.createForUser(user);
ability.can(Action.Read, Article); // true
ability.can(Action.Delete, Article); // false
ability.can(Action.Create, Article); // false
위와 같이 boolean으로 반환되는 값에 대해 if문으로 먼저 권한이 있는지 검증을 한 후 이후에 프로세스를 진행하도록 하는 방식인 것입니다.
또한 앞선 요구사항에 명시된 대로 사용자가 자신의 article을 업데이트할 수 있는 권한은 가져야 합니다:
const user = new User();
user.id = 1;
const article = new Article();
article.authorId = user.id;
const ability = this.caslAbilityFactory.createForUser(user);
ability.can(Action.Update, article); // true
article.authorId = 2;
ability.can(Action.Update, article); // false
보시다시피 Ability 인스턴스를 사용하면 매우 읽기 쉬운 방식으로 권한을 확인할 수 있습니다. 마찬가지로 AbilityBuilder를 사용하면 비슷한 방식으로 권한을 정의하고 다양한 조건들을 지정할 수 있습니다.
3-4. Advanced: Implementing a PoliciesGuard (심화: PoliciesGuard 구현)
이 섹션에서는 메서드 수준(method-level)에서 구성할 수 있는 특정 권한 부여 정책(authorization policies)을 사용자가 충족하는지 확인하는, 다소 정교한 guard를 구축하는 방법을 보여드리겠습니다(물론 클래스 수준에서도 구성된 정책을 따르도록 확장할 수 있습니다).
이 예제에서는 설명 목적으로 CASL 패키지를 사용하지만 이 라이브러리를 반드시 사용해야 하는 것은 아닙니다.
이전 섹션에서 생성한 CaslAbilityFactory 프로바이더를 계속 사용할 것입니다.
먼저 요구 사항을 구체화해 보겠습니다. 목표는 라우트 핸들러별로 정책 검사를 지정할 수 있는 메커니즘을 제공하는 것입니다.
먼저 정책 핸들러의 인터페이스를 정의하는 것부터 시작하겠습니다:
import { AppAbility } from '../casl/casl-ability.factory';
interface IPolicyHandler {
handle(ability: AppAbility): boolean;
}
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
위에서 언급했듯이 정책 핸들러(PolicyHandler)를 정의할 수 있는 두 가지 방법인 객체(IPolicyHandler 인터페이스를 구현하는 클래스의 인스턴스)와 함수(PolicyHandlerCallback 유형을 충족하는 함수)를 각각 제공하고 있습니다.
이와 더불어 @CheckPolicies() 데코레이터를 만들어 주어야 합니다. 이 데코레이터를 사용하면 특정 리소스에 액세스하기 위해 충족해야 하는 구체적인 정책을 지정할 수 있습니다.
export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
SetMetadata(CHECK_POLICIES_KEY, handlers);
자 그럼 이제 라우트 핸들러에 바인딩된 모든 policyHandler를 추출하고 실행하는 PoliciesGuard를 만들어 보겠습니다.
@Injectable()
export class PoliciesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private caslAbilityFactory: CaslAbilityFactory,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandlers =
this.reflector.get<PolicyHandler[]>(
CHECK_POLICIES_KEY,
context.getHandler(),
) || [];
const { user } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);
return policyHandlers.every((handler) =>
this.execPolicyHandler(handler, ability),
);
}
private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
if (typeof handler === 'function') {
return handler(ability);
}
return handler.handle(ability);
}
}
이 예제에서는 request.user에 사용자 인스턴스가 포함되어 있다고 가정했습니다. app에서는 custom athentication guard에서 해당 연결을 만들 수 있습니다.
위 예제를 자세히 살펴봅시다.
- policyHandlers는 @CheckPolicies() 데코레이터를 통해 메서드에 할당된 핸들러의 배열입니다.
- 다음으로, 사용자가 특정 작업을 수행하기 위해 충분한 권한을 가지고 있는지 확인할 수 있게 하는 Ability 객체를 구성하기 위해 CaslAbilityFactory#create 메서드를 사용합니다.
- 이 객체를 IPolicyHandler를 구현하는 함수 또는 클래스의 인스턴스인 PolicyHandler에 전달하여 boolean을 반환하는 handle() 메서드를 제공합니다.
- 마지막으로, 모든 핸들러가 true 값을 반환하는지 확인하기 위해 Array#every 메서드를 사용합니다.
마지막으로 이 guard를 테스트하려면 다음과 같이 라우트 핸들러에 바인딩하고 인라인 정책 핸들러(기능적 접근 방식)를 등록합니다:
@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
return this.articlesService.findAll();
}
혹은 다음처럼 IPolicyHandler 인터페이스를 구현하는 클래스를 따로 정의하여 사용할 수도 있습니다:
export class ReadArticlePolicyHandler implements IPolicyHandler {
handle(ability: AppAbility) {
return ability.can(Action.Read, Article);
}
}
그리고 이는 다음과 같이 사용합니다.
@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies(new ReadArticlePolicyHandler())
findAll() {
return this.articlesService.findAll();
}
'new' 키워드를 사용하여 Policy 핸들러를 in-place 방식으로 인스턴스화해야 하므로 ReadArticlePolicyHandler 클래스는 의존성 주입을 사용할 수 없습니다. 이 문제는 ModuleRef#get 메서드를 사용하여 해결할 수 있습니다. 기본적으로 @CheckPolicies() 데코레이터를 통해 함수와 인스턴스를 등록하는 대신 Type<IPolicyHandler> 전달을 허용해야 합니다.
그런 다음 guard 내부에서 type reference를 사용하여 인스턴스를 가져올 수 있습니다:
즉, moduleRef.get(YOUR_HANDLER_TYPE) 또는 ModuleRef#create 메서드를 사용하여 동적으로 인스턴스를 인스턴스화할 수도 있습니다.
이렇게 이번 포스팅에서는 Guard의 사용법과 더불어 athenticaiton과 athorization을 Nest에서 어떻게 manage하는지 알아보았습니다. 이에 대한 기능을 다루는 라이브러리인 passport는 굉장히 강력하여 많이 사용되는데요. 중요한 만큼 다른 포스팅에서 해당 라이브러리를 다뤄보도록 하겠습니다.
감사합니다.
'Back-end > NestJS' 카테고리의 다른 글
[NestJS] NestJS 트랜잭션(Transaction) 관리하기(With TypeORM) (0) | 2024.02.17 |
---|---|
[NestJS] Logging 알아보기 (feat. winston) (0) | 2024.02.16 |
[NestJS] TypeORM으로 MySQL 데이터베이스 연결하기(TypeORM 기능 다 알아보기) (0) | 2024.02.13 |
[NestJS] Validation과 Transformation (검증과 변환) (feat. Serialization-직렬화) (0) | 2024.02.12 |
[NestJS | Docs] Pipes 알아보기 (feat. 변환과 검증) (0) | 2024.02.09 |
소중한 공감 감사합니다