NestJs에서 토큰기반 인증 구현하기 (with JWT)

이우길·2022년 6월 28일
10

NestJs

목록 보기
14/20
post-thumbnail

NestJs에서 토큰기반 인증처리 하기

예제코드는 Github에서 볼 수 있습니다 :)

Goal

  • 토큰 기반의 인증처리 정리 (with JWT)

  • 기존 jsonwebtoken을 이용하던 것을 @nestjs/jwt를 이용하여 처리하기


Why Token 기반 인증?

토이 프로젝트를 하면서 인증에 관한 부분을 구현하는 도중 session기반의 인증과 token기반의 인증을 두고 고민을 하였다.

아무래도 session기반의 인증을 사용하게 되면 session을 저장해야 하기 때문에 session기반의 인증이 아닌 token기반의 인증을 선택하였다. (refreshToken는 DB에 저장하기는 한다)

메모리 상에 저장한다고 하더라도 로그인 한 유저가 증가하면 서버에 부하가 걸릴 수도 있기 때문에 선택하지 않았다.

추 후 토이 프로젝트에서 AWS의 RDS를 사용하게 된다고 하면 클라이언트가 API를 호출할 때 마다 DB에 있는 session을 조회하는 네트워크 비용을 줄이고 싶어서이기도 하다.


Token 기반 인증

토큰 기반의 인증 시스템은 인증받은 사용자들에게(로그인을 통과한) 토큰을 발급하고, 서버에 요청을 할 때 헤더에 토큰을 함께 보내도록 하여 유저의 인증을 처리한다.

이로 인해 사용자의 인증 정보를 서버나 세션에 유지하지 않고 클라이언트 측에서 들어오는 요청만으로 작업을 처리한다.

서버에는 따로 저장을 하는 것이 없기 때문에 별도의 저장소가 필요하지 않으며 상태를 가지지 않게 되기 때문에 확장에 용이한 구조를 지향할 수 있다.


내가 정의한 인증 과정

토이 프로젝트를 진행하면서 정의한 인증 과정의 흐름이다.

  1. 유저가 id와 password를 가지고 로그인을 요청한다.

  2. 유저가 인증을 완료하면 토큰을 생성한다. Refresh 정책을 가져가기로 하였기 때문에 AccessTokenRefreshToken을 생성한다.

    • AccessToken에는 유저에 정보를 claim으로 사용하고 RefreshToken에는 유저의 id를 claim으로 사용한다.
    • AccessTokenRefreshToken의 만료시간을 설정하며 RefreshToken의 유효시간을 더 길게 가져간다.
  3. AccessToken은 Client에게 넘겨주고 RefreshToken은 DB에 저장한다.

  4. Client는 요청을 보낼 때 마다 HeaderAuthorization: Bearer ${AccessToken}을 추가하여 요청을 보낸다.

  5. Server는 Heaedr에서 토큰을 파싱하여 유효한지 검사 한 후 요청에 알맞는 응답을 한다.

  6. 만약 Client가 가지고 있는 AccessToken이 만료가 되면 만료된 AccessToken을 가지고 Refresh를 요청하게 되며 DB에 RefreshToken이 유효하면 새로운 AccessToken을 만들어 응답을 하게 된다.

  7. 유저가 로그아웃을 하면 DB에 저장되어 있는 RefreshToken을 삭제한다.


JWT Token Service 구현

nestjs에서 JWT를 사용하기 위해서는 @nestjs/jwt를 추가하여 준다. @nestjs/jwt는 내부적으로 jsonwebtoken을 사용한다.

기존에는 jsonwebtoken를 사용하여 직접 구현하여 사용하였지만 nestjs에서 제공하는 것을 사용하여 처리하기로 하였다. (사실 jsonwebtoken를 사용하여 직접 구현하는 것과 크게 차이가 없음)

yarn add @nestjs/jwt

JwtModule 생성

사용법은 간단하다. JwtModuleimport받아서 jwt에 대한 options들을 설정해주면 된다. 추가적인 options들은 jwt module options을 참조하면 된다.

정의하는 법은 아래의 코드와 같으며 JwtServiceinject받기만 하면 쉽게 사용할 수 있으며 secret를 새로 부여하지 않는 이상 JwtModule를 만들 때 정의한 secret를 사용하게 된다.

@Module({
  imports: [
    JwtModule.register({
      secret: ${secretKey},
      signOptions: {
        ...
      }
    })
  ]
})
export class AppModule{}

