NestJS와 nodemailer로 이메일 인증을 구현하는 방법입니다.
시작
먼저 Nest CLI
로 프로젝트를 생성해줍니다.
npm i -g @nestjs/cli
nest new email-verification
프로젝트 폴더로 이동한 후 nodemailer 모듈을 설치해줍니다.
npm i @nestjs-modules/mailer nodemailer
npm i --save-dev @types/nodemailer
다음으로 hbs
파일을 사용하기 위해서 handlebars
와 hbs
를 설치해줍니다.
npm i hbs handlebars
MailerModule 추가
app.module.ts
파일에 가서 MailerModule
를 추가해줍니다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
@Module({
imports: [
MailerModule.forRoot({
transport: {
host: 'smtp.gmail.com',
port: 587,
auth: {
user: // 이메일 송신지 입력,
pass: // 앱 비밀번호 입력,
},
},
defaults: {
from: '"nest-modules" <modules@nestjs.com>',
},
template: {
dir: __dirname + '/templates/',
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
auth 부분의 user에 이메일을 보낼 송신지 이메일을 입력하시고 pass 부분에 앱 비밀번호를 입력합니다. 앱 비밀번호를 발급받는 방법은 이곳에서 확인할 수 있습니다. (저는 gmail을 사용했습니다.) user과 pass는 하드코딩할 경우 이메일과 비밀번호과 유출될 수 있으므로 @nest/cofing의 환경변수(env)를 이용해서 넣어줍니다.
메인페이지 생성 및 렌더링
'/' 요청
의 페이지를 만들기 위해 루트 폴더 밑에 views
폴더를 생성한 후 안에 index.hbs
파일을 생성해줍니다.
<form action='/' method='post'>
<p><input type='email' name='email' placeholder='이메일 입력' required /></p>
<button type='submit'>인증번호 보내기</button>
</form>
index.hbs
파일을 불러오기 위해 main.ts
에 다음 코드를 넣어줍니다.
//main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(3000);
}
bootstrap();
다음으로 controller
에 index.hbs
파일을 렌더링 하기 위한 코드를 만듭니다.
//app.controller.ts
import { Get, Controller, Render, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/')
@Render('index')
root() {
return {};
}
}
등록한 후 http://localhost:3000/
에 접속하시면 다음과 같이
index.hbs
파일이 렌더링됩니다.
인증번호 생성 및 발송
이제 이메일을 입력하고 인증번호 보내기 버튼을 눌렀을때 인증번호와 함께 이메일을 보내는 코드를 만듭니다.
controller
와 service
에 다음 코드를 입력합니다.
//app.controller.ts
import { Get, Controller, Render, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/')
@Render('index')
root() {
return {};
}
@Post('/')
async sendCode(@Body('email') userEmail: string): Promise<boolean> {
return this.appService.sendCode(userEmail);
}
}
//app.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer/dist';
import * as crypto from 'crypto';
@Injectable()
export class AppService {
constructor(private readonly mailerService: MailerService) {}
sendCode(userEmail: string): boolean {
//6자리 인증코드 생성
const authCode = crypto.randomBytes(3).toString('hex');
console.log(`발송대기`);
this.mailerService
.sendMail({
to: //이메일 수신지 주소 입력,
from: //이메일 송신지 주소 입력,
subject: 'Hello', //이메일 제목입력
template: 'email', //templates 폴더 안에 있는 hbs 파일명 입력
context: {
userEmail,
authCode,
},
})
.then((res) => {
console.log(res); //이메일 발송 결과
})
.catch((err) => {
new ConflictException(err);
});
return true;
}
}
인증번호는 보안을 위해서 랜덤한 문자를 생성하여 보냅니다. 자바스크립트에서는 난수를 만드는 법으로 Math.random()
함수를
지원하지만 완벽한 난수가 아니여서 보안상 문제가 있을 수 있습니다. 따라서 안전한 crypto
로 난수를 만드는 것을 추천합니다.
이메일 템플릿
이제 이메일 내용에 들어갈 템플릿을 만들겠습니다.
src
폴더 안에 templates
폴더를 만들고 안에 email.hbs
파일을 만듭니다.
<h1>{{userEmail}}님 반갑습니다.</h1>
<p>인증번호는 [{{authCode}}] 입니다.</p>
ejs에서는 변수를 넣을려면 <%= userEmail %>
같이 넣어야 돼서 귀찮은데 hbs는 간단해서 맘에듭니다.
여기서 이제 이메일을 보내면 다음과 같이 인증번호와 함께 이메일이 잘 발송됩니다.
오류
갑자기 dist폴더에서 hbs을 못찾는 오류가 발생하여 몇시간 삽질을 하다가 깃허브 이슈에서 해결법을 찾았습니다. nest-cli.json
파일의 compilerOptions
에 다음과
같은 코드를 추가하시고 서버를 껐다가 다시 시작하시면 해결이 됩니다.
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
{
"include": "**/*.hbs",
"watchAssets": true
}
],
"deleteOutDir": true
}
}
인증번호 암호화
인증번호를 암호화없이 어딘가 저장해둔다면 보안상의 문제가 발생합니다. 따라서 암호화를
시키고 저장해야되는데 NestJS 공식문서에서 양방향 암호화로 aes-256-ctr
를
단방향 암호화으로는 argon2
와 bcrypt
를 추천합니다. 서버가 생성한 인증 번호를 암호화한 후
유저가 입력한 인증 번호를 암호화해서 두 개를 비교하면 되므로 양방향 암호화 대신 단방향 암호화를 선택했고
그 방식으로는 bcrypt
를 사용하겠습니다.
npm i bcrypt
npm i -D @types/bcrypt
app.service.ts
파일에 인증번호가 성공적으로 발송이 되었다면 인증번호를 암호화하는 로직을 추가시켜줍니다.
//app.service.ts
import * as bcrypt from 'bcrypt'; //코드 상단에 추가
...
.then((res) => {
console.log(res); //이메일 발송 결과
// 인증번호 암호화
encrypt(authCode);
})
...
const encrypt = async (authCode: string) => {
// salt 생성
const salt = await bcrypt.genSalt();
// 인증코드와 salt로 hash 생성
const hash = await bcrypt.hash(authCode, salt);
return hash;
};
인증번호 저장
내용이 길어져서 다음글에서 계속.