이전글에서 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 + F
로 interface SessionData
를 검색해 줍니다.
interface SessionData {
authCode: string;
salt: string;
}
SessionData 인터페이스
를 위와 같이 수정해 줍니다. 세션으로 authCode
와 salt
를 사용하겠다는 의미입니다.
이어서 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
함수로 두 인증코드를 비교한 후 true
와 false
를 결과 값으로 받으면 끝입니다.(간단한 작업일 거 같아서 여기서 포스트 끝)