본문 바로가기
개발/Nest.js

01) Nest.js JWT인증 + RoleGuard 가이드

by jeounpar 2023. 6. 9.

전체 코드 : https://github.com/jeounpar/nestjs-jwt-roleguard-tutorial

 

JWT란 무엇인가요?

https://jwt.io/introduction

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

1. nest 프로젝트 생성 및 auth 모듈 생성

$ nest new jwt-tutorial
$ cd jwt-tutorial
$ nest generate module auth
$ nest generate service auth
$ nest generate controller auth

 

2. JWT 및 환경변수 라이브러리 설치, .env 파일 생성

$ npm install @nestjs/jwt @nestjs/config
// .env
JWT_SECRET="여기에 시크릿 키 입력"

 

3. auth.guard.ts 파일 생성

// auth/auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
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: process.env.JWT_SECRET,
      });
      console.log(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;
  }
}

CanActivate

NestJS 에서 사용되는 *가드(guard)의 일종이다.  CanActivate 인터페이스는 라우터 또는 컨트롤러 메서드에 적용되며, 요청이 실행되기 전에 조건을 확인하여 요청을 허용하거나 거부하는 역할을 한다.

CanActivate 인터페이스는 단일 메서드를 가지고 있으며, 해당 메서드의 반환 값은 boolean 또는 Promise<boolean>이어야 한다. 이 메서드는 라우터 또는 컨트롤러 메서드가 실행되기 전에 호출되며, true를 반환하면 요청이 계속 진행되고, false를 반환하면 요청이 차단된다.

일반적으로 CanActivate 인터페이스를 구현하기 위해 사용자 정의 가드 클래스를 작성하게 되는데, AuthGuard 와 같은 내장된 가드 클래스를 상속하여 재사용 가능한 로직을 구현할 수도 있다.

(*가드 : 라우터 핸들러 또는 컨트롤러 메서드를 실행하기 전에 요청을 검사하고 조작하는 데 사용됨)

 

ExecutionContext

NestJS 에서 사용되는 실행 컨텍스트를 나타내는 인터페이스이다. 이 인터페이스는 요청을 처리하는 동안 현재 실행 중인 컨트롤러, 핸들러, 파이프, 인터셉터 등의 정보에 접근할 수 있는 메서드와 프로퍼티를 제공한다.

ex) 인터셉터에서 ExecutionContext를 이용하여 현재 실행 중인 요청의 정보에 접근하거나 조작할 수 있다.

4. auth.controller.ts 및 auth.service.ts 파일 수정

// auth.controller.ts
import { Controller, Get, Post, Req } from '@nestjs/common';
import { AuthService } from './auth.service';

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

  @Post('login')
  async login(@Req() req) {
    const id: string = req.body.id;
    const password: string = req.body.password;
    return await this.authService.login(id, password);
  }

  @Get('verify')
  async verify(@Req() req) {
    const [type, token] = req.headers.authorization?.split(' ') ?? [];
    return await this.authService.verify(token);
  }
}
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}
  async login(id: string, password: string) {
  // 프로덕션 환경에서는 페이로드에 패스워드 같은 중요 정보는 탑재 금지!!
    const payload = {
      id: id,
      password: password,
    };
    const accessToken = await this.jwtService.signAsync(payload);
    return accessToken;
  }

  async verify(token: string) {
    const payload = await this.jwtService.verifyAsync(token, {
      secret: process.env.JWT_SECRET,
    });
    return payload;
  }
}

login

클라이언트로부터 id와 password를 전달받아 jwt token을 생성해준다.

 

verify

클라이언트로부터 jwt token을 전달받아 token에 해당하는 클라이언트의 id와 password를 알려준다.

 

5. app.module.ts 생성 및 auth.module.ts 파일 수정

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.registerAsync({
      useFactory: async () => ({
        secret: process.env.JWT_SECRET,
        signOptions: { expiresIn: '60s' },
      }),
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

.env파일 사용을 위해 ConfigModule을 글로벌로 설정하고, auth.module 에서 @Module 안에 process.env 사용을 위해 registerAsync와 useFactory를 사용했다.

 

6. JWT 테스트

auth/login 테스트

auth/verify 테스트

  • iat (Issued At): JWT가 발급된 시간. 이 필드는 UNIX 타임스탬프(UTC 기준)로 표현되고 보통 JWT가 발급된 시간을 추적하기 위해 사용됨.
  • exp (Expiration Time): JWT의 만료 시간. 이 필드는 UNIX 타임스탬프(UTC 기준)로 표현되고 만료 시간 이후에는 JWT가 더 이상 유효하지 않음. 보통 JWT의 수명을 관리하고 재인증을 유도하기 위해 사용됨.

7. app.controller.ts 에 UseGuards 추가

// app.module.ts
...
imports: [
    AuthModule,
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    JwtModule.registerAsync({
      useFactory: async () => ({
        secret: process.env.JWT_SECRET,
        signOptions: { expiresIn: '60s' },
      }),
    }),
  ],
...
// app.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { AuthGuard } from './auth/auth.guard';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @UseGuards(AuthGuard)
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

 

8. UseGuards 테스트

인증 토큰 없이 API 요청
인증 토큰 발급 후 API 요청