NestJS를 기반으로 한 인증 시스템을 구현하며, 회원가입과 로그인 기능을 개발한 과정을 기술 블로그 형식으로 자세히 정리합니다. 이 글에서는 강의에서 제공한 AppModule, AuthModule, AuthService, AuthController 등의 소스코드를 기반으로 전체 흐름과 각 구성 요소의 역할을 설명합니다.
1. 프로젝트 구조 및 환경 설정
1-1. 환경 설정
AppModule에서는 프로젝트 전역 설정과 모듈 구성을 담당합니다.
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validationSchema: Joi.object({ REFRESH_TOKEN_SECRET: Joi.string().required(), ACCESS_TOKEN_SECRET: Joi.string().required(), HTTP_PORT: Joi.number().required(), DB_URL: Joi.string().required(), }), }), TypeOrmModule.forRootAsync({ useFactory: (configService: ConfigService) => ({ type: 'postgres', url: configService.getOrThrow('DB_URL'), autoLoadEntities: true, synchronize: true, }), inject: [ConfigService], }), UserModule, AuthModule, ], }) export class AppModule {}
ConfigModule로 환경 변수를 정의하고, Joi를 통해 스키마 유효성 검증을 수행합니다.
TypeOrmModule로 PostgreSQL 연결 및 엔티티 자동 로딩을 설정합니다.
2. Auth 모듈 구성
2-1. AuthModule
@Module({ imports: [ JwtModule.register({}), TypeOrmModule.forFeature([User]), UserModule, ], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {}
JwtModule을 임포트하여 JWT 발급 기능을 사용할 수 있도록 준비합니다.
UserModule, User 엔티티를 함께 불러와 의존성을 주입받습니다.
3. AuthController: API 엔드포인트 구현
@Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') @UsePipes(ValidationPipe) registerUser(@Authorization() token: string, @Body() registerDto: RegisterDto) { if (token === null) throw new UnauthorizedException('토큰을 입력해주세요!'); return this.authService.register(token, registerDto); } @Post('login') @UsePipes(ValidationPipe) loginUser(@Authorization() token: string) { if (token === null) throw new UnauthorizedException('토큰을 입력해주세요!'); return this.authService.login(token); } }
register와 login 엔드포인트는 Authorization 헤더를 통해 전달받은 token을 사용합니다.
@Authorization() 데코레이터를 통해 토큰을 추출합니다. (별도 구현 필요)
등록과 로그인은 AuthService에 위임되어 처리됩니다.
4. AuthService: 핵심 비즈니스 로직
4-1. 회원가입
async register(rawToken: string, registerDto: any) { const { email, password } = this.parseBasicToken(rawToken); return this.userService.create({ ...registerDto, email, password }); }
Basic 방식으로 인코딩된 토큰을 디코딩하여 이메일과 비밀번호를 파싱합니다.
registerDto의 나머지 정보와 함께 유저 생성 요청을 보냅니다.
4-2. 로그인
async login(rawToken: string) { const { email, password } = this.parseBasicToken(rawToken); const user = await this.authenticate(email, password); return { refreshToken: await this.issueToken(user, true), accessToken: await this.issueToken(user, false), }; }
이메일/비밀번호 인증 후, access token과 refresh token을 각각 발급하여 반환합니다.
4-3. 토큰 파싱
parseBasicToken(rawToken: string) { const basicSplit = rawToken.split(' '); if (basicSplit.length !== 2 || basicSplit[0].toLowerCase() !== 'basic') { throw new BadRequestException('토큰 포맷이 잘못됐습니다!'); } const decoded = Buffer.from(basicSplit[1], 'base64').toString('utf-8'); const [email, password] = decoded.split(':'); if (!email || !password) { throw new BadRequestException('토큰 포맷이 잘못됐습니다!'); } return { email, password }; }
클라이언트에서 base64로 인코딩한 email:password 형태의 토큰을 디코딩합니다.
4-4. 사용자 인증
async authenticate(email: string, password: string) { const user = await this.userRepository.findOne({ where: { email }, select: { id: true, email: true, password: true }, }); if (!user || !(await bcrypt.compare(password, user.password))) { throw new BadRequestException('잘못된 로그인 정보입니다!'); } return user; }
4-5. JWT 발급
async issueToken(user: any, isRefreshToken: boolean) { const secret = isRefreshToken ? this.configService.getOrThrow<string>('REFRESH_TOKEN_SECRET') : this.configService.getOrThrow<string>('ACCESS_TOKEN_SECRET'); return this.jwtService.signAsync( { sub: user.id, role: user.role, type: isRefreshToken ? 'refresh' : 'access', }, { secret, expiresIn: '3600h', }, ); }
JWT 페이로드에는 사용자 ID, 권한(role), 토큰 타입 정보를 담아 생성합니다.
5. 마무리
인증 방식은 Bearer가 아닌 Basic 기반 토큰을 사용하여 이메일/비밀번호를 전달받는 독특한 구조입니다.
NestJS의 ConfigModule, JwtModule, TypeOrmModule, ValidationPipe, Exception 처리 방식 등 실무에서 자주 쓰이는 기능들을 실습할 수 있었습니다.
단순하지만 강력한 인증 로직을 통해 NestJS 구조의 장점을 잘 활용한 예제였습니다.
댓글 ( 0)
댓글 남기기