로그인 인증 방식으로 요즘 가장 많이 사용되는 기술은 JWT이다. 우리 회사 서비스 역시 JWT로 인증/인가를 구현한다. 이 프로젝트도 jwt로 인증 기능을 구현할 것이다.
우선 공식 문서의 SECURITY/Authentication에 들어가보자.
https://docs.nestjs.com/security/authentication#authentication
모듈 설치
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
가능하면 순환 종속이 되는 상황을 만들지 않아야 하지만, 쉽지만은 않다. 이를 해결하기 위해서 정방향 참조(forward reference) 방식인 forwardRef() 함수를 사용했다.
'개발자 도전기 > [STUDY] JS || TS' 카테고리의 다른 글
실무에서 인증 에러로 로그인 자주 풀리는 문제 트러블 슈팅... JWT는 절대 간단하지 않다 + 자바스크립트 비동기적 처리 (2) | 2024.04.03 |
---|---|
NestJS | Repository pattern (0) | 2023.03.05 |
NestJS | NestJS와 DB 연결, 환경 변수 설정, DTO, 회원가입 기능 구현 (1) | 2023.02.27 |
NestJS | docs | Interceptors & AOP Pattern (0) | 2023.02.24 |
NestJS | docs | Guards (0) | 2023.02.24 |
댓글