이번 포스팅에서는 진행중인 개인프로젝트에 nodemailer를 사용하여 이메일 인증기능과 비밀번호 초기화 기능을 구현했던 과정을 정리해보려 한다.
• 개요
사용자가 계정 비밀번호를 분실하였을 때 회원가입시 입력한 이메일주소를 통해 비밀번호를 변경할 수 있는 링크를 보내서 비밀번호를 변경할 수 있게 해주고 싶었다. 하지만 이 기능은 회원가입시에 사용자의 메일주소가 유효한지 인증이 우선적으로 필요하기 때문에 회원가입시 사용자가 입력한 이메일주소로 인증번호를 보내고 입력한 인증번호가 일치할 시에만 회원가입이 되도록 이메일 인증 기능도 구현이 필요한 상황이였다.
두 기능 모두 메일발송이 우선적으로 되어야 구현할 수 있기 때문에 혹시 npm 에 메일발송을 할 수 있는 모듈이 없을까 찾아보다가 nodemailer를 발견했고 사용법도 비교적 간편해서 gmail계정으로 nodemailer를 사용하여 메일발송기능을 구현해보기로 했다.
1. Gmail 앱 비밀번호 발급
nodemailer를 사용해서 메일을 발송하기 위해서는 사용하려는 메일 서비스명과 메일 계정 및 비밀번호가 필요하다.
Gmail로 메일 발송시 비밀번호는 Gmail 계정 비밀번호가 아니라 앱 비밀번호를 입력해야 하는데 앱 비밀번호를 따로 발급을 받야아한다.
1) 구글 계정 로그인 및 구글 계정 관리 이동
우선 구글 로그인 후에 우측 상단 프로필 이미지를 클릭하여 구글 계정 관리 화면으로 이동한다. 구글 계정 관리 화면에서 좌측 보안 탭을 클릭하여 보안화면에서 2단계 인증 항목을 클릭한다.
2) 2단계 인증 -> 앱 비밀번호 생성
2단계 인증 화면에서 아래로 쭉 내리다 보면 제일 하단에 앱 비밀번호 항목이 있는데 클릭해서 앱 비밀번호 생성화면으로 이동한다.
앱 비밀번호 생성전에 앱 비밀번호 이름을 입력해야 하는데 하고 싶은 이름을 입력하고 만들기를 클릭한다.
만들기 버튼을 클릭하면 화면에 16자리의 앱 비밀번호가 표출되는데 비밀번호는 한 번 밖에 표출되지 않으니 메모장에 잠시 적어두자.
2. nodemailer 패키지 설치
우선 프로젝트가 설치된 경로에서 아래의 명령어를 실행하여 nodemailer를 설치한다.
npm i nodemailer
.env 작성
#Gmail Account
MAIL_SERVICE = Gmail
MAIL_ADDRESS = abcdef@gmail.com # 구글 메일 주소
MAIL_PASSWORD = abcdefghijklmnop # 앱 비밀번호
앱 비밀번호는 노출되면 안되기 때문에 .env 파일에 위와 같이 환경변수로 만들었다.
차례대로 메일 서비스는 Gmail을 사용할 것이기에 Gmail, 메일 주소는 본인의 구글 계정 메일주소, 마지막으로 비밀번호에 위에서 만든 앱비밀번호를 띄어쓰기없이 16자리를 작성한다.
3. nodemailer 사용법
mport { NextApiRequest, NextApiResponse } from 'next';
import nodemailer from 'nodemailer';
export default async function SendMailHandler(request: NextApiRequest, response: NextApiResponse) {
const params = request.body.data;
let mailOptions;
const transporter = nodemailer.createTransport({
service: process.env.MAIL_SERVICE,
auth: {
user: process.env.MAIL_ADDRESS,
pass: process.env.MAIL_PASSWORD,
},
});
mailOptions = {
from: process.env.MAIL_ADDRESS,
to: 'abc123@mail.com',
subject: '메일 제목',
text: '메일내용',
// html: '<div><h1>메일내용</h1></div>'
};
await transporter.sendMail(mailOptions); // 메일 전송
};
기본적인 사용방법은 위와 같다. nodemailer.createTransport에 .env에서 작성한 메일 서비스, 메일 주소, 앱 비밀번호가 담긴 환경변수를 작성하고 mailOptions에 to 는 이메일을 받을 상대방의 이메일 주소, subject에는 메일 제목, text 에는 메일 본문 내용이 들어간다.
(참고로 text 대신 html 프로퍼티를 사용하여 html을 작성할 수도 있다.)
이 후 transporter.sendMail 함수로 메일이 전송되는데 인자로 mailOptions를 넣어줘야 한다.
4. 회원가입 이메일 인증 기능 구현
SendMailHandler.tsx
//SendMailHandler.tsx
import { NextApiRequest, NextApiResponse } from 'next';
import { handleMySql } from './HandleUser';
import { timeToString, timeFormat, generateRandomChar } from '@/utils/CommonUtils';
import nodemailer from 'nodemailer';
export default async function SendMailHandler(request: NextApiRequest, response: NextApiResponse) {
const params = request.body.data;
const toMailAddress = params.mailAddress; // 사용자가 입력한 메일주소
//숫자를 랜덤하게 섞은 인증번호 6자리 생성
const verifyCode = generateRandomChar(6, 'mailcode');
const expireTime = new Date(Date.now() + 1000 * 60 * 60 * 24); // 만료시간 24시간
const expireTimeToStr = timeToString(expireTime);
const mailOptions = {
from: 'verify@keylog.io',
to: toMailAddress,
subject: 'Keylog 회원가입 인증번호',
text: `
Keylog 회원가입을 위한 인증번호입니다.
아래의 인증번호를 입력하여 인증을 완료해주세요.
인증번호 : ${verifyCode}
인증번호는 ${timeFormat(expireTimeToStr)}까지 유효합니다.
`,
};
try {
await transporter.sendMail(mailOptions);
//DB에인증번호, 만료시간 저장
params.type = 'insertVerifyCode';
params.verifyCode = verifyCode;
params.expireTime = expireTimeToStr;
params.rgsnDttm = timeToString(new Date());
const veryfyCodeId = await handleMySql(params);
response.status(200).json(veryfyCodeId);
} catch (error) {
console.log(error);
}
}
개인프로젝트는 next.js로 구성되어 있고 회원가입 화면에서 이메일 인증 번호 받기 버튼을 클릭하면 서버에서 위 코드가 실행되어 6자리 난수로 구성된 인증번호를 생성하고 사용자가 입력한 메일주소로 인증번호와 만료시간을 보내준다.
메일 전송 후에는 인증번호, 만료시간 을 DB에 저장하고 인증번호 ID값을 return 받아 state로 관리한다.
CheckVerifyCode.tsx
import { handleMySql } from './HandleUser';
import { NextApiRequest, NextApiResponse } from 'next';
import { timeToString } from '@/utils/CommonUtils';
const CheckVerifyCode = async (request: NextApiRequest, response: NextApiResponse) => {
const params = request.body.data;
const verifyCodeId = params.verifyCodeId;
const userInputCode = params.userInputCode;
const isValid = await handleMySql({ type: 'getVerifyCode', verifyCodeId: verifyCodeId }).then((res) => {
const verifyCode = res.items[0].VERIFY_CODE;
const expireTime = res.items[0].EXPIRATION_TIME;
const currTime = timeToString(new Date());
return verifyCode === userInputCode && currTime <= expireTime;
});
return response.status(200).json({ isValid });
};
export default CheckVerifyCode;
이후 회원가입 완료 버튼을 클릭할 때 사용자가 입력한 인증번호, 클릭시간과 DB에 저장된 인증번호, 만료시간을 가져와서 비교하여 일치하는지 인증여부를 전달한다.
5. 비밀번호 초기화 기능 구현
SendMailHandler.tsx
import { NextApiRequest, NextApiResponse } from 'next';
import { handleMySql } from './HandleUser';
import { timeToString, timeFormat, generateRandomChar } from '@/utils/CommonUtils';
import nodemailer from 'nodemailer';
export default async function SendMailHandler(request: NextApiRequest, response: NextApiResponse) {
const params = request.body.data;
const transporter = nodemailer.createTransport({
service: process.env.MAIL_SERVICE,
auth: {
user: process.env.MAIL_ADDRESS,
pass: process.env.MAIL_PASSWORD,
},
});
params.type = 'getUser';
const user = await handleMySql(params).items[0]; // 사용자 정보 가져옴
const crypto = require('crypto');
if (user.totalItems > 0) {
const token = crypto.randomBytes(20).toString('hex'); //토큰 생성
const expireTime = timeToString(new Date(Date.now() + 1000 * 60 * 30)); // 만료시간 30분
const url = `http://localhost:3000/resetPassword/${token}`; // 링크 클릭시 비밀번호를 변경할 화면 RL
mailOptions = {
from: 'verify@keylog.io',
to: user.user_email,
subject: 'Keylog 비밀번호 변경 인증 메일',
html:`
안녕하세요 ${user.user_nickname}님<br><br>
계정 비밀번호를 재설정하기 위해 아래의 링크를 클릭하여 주세요.<br><br>
<a href='` +url +`'>비밀번호 재설정</a><br><br>
위 링크는 ${timeFormat(expireTime)}까지 유효합니다.
`,
};
try {
await transporter.sendMail(mailOptions); // 메일 전송
//DB에 토큰과 사용자ID, 만료시간을 저장
params.type = 'insertUserToken';
params.token = token;
params.id = user.user_id;
params.expireTime = expireTime;
params.rgsnDttm = timeToString(new Date());
await handleMySql(params);
} catch (error) {
console.log(error);
}
}
response.status(200).json(user);
회원가입 이메일 인증과 전체적으로 로직이 비슷하다. 한가지 다른 점이 있다면 crypto를 사용하여 토큰을 만들고 비밀번호를 변경할 화면url 에 토큰을 같이 넣어서 메일로 전송한다. 메일 전송 이후에는 사용자 ID와 토큰, 만료시간을 DB에 저장한다.
CheckVerifyToken.tsx
import { handleMySql } from './HandleUser';
import { NextApiRequest, NextApiResponse } from 'next';
import { timeToString } from '@/utils/CommonUtils';
const CheckVerifyToken = async (request: NextApiRequest, response: NextApiResponse) => {
const params = request.body.data;
const token = params.token; //url에 포함되어있던 토큰
const password = params.password;// 사용자가 입력한 새 비밀번호
const isValid = await handleMySql({ type: 'getUserToken', token: token, password: password }).then(async (res) => {
const tokenInfo = res.items[0]
const currTime = timeToString(new Date()); // 현재시간:YYYYMMDDHHMMSS
//DB에 해당 토큰정보가 있고 만료시간안이라면 비밀번호 업데이트
if (res.totalItems > 0 && currTime <= tokenInfo.EXPIRE_TIME) {
const userId = tokenInfo.USER_ID;
//사용한 토큰은 삭제
handleMySql({ type: 'deleteUserToken', token: token, id: userId });
return await handleMySql({ type: 'updatePassword', id: userId, amntDttm: currTime, password: password }).then((res) => {
return true;
});
} else {
return false;
}
});
return response.status(200).json({ isValid });
};
export default CheckVerifyToken;
사용자가 메일에서 비밀번호를 변경하기 위해 링크를 클릭하면 URL에 포함된 토큰을 DB에서 조회한다.
토큰정보가 있고 만료시간안이라면 사용자가 입력한 비밀번호를 업데이트한다.