Guards는 CanActivate 인터페이스를 구현하는 클래스로, @injectable()
데코레이터를 포함하고 있다.
Guards는 단 하나의 책임만을 갖고 있다.
런타임에 존재하는 특정 조건(권한, 역할, ACL 등)에 따라 주어진 요청이 경로 핸들러에 의해 처리될 지 여부를 결정하며, 이것을 우리는 인증(인가)라고 한다.
인증(인가)는 일반적으로 전통적인 Express 응용 프로그램에서 미들웨어에 의해 처리되었다.
그러나 미들웨어는 본질적으로 멍청하기 때문에, next()
함수를 호출한 후에 어떤 핸들러가 실행될지 알지 못한다.
next()
는 미들웨어 함수에 대한 콜백 매개변수로, 호출하게 되면 다음 미들웨어 함수가 호출된다.
반면 Guards는 ExecutionContext
인스턴스에 액세스 할 수 있으므로 다음에 실행될 내용을 정확히 알 수 있다.
즉, Exception Filter, Pipe 및 Interceptor와 같이, 요청/응답 사이클의 적절한 지점에 처리 로직을 삽입하고 선언적으로 수행할 수 있도록 설계 되었으며, 이를 통해 코드의 DRY(Don't Repeat Yourself) 원칙과 선언적 프로그래밍을 유지하는데 도움을 준다.
DRY 원칙이란 개발을 이해하고 소프트웨어를 신뢰성 높게 개발하여 유지보수하기 쉽도록 하기 위해 지켜야 하는 원칙을 말한다.
선언적 프로그래밍이란 "어떻게" 보다 "무엇"을 나타내야하는지를 프로그래밍적으로 표현한다고 한다.
즉, 필요한 것을 달성하는 과정을 하나하나 기술하는 것보다, 필요한 것이 어떤 것인지 기술하는 데 방점을 두고 애플리케이션의 구조를 세워 나가는 프로그래밍 구조이다.
NestJS에서는 @UseGuards()
데코레이터를 통해 API 접근에 대한 인증 과정을 제어할 수 있다.
Pipe 및 Exception Filter와 마찬가지로 Guards는 컨트롤러 범위, 메서드 범위 또는 전역 범위로 설정할 수 있다. 아래는 @UseGuards()
데코레이터를 사용하여 컨트롤러 범위의 가드를 설정한 예이다.
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
@UseGuards()
데코레이터는 단일 인수를 사용하거나 쉼표로 구분된 인수 목록을 사용할 수 있으며, 이를 통해 한 번의 선언으로 적절한 가드 집합을 쉽게 적용할 수 있다.
ExecutionContext
는 ArgumentsHost
를 상속받는 인터페이스로, ArgumentsHost
를 확장하여 현재 실행 프로세스에 대한 추가적인 세부 정보를 제공한다.
ArgumentsHost
와 마찬가지로 Nest는 Guards의 canActivate()
메서드나 Interceptor의 intercept()
메서드와 같이, 필요한 곳에 ExecutionContext
의 인스턴스를 제공한다.
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
getHandler()
메서드는 호출하려는 핸들러에 대한 참조를 반환하며, getClass()
메서드는 이 특정 핸들러가 속한 컨트롤러 클래스의 유형을 반환한다.
예를 들어, HTTP 컨텍스트에서 현재 처리된 요청이 CatsController
의 create()
메서드에 바인딩된 POST 요청인 경우, getHandler()
는 create()
메서드에 대한 참조를 반환하고, getClass()
는 CatsController
유형(인스턴스 X)을 반환한다.
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
중요한 것은 Reflector#CreateDecorator
또는 내장된 @SetMetadata()
데코레이터를 통해 생성된 메타데이터 세트에 액세스할 수 있는 기회를 Guards 또는 Interceptor 내에서 제공한다는 것이다.
Passport는 가장 인기 있는 node.js 인증 라이브러리로, 사용자의 자격 증명을 확인하여 사용자를 인증하고, 인증된 상태를 관리하며 핸들러에서 추가로 사용할 수 있도록 인증된 사용자에 대한 정보를 Request 개체에 첨부하는 기능을 제공한다.
Passport는 다양한 인증 매커니즘을 구현하는 풍부한 Strategy(username/password, jwt 등)를 가지고 있으며, 이를 통해 Guards에 적용할 사용자 인증 전략을 설정할 수 있다.
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
위 코드는 로컬 인증 전략을 구현한 코드이며, username
과 password
의 유효성 검사 결과에 따라 인증이 처리된다.
정의한 인증 전략을 사용하고 싶다면 다음과 같이 module 내에 선언해주어야 한다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
해당 인증 전략을 특정 Guards에 적용하고 싶다면 다음과 같이 @UseGuards()
데코레이터 안에서 명시해주어야 한다.
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}