[NestJS] 이메일 인증 구현하기(2)

이전글에서 NestJs와 nodemailer로 인증번호를 발송하는 것까지 정리했습니다. 이번에는 인증번호를 저장, 관리하고 사용자로부터 입력받은 인증번호에 따라 인증 성공/실패를 나눠보겠습니다.


인증코드를 어딘가에 저장해두고 사용자가 인증코드를 입력했을 때 저장되어 있는 인증코드를 불러와서 두 개를 비교하여 두 인증코드가 일치하면 인증 성공 로직을 이어가면 됩니다. 그러면 서버가 생성한 인증코드를 어디에 저장해야 되나.. DB에 저장하는 방법도 있겠으나 인증코드는 대부분 몇 분이 지나면 사라지는 휘발성 데이터이므로 DB에 저장하면 서버 자원의 낭비가 될 수 있다. 그래서 생각한 게 캐시를 사용하고 캐시 솔루션으로 Redis를 사용하려고 했지만 Redis가 윈도우OS를 공식적으로 지원하지 않고 아직 내 지식이 부족해서 어떻게 할까 고민하다가 그냥 세션을 이용하기로 했다.

여기서 express-session을 사용하면서 @Req와 @Res 데코를 이용해서 req와 res 객체를 사용했는데 사실 NestJS 내에서는 Express 객체를 직접적으로 사용하는 것은 좋은 방법이 아니라고 한다.


express-session 설치

NestJS에서 세션을 이용하려면 express-session을 설치하면 됩니다.

npm i express-session
npm i -D @types/express-session

설치가 완료되면 express-session 미들웨어를 main.ts에서 글로벌 미들웨어로 적용해줍니다.

//main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import * as session from 'express-session';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
 
  app.use(
    session({
      secret: 'my-secret',
      resave: false,
      saveUninitialized: false,
    }),
  );
 
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('hbs');
 
  await app.listen(4000);
}
bootstrap();

secret값은 보안을 위해서 환경변수등에 저장한 후 불러오는 방법이 좋습니다.

Controller

컨트롤러에서 Post 요청이 오면 sendCode함수를 실행한 후 사용자가 인증 코드를 입력할 수 있도록 auth.hbs을 렌더링 시켜줍니다.

//app.controller.ts
  @Post('/')
  async sendCode(
    @Body('email') userEmail: string,
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    this.appService.sendCode(userEmail, req);
    res.render('auth');
  }

sendCode함수의 매개변수로 req도 넘겨주는 이유는 service에서 req.session을 사용하기 위해서입니다. 아래는 auth.hbs 파일입니다.

{{!-- views/auth.hbs --}}
<form action='/auth' method='post'>
  <p><input type='text' name='authCode' placeholder='인증번호 입력' required /></p>
  <button type='submit'>제출</button>
</form>

interface SessionData

이제 제가 사용할 세션 데이터를 등록해 보겠습니다. app.service.ts파일의 상단에 다음 코드를 넣어줍니다.

import session from 'express-session';

vscode기준으로 'express-session' 부분에 마우스를 올리고 Ctrl + Click을 하시면 index.d.ts파일을 열 수 있습니다. index.d.ts파일에서 Ctrl + Finterface SessionData를 검색해 줍니다.

interface SessionData {
    authCode: string;
    salt: string;
}

SessionData 인터페이스를 위와 같이 수정해 줍니다. 세션으로 authCodesalt를 사용하겠다는 의미입니다. 이어서 app.service.ts파일을 아래와 같이 수정합니다.

Service

//app.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer/dist';
import { Request } from 'express';
import * as crypto from 'crypto';
import * as bcrypt from 'bcrypt';
// import session from 'express-session';
 
@Injectable()
export class AppService {
  constructor(private readonly mailerService: MailerService) {}
 
  sendCode(userEmail: string, req: Request): boolean {
    //인증코드 생성
    const authCode = crypto.randomBytes(3).toString('hex');
    console.log(`발송대기`);
    this.mailerService
      .sendMail({
        to: //이메일 수신지 주소 입력,
        from: //이메일 송신지 주소 입력,
        subject: 'Hello', //이메일 제목입력
        template: 'email', //templates폴더안에 있는 hbs파일명 입력
        context: {
          userEmail,
          authCode,
        },
      })
      .then(async (res) => {
        console.log(res); //이메일 발송 결과
        // 인증번호 암호화
        encrypt(authCode, req);
      })
      .catch((err) => {
        new ConflictException(err);
      });
    return true;
  }
}
 
const encrypt = async (authCode: string, req: Request): Promise<void> => {
  // salt 생성
  const salt = await bcrypt.genSalt();
  // 인증코드와 salt로 hash 생성
  const hash = await bcrypt.hash(authCode, salt);
  // 암호화된 인증 번호와 salt를 세션에 저장
  req.session.authCode = hash;
  req.session.salt = salt;
  // 인증번호와 salt가 세션에 잘 들어갔는지 확인차 출력
  console.log(req.session.authCode);
  console.log(req.session.salt);
};

이제 서버를 시작하고 index.hbs에서 이메일을 입력하고 버튼을 누르면 auth.hbs가 렌더링되고 세션에 등록된 암호화된 인증코드와 salt가 다음과 같이 콘솔에 잘 출력되는 것을 확인할 수 있습니다.

$2b$10$uBYouTnNtlwhEm7qVDH94uZLFSaZYZRxAublhDxLvOPyJ9uk5JviO
$2b$10$uBYouTnNtlwhEm7qVDH94u

이제 사용자로부터 받은 인증코드를 req.session.salt와 함께 같은 방식으로 암호화한 후 bcrypt.compare함수로 두 인증코드를 비교한 후 truefalse를 결과 값으로 받으면 끝입니다.(간단한 작업일 거 같아서 여기서 포스트 끝)