[NestJS] 인증(Auth) - JWT & Passport

soyeon·2023년 4월 13일

Nest

목록 보기
7/10
post-thumbnail

JWT

JWT
JWT(JSON Web Token)는 당사자간의 정보를 JSON 개체로 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준(RFC 7519). 이 정보는 디지털 서명이되어 있으므로 확인하고 신뢰할 수 있다.
정보를 안전하게 전할 때 혹은 유저의 권한 등을 체크하기 위해 사용하는데 유용한 모듈.

JWT 구조


. 을 기준으로 앞에부터 순서대로 HEADER, PAYLOAD, VERIFY SIGNATURE 로 구성

HEADER

  • 토큰에 대한 메타 데이터를 포함(타입, 해싱 알고리즘 SHA256, RSA...)

PAYLOAD

  • 유저 정보(issuer), 만료기간(expiration time), 주제(subject) 등등...

VERIFY SIGNATURE

  • 토큰이 보낸 사람에 의해 서명되었으며 어떤 식으로든 변경되지 않았는지 확인하는 데 사용되는 서명.
  • 서명은 헤더 및 페이로그 세그먼트, 서명 알고리즘, 비밀 또는 공개 키를 사용하여 생성된다.

JWT 사용 흐름

유저 로그인 -> 토큰 생성 -> 토큰 보관
위와 같이 사용 흐름이 되며
토큰 생성 과정에서는 유저 이름, 롤, 생성일, 만료일 등의 정보와 시크릿 텍스트를 합친 값을 해싱 알고리즘을 사용하여 토큰을 생성한다.

보관한 토큰을 요청을 보낼때 해더에 담아서 같이 보내준다.
그러면 클라이언트에서 온 Headers와 Payload 를 서버에서 가지고 있는 Secret Text를 사용하여 Signature 부분을 다시 생성하여 비교하여 일치하면 올바른 토큰인지 판별한다.

JWT 사용하기

필요 모듈 설치

npm i @nestjs/jwt @nestjs/passport passport passport-jwt

@nestjs/jwt:

  • nestjs에서 jwt를 사용하시 위해 필요한 모듈

@nestjs/passport:

  • nestjs에서 passport를 사용하기 위해 필요한 모듈

passport:

  • 인증 로직을 passport 모듈을 통해 더 쉽게 처리할 수 있기에 필요한 모듈

passport-jwt:

  • passport를 사용할때 jwt를 사용할 건데 기본 전략을 가져올 수 있음

애플리케이션에 JWT 모듈 등록하기

auth 모듈 imports에 넣어주기

// AuthModule
imports: [
    JwtModule.register({
      secret: 'Secret1234',
      signOptions: {
        expiresIn: 3600,
      },
    }),

secret: 토큰을 만들 때 이용하는 secret text
expiresIn: 정해진 시간 이후에는 토큰이 유효하지 않게 됨. 여기서 3600ms은 한시간 이후에 토큰이 유효하지 않도록 설정.

애플리케이션에 Passport 모듈 등록하기

import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }), // passport 모듈 추가
    JwtModule.register({
      secret: 'Secret1234',
      signOptions: {
        expiresIn: 3600,
      },
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AuthController],
  providers: [AuthService, UserRepository]
})
export class AuthModule {}

로그인 시 토큰 생성하는 로직 추가

auth service에서 signIn 함수에 토큰을 생성하는 로직을 추가하고 accessToken이라는 이름의 객체로 내려주자.

// auth service

...
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(UserRepository) private userRepository: UserRepository,
    private jwtService: JwtService, // jwt service를 사용하기 위해 추가
  ) {}
  
...

async signIn(
    authCredentialDto: AuthCredentialDto,
  ): Promise<{ accessToken: string }> {
    const { username, password } = authCredentialDto;
    const user = await this.userRepository.findOneBy({ username });

    if (user && (await bcrypt.compare(password, user.password))) {
      // 유저 토큰 생성 (Secret + Payload)
      const payload = { username };
      const accessToken = await this.jwtService.sign(payload);

      return { accessToken };
    } else {
      throw new UnauthorizedException('login failed');
    }
  }

