[Typescript] Nest.js JWT 인증 모듈 만들기 (+ 커스텀 데코레이터)
서론
이번 포스팅에서는 Nest.js에 JWT 인증 모듈을 개발한 경험을 담았다. 실제 필자가 JWT 인증 모듈을 만들면서 개인적으로 고려했던 부분들이 포함되어있다.
1. 프로젝트 구조
JWT 인증/발급 기능에 대한 관심사를 "JWT 인증 모듈"만 가지도록 분리하기 위해 다음과 같은 프로젝트 구조를 가졌다.
- global.d.ts: 전역에서 JWT의 사용하기 위해 전역으로 인터페이스를 본 파일에 정의하게 된다.
- jwt-auth.module.ts: PassportModule이나 JwtModule을 import 및 initialize하기 위한 파일이다. 이 모듈은 @Global() 데코레이터가 붙였기 때문에, jwt 인증/발급 기능을 사용할 다른 모듈에선 jwt-auth.module을 import하지 않아도 이후 설명할 jwt-auth.service만 provider로 등록해 사용할 수 있다.
- jwt-auth.service.ts: JWT 인증과 발급을 처리하는 서비스(@Injectable() class)이다. jwt-auth.module을 통해 export되며, 다른 모듈에선 본 jwt-auth.service만 provider로 등록하여 JWT 인증/발급/파싱 기능을 사용하면 된다.
- jwt-auth.strategy.ts: 클라이언트에서 요청이 들어올때 Header나 Cookie에 포함된 JWT를 인증하는 정책(방법)을 정의하는 파일이다. 본 파일에서 DB에서 사용자를 조회해 유효한 사용자일 경우만 인증을 통과시키는 등의 작업을 수행할 수 있다.
- jwt-auth.guard.ts: @nestjs/passport의 AuthGuard를 상속한 인증 필터이며, 실제 컨트롤러 함수에 @UseGuards() 데코레이터를 통해 붙여 사용할 수 있다. @UseGuards() 데코레이터의 파라미터로 이 파일에서 정의한 Guard를 넣고 컨트롤러에 붙이면 jwt-auth.strategy에 정의된 정책에 의해 인증 작업을 수행한다. 그리고 본 파일에는 AuthGuard의 canActive()를 override하여 Header나 Cookie에 있는 JWT 문자열을 파싱하여 request 객체에 프로퍼티로 저장해두는 기능을 구현해놨다. 이후 이 프로퍼티는 아래에서 설명할 jwt.decorator에서 사용된다.
- jwt.decorator.ts: @nestjs/comon의 createParamDecorator()로 만들어진 JWT 획득 파라미터 데코레이터이다. 이 데코레이터는 request 객체 프로퍼티에 저장해둔 파싱된 JWT 객체를 파라미터로 주입해주는 기능을 구현되어있다.
위와 같은 구조로 JWT 관련 기능에 대한 관심사는 이 모듈이 전담하도록 했다. 그리고 @Global() 모듈과 Guard와 Decorator를 이용해, 이 모듈을 사용할 다른 모듈들에서 작성되는 보일러플레이트를 줄였다. 덕분에 테스트와 리팩토링도 간편한 구조를 가지게 되었다.
2. 모듈 구현
2-1. global.d.ts
전역에서 JWT 인터페이스를 사용하도록 정의하였다. JWT payload 안에 들어갈 인터페이스의 구조는 필요에 따라 조정하여 사용하면 된다.
2-2. jwt-auth.module.ts
JWT 모듈을 정의한 파일이다. 전역에서 다른 모듈들이 jwt-auth.module을 import하지 않아도 jwt-auth.module에서 export하는 클래스를 사용할 수 있도록 @Global() 데코레이터를 붙인다. 본 파일에선 @nestjs/passport의 PassportModule과 @nestjs/jwt의 JwtModule을 import하는데, 이때 환경변수를 이용하여 JwtModule의 secret을 initialize하기 위해 ConfigModule의 서비스를 주입하여 비동기로 처리한다. 그리고 jwt-auth.strategy.ts와 jwt-auth.service.ts를 provider로 등록하고 jwt-auth.service.ts는 다른 모듈에서도 사용할 수 있도록 export로 등록한다. 이때 주의할 점은, 꼭 @nestjs/jwt의 JwtService도 provider로 등록해주고 JwtModule을 export에 등록해주어야 정상적으로 JWT 인증/발급/파싱 기능을 사용할 수 있다.
2-3. jwt-auth.service.ts
JWT 인증/발급/파싱 기능을 커스텀하기 위해 서비스를 정의한 파일이다. @nestjs/jwt의 JwtService를 직접사용하게 되면 매번 전역으로 정의한 JwtPayload를 제네릭으로 넣어주어야 하기 때문에, JwtAuthService라는 별도의 서비스를 만들어서 정의해주었다. JwtService의 기능을 wrapping 하기 위함도 있지만, request 객체의 Header나 Cookie 프로퍼티에 있는 JWT 문자열을 추출하는 함수나, 추출된 JWT 문자열을 파싱까지 하여 객체로 반환하는 함수등을 정의하기 위한 목적도 있었다.
2-4. jwt-auth.strategy.ts
JWT 인증을 위해 JWT 추출 위치와 secret을 정의하고 인증을 위한 로직을 정의하는 파일이다. 필자는 Authorization Header와 cookie 'jwt'에서 모두 추출 할 수 있도록 정의해놓았으며, validate() 함수 정의를 통해 사용자 인증을 처리한다. validate() 함수 안에 DB select등의 과정을 넣어 유효한 사용자만 인증을 통과시킬 수 있다.
2-5. jwt-auth.guard.ts
실질적으로 다른 모듈의 컨트롤러 함수에 @UseGuards() 데코레이터를 이용하여 JWT 인증 필터를 붙이기 위한 파일이다. @nestjs/passport의 AuthGuard를 상속받아 구현되며, AuthGuard의 canActivate() 함수를 override해 request 객체에 파싱된 JWT 객체를 주입하는 기능을 구현했다. jwtAuthService.getJwtAndVerifyFromReq() 함수는 request 객체 안에서 JWT 문자열을 추출하고 파싱하여 객체로 반환하는 함수인데, 이를 실패할 경우 예외를 던지게 된다. 그럴 경우 인증에 실패한 것이니 catch해서 UnauthorizedException을 던지도록 해놨다.
2-6. jwt.decorator.ts
jwt-auth.guard를 통해 request 객체 프로퍼티로 주입해둔 JWT 객체를 파라미터에 주입해주는 데코레이터를 정의한 파일이다. 컨트롤러 내에 JWT를 request 객체에서 뽑아내는 보일러플레이트를 매번 작성하게 되면 유지보수가 힘들어지기 때문에 이 과정 데코레이터로 분리한 것이다.
3. 사용법
우선 이 JwtAuthModule을 사용하기 위해선 app.module.ts에 ConfigModule과 JwtAuthModule을 import해야한다.
그리고 JWT 기능을 사용할 모듈에서 JwtAuthModule의 서비스를 provider로 등록하고 서비스 constructor에서 가져오면 된다.
이때 주의할 점은, JwtAuthGuard에서 JwtAuthModule의 서비스를 사용하고 있기 때문에 컨트롤러에서 JwtAuthGuard를 사용할 모듈에 JwtAuthModule의 서비스를 provider로 등록하지 않으면 정상적으로 인증이 처리되지 않는다.
컨트롤러에서는 @UseGuards() 데코레이터를 사용해 JwtAuthGuard를 컨트롤러 함수에 붙여주면 되고, JWT 객체 획득이 필요한 경우 @Jwt() 데코레이터를 이용하면 된다. 이때 주의할점은, JwtAuthGuard가 컨트롤러 함수에 붙어있어야만 @Jwt() 데코레이터를 통해 JWT 객체 획득이 가능하다.