본문 바로가기
개발자 도전기/[STUDY] JS || TS

NestJS | JWT 인증, 로그인 기능 구현

by 답수 2023. 3. 6.
728x90
반응형

 

 

 

 

로그인 인증 방식으로 요즘 가장 많이 사용되는 기술은 JWT이다. 우리 회사 서비스 역시 JWT로 인증/인가를 구현한다. 이 프로젝트도 jwt로 인증 기능을 구현할 것이다.

 

우선 공식 문서의 SECURITY/Authentication에 들어가보자.

https://docs.nestjs.com/security/authentication#authentication

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

모듈 설치

Nest에서 인증을 위해 Possport 모듈과 JWT 모듈이 필요하다고 한다.

$ npm install --save @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-local @types/passport-jwt

 

Passport strategy 구현

Passport 모듈은 jwt뿐만 아니라 session 구현에도 사용되며 인증 프로세스를 구현하는 전략에 따라 커스터마이징할 수 있는 기능을 가지고 있다(예전에 MySQL과 Sequelize ORM을 사용할 때 세션 구현 방식으로 passport 모듈을 사용했던 적이 있음).

 

nest에서 jwt, passport를 통해 인증을 어떻게 구현해야 하는지 차근차근 정리해보자.

 

먼저 auth 모듈을 설치하자.

$ nest g module auth
$ nest g service auth

 

src/auth 디렉토리 레벨에서 jwt 폴더를 만든 후, 아래의 파일들 추가

// src/auth/jwt/jwt.guard.ts

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

 

// src/auth/jwt/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),  // 헤더로부터 토큰 추출하는 함수
      secretOrKey: process.env.JWT_SECRET_KEY,
      ignoreExpiration: false,
    });
  }
}

 

jwt.guard.ts 만들면 strategy가 자동으로 실행된다고 한다.

 

로그인 기능 구현

authService파일 내에서 로그인 기능을 구현할 것이다.

// src/auth/auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { CatsRepository } from 'src/cats/cats.repository';
import { LoginRequestDto } from './dto/login.request.dto';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly catsRepository: CatsRepository,
    private readonly jwtService: JwtService,  // auth.module의 JwtModule로부터 공급 받음
  ) {}

  async jwtLogin(data: LoginRequestDto) {
    const { email, password } = data;
    const cat = await this.catsRepository.findCatByEmail(email);
    if (!cat) {
      throw new UnauthorizedException('이메일과 비밀번호를 확인해주세요.');
    }
    const isPasswordValidated: boolean = await bcrypt.compare(
      password,
      cat.password,
    );
    if (!isPasswordValidated) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }
    const payload = { email, sub: cat.id };
    return {
      token: this.jwtService.sign(payload),
    };
  }
}

 

// src/auth/dto/login.request.ts

import { PickType } from '@nestjs/mapped-types';
import { Cat } from 'src/cats/cats.schema';

export class LoginRequestDto extends PickType(Cat, [
  'email',
  'password',
] as const) {}

 

Cat repository 내에 로그인할 때 필요한 메서드 추가

// src/cats/cats.repository.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Cat } from './cats.schema';
import { CatRequestDto } from './dto/cats.request.dto';

@Injectable()
export class CatsRepository {
  constructor(@InjectModel(Cat.name) private readonly catModel: Model<Cat>) {}

  async existsByEmail(email: string): Promise<boolean> {
    const result = await this.catModel.exists({ email });
    return !!result;
  }

  // 로그인 시 유효한 이메일인지 확인하는 메서드
  async findCatByEmail(email: string): Promise<Cat | null> {
    const cat = await this.catModel.findOne({ email });
    return cat;
  }

  async create(cat: CatRequestDto): Promise<Cat> {
    return await this.catModel.create(cat);
  }
}

 

cats.controller에서 로그인 라우트 담당하는 메서드 작성

import { Body, Controller, Get, Post, UseInterceptors } from '@nestjs/common';
import { AuthService } from 'src/auth/auth.service';
import { LoginRequestDto } from 'src/auth/dto/login.request.dto';
import { SuccessInterceptor } from 'src/common/interceptors/success.interceptor';
import { CatsService } from './cats.service';
import { CatRequestDto } from './dto/cats.request.dto';

@Controller('cats')
@UseInterceptors(SuccessInterceptor)
export class CatsController {
  constructor(
    private readonly catsService: CatsService,
    private readonly authService: AuthService,  // 의존성 주입
  ) {}
  ...
  // 로그인 로직 구현
  @Post('login')
  logIn(@Body() data: LoginRequestDto) {
    return this.authService.jwtLogin(data);
  }
  ...
}

 

모듈간 의존성 주입

// src/auth/auth.module.ts

import { forwardRef, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { CatsModule } from 'src/cats/cats.module';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt/jwt.strategy';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),
    JwtModule.register({
      secret: process.env.JWT_SECRET_KEY,
      signOptions: { expiresIn: '1y' },
    }),
    forwardRef(() => CatsModule),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}
// src/cats/cats.module.ts

import { forwardRef, Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AuthModule } from 'src/auth/auth.module';
import { CatsController } from './cats.controller';
import { CatsRepository } from './cats.repository';
import { Cat, CatSchema } from './cats.schema';
import { CatsService } from './cats.service';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]),
    forwardRef(() => AuthModule),
  ],
  controllers: [CatsController],
  providers: [CatsService, CatsRepository],
  exports: [CatsService, CatsRepository],
})
export class CatsModule {}
// src/app.module.ts

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import mongoose from 'mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { LoggerMiddleware } from './common/middlewares/logger/logger.middleware';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    ConfigModule.forRoot(),
    MongooseModule.forRoot(process.env.MONGODB_URI),
    CatsModule,
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  private readonly isDev: boolean = process.env.MODE === 'dev' ? true : false;
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
    mongoose.set('debug', true);
  }
}

 

auth 모듈과 cat 모듈을 보면 서로를 참조하고 있는, 순환 종속(circular dependency)이 발생한다. 인스턴스화의 순서는 불확정적이기 때문에 순환 종속이 일어난다면 정의되지 않는 의존성이 발생할 수 있다.

https://docs.nestjs.com/fundamentals/circular-dependency

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

가능하면 순환 종속이 되는 상황을 만들지 않아야 하지만, 쉽지만은 않다. 이를 해결하기 위해서 정방향 참조(forward reference) 방식인 forwardRef() 함수를 사용했다.

728x90
반응형
LIST

댓글