서버 실행 후 올바른 정보로 로그인하면 { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InF3ZTEiLCJpYXQiOjE2ODE3NDAyMjMsImV4cCI6MTY4MTc0MzgyM30.vpSvLmb-hxs4s0hh1KQlXqwxMCBBz0F6iA2SrMp-Bc0" } 다음과 같은 값을 받을 수 있다.

이 값을 https://jwt.io/ 로 가서 살펴볼 수 있었다.

JWT & passport 이용하여 토큰 인증 후 유저정보 가져오기

  1. npm i @types/passport-jwt
  • @types/passport-jwt: passport-jwt 모듈을 위한 타입 정의 모듈
  1. jwt-strategy.ts 파일 생성
// auth/jwt-strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserRepository } from './user.repository';
import { User } from './user.entity';

@Injectable() // 해당 내용을 다른 서비스에서 사용가능하도록 데코레이터 추가
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(UserRepository)
    private userRepository: UserRepository,
  ) {
    super({
      secretOrKey: 'Secret1234',
      // 토큰이 유효한지 체크할 것임
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // 토큰을 어떤 타입의 옵션으로 가져올 것인지 정의
    });
  }

  async validate(payload) { // 유효한 유저정보인지 검증
    const { username } = payload;
    const user: User = await this.userRepository.findOneBy({ username }); // payload의 username을 통해 유저정보를 가져온다.

    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}
  1. JwtStrategy와 Passport Module을 다른 모듈에서 사용할 수 있도록 AuthModule 수정
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: 'Secret1234',
      signOptions: {
        expiresIn: 3600,
      },
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AuthController],
  providers: [AuthService, UserRepository, JwtStrategy], // JwtStrategy를 이 Auth 모듈에서 사용할 수 있도록 등록
  exports: [JwtStrategy, PassportModule], // 다른 모듈에서 사용할 수 있도록 등록
})
export class AuthModule {}

요청안에 유저 정보(유저 객체)가 들어가게 하기

NestJS의 미들웨어 사용하기

NestJS 미들웨어

  • Pipes:
    요청 유효성 검사 및 페이로드 변환을 위해 만들어지며 데이터를 예상한 대로 직렬화한다.
  • Filters:
    오류 처리 미들웨어로 특정 오류 처리기를 사용할 경로와 각 경로 주변의 복잡성을 관리하는 방법을 알 수 있다.
  • Guards:
    인증 미들웨어로 지정된 경로로 통과할 수 있는 사람과 허용되지 않는 사람을 서버에 알려준다.
  • Interceptors:
    응답 매핑 및 캐시 관리와 함께 요청 로깅과 같은 전후 미들웨어로 각 요청 전후에 이를 생성하는 기능은 매우 강력하고 유용하다.

UseGuards를 통해 구현해보자.
UseGuards안에 @nestjs/passport에서 가져온 AuthGuard()를 이용하면 요청안에 유저 정보를 넣어줄 수 있다.

요청을 직접 확인해보자.

// auth Controller
@Post('/test')
  @UseGuards(AuthGuard())
  test(@Req() req) {
    console.log('req: ', req);
  }

authController에 test 메소드를 만들고 아래와 같이 요청을 보내면

콘솔에서 엄청 많은 req 객체안의 내용을 확인할 수 있다.
그중 살펴보면 중간에 다음과 같이 유저정보가 담겨있는 것을 확인할 수 있다.

이를 활용하여 req.user가 아니라 user라는 객체로 보내주도록 하기 위해 커스텀 데코레이터를 활용하자.

커스텀 데코레이터 사용하기

get-user.decorator.ts 작성하기

// auth/get-user.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { User } from './user.entity';

export const GetUser = createParamDecorator(
  (data, ctx: ExecutionContext): User => {
    const req = ctx.switchToHttp().getRequest(); // 요청 정보를 가져옴
    return req.user; // req.user 반환
  },
);
// auth controller
@Post('/test')
  @UseGuards(AuthGuard())
  test(@GetUser() user: User) { // createParamDecorator 를 사용했기 때문에 parameter 위치에서 사용
    console.log('user: ', user);
  }

다시 테스트하면 다음과 같이 user 라는 객체에 user 정보만 담아서 가져올 수 있는 것을 확인할 수 있다.

이렇게 커스텀 데코레이터도 이용해보고 nestjs의 미들웨어도 활용하여 요청안에 유저정보를 담아서 보낼 수 있게 되었다.

이제 다음에는 이를 활용하여 인증을 통해서 권한을 주는 부분을 작성해보자.

profile
사부작 사부작

0개의 댓글