jsonwebtoken api spec를 자세히 보면 jsonwebtoken와 달리 sign(), verfify()를 이용할 때 secret, publicKey 속성을 재정의 할 수 있다. 따라서 test를 작성할 때 secret과 같은 값을을 커스텀 할 수 있다.

Differing from jsonwebtoken it also allows an additional secret, privateKey, and publicKey properties on options to override options passed in from the module. It only overrides the secret, publicKey or privateKey though not a secretOrKeyProvider.


JwtService를 이용하여 token 생성하기

JwtService가 토큰을 생성할 때 제공해주는 메소드는 sign()signAsync()를 제공한다.

차이점은 이름에서부터 알 수 있듯 sign()이 동기적으로 동작하고 signAsync()는 비동기로 Promise를 return 한다. (현재 글은 sign()기준으로 작성)

//@nestjs/jwt
sign(payload: string | Buffer | object, options?: JwtSignOptions): string;

//tokenServie
export class TokenService implements ITokenService {
  constructor(
    private readonly jwtService: JwtService,
  ) {}

  createToken(claimPlain): { accessToken: string } {
    const accessToken = this.jwtService.sign(claimPlain, { ...options });
    ...
  }
}

token을 생성할 때 sign에 들어가는 첫번 째 파라미터는 string | Buffer | object를 받는다. 프로젝트를 class기반으로 코드를 작성하다 보니 첫번 째 인자로 class의 인스턴스를 넣었더니 아래와 같은 error를 만나게되었다.

Expected "payload" to be a plain object.

해결법

해결하는 방법은 첫번 째 인자로 plain을 넣어주는 것이다. class의 인스턴스를 plain으로 변경하는 방법은 2가지 정도가 있는다.(더 있을 수도?)

첫번째는 classtoPlain()이라는 메소드를 구현하는 것이다. return으로 plain 객체를 리턴해주면 된다.

toPlain(): { id: number; name: string; roleName: string } {
  return {
    id: this._id,
    name: this._name,
    roleName: this._roleName,
  };
}

두번째 방법으로는 class-transformer를 이용하는 것이다. class-transformer가 제공해주는 instanceToPlain()을 이용하여 넣어주면 된다.

instanceToPlain(${instance});

sign Option 부여하기

토큰을 생성할 때 부여할 수 있는 optionssign()의 두번 째 인자로 부여를 하며 jsonwebTokenSignOptions을 상속받은 JwtSignOptions 타입이며 아래와 같다.

secret을 재정의 할 수 있는 것 말고는 jsonwebtoken - sign과 동일하다

export interface JwtSignOptions extends jwt.SignOptions {
  secret?: string | Buffer;
  privateKey?: string | Buffer;
}

JwtService를 이용하여 토큰 검증하기

JwtService가 토큰을 검증할 때 제공해주는 메소드는 sign()과 동일하게 동기, 비동기 2가지를 제공한다. (현재 글은 verfify()기준으로 작성)

//@nestjs/jwt
verify<T extends object = any>(token: string, options?: JwtVerifyOptions): T;

//tokenService
export class TokenService implements ITokenService {
  constructor(private readonly jwtService: JwtService) {}

  validate<T extends Object>(token: string): T {
    try {
      return this.jwtService.verify<T>(token, { ...options });
    } catch (e) {
      throw new UnauthorizedException();
    }
  }
}

verfify()를 통해 token을 검증할 수 있으며 검증 시 토큰이 유효하지 않으면 error를 발생시킨다. 그렇기 때문에 try/catch문을 사용하여 error처리를 해줘야한다.


verfify Option 부여하기

sign()과 동일하게 검증을 할 때 사용하는 options를 두번째 인자로 정의하며 jsonwebtokenVerifyOptions를 상속받은 JwtVerifyOptions 타입이다.

secret을 재정의 할 수 있는 것 말고는 jsonwebtoken - verfify과 동일하다

export interface JwtVerifyOptions extends jwt.VerifyOptions {
  secret?: string | Buffer;
  publicKey?: string | Buffer;
}

다음으로

@nestjs/jwt를 이용하여 토큰 기반의 인증을 구현해보았다. 다음으로는 인증된 User의 정보를 @nestjs/passport를 이용하여 사용하는 법을 정리해보겠다.


REFERENCE

profile
leewoooo

0개의 댓글