Node js 아이디 찾기 - Node js aidi chajgi

오늘은 비밀번호를 분실할 경우, 임시 비밀번호를 메일 발송하는 기능을 구현해보았다. 코드를 작성하기 앞서, 메일 서버가 필요하지만, 직접 구축하지 않고 네이버에서 제공해주는 SMTP 기능을 활성화하여 사용할 생각이다. 이 글에서는 네이버 SMTP 기능을 활성화 하는 방법에 대해서 설명하지 않았다. 활성화하는 방법이 궁금하다면 링크를 참고하면 된다.


의존성 설치

$ npm install --save nodemailer

node에서 손쉽게 메일을 보내기 위해서는 추가적으로 의존성을 추가해야 한다. 이번에는 nodemailer을 사용하기 했다.


설정 추가(/config/config.json)

...
"mail": {
  "from": "발송 이메일",
  "config": {
    "service": "Naver",
    "host": "smtp.naver.com",
    "port": 587,
    "auth": {
      "user": "네이버 아이디",
      "pass": "네이버 비밀번호"
    }
  }
}
...

/utils/mail.utils.js

const nodemailer = require("nodemailer");

/**
 * 설정
 */
const config = require("../utils/config.util").getConfg();

/**
 * SMTP 접속 정보
 */
const SMTPConnection = config.mail.config;

/**
 * Logging
 */
const { logger } = require("../config/winston");

module.exports = {
  /**
   * 메일 발송
   * @param {*} data 
   */
  send: (data) => {
    try {
      let transporter = nodemailer.createTransport(SMTPConnection);
      transporter.sendMail({
        from: config.mail.from,
        to: data.to,
        subject: data.subject,
        html: data.html
      });
      logger.info("Send mail.");
    } catch (err) {
      logger.error(error);
    }
  }
}

실제로 메일이 발송되는 부분이다. send 함수에 data라는 인자를 받는데 다음과 같은 데이터를 가지고 온다.

  • data
    • to: 받는 사람 메일
    • subject: 메일 제목
    • html: 메일 내용

임시 비밀번호 발급 API

const express = require("express");
const { validationResult } = require("express-validator");
const router = express.Router();
const Op = require("sequelize").Op;

/**
 * Logging
 */
const { logger } = require("../config/winston");

/**
 * 설정
 */
const config = require("../utils/config.util").getConfg();

/**
 * Database Models
 */
const { models } = require('../sequelize');

/**
 * User 비밀번호 찾기 유효성 객체
 */
const userFindPasswordValid = require("../validates/user.findPassword.valid");

/**
 * 메일 객체
 */
const mail = require("../utils/mail.util");

/**
 * AES-256 암호화 객체
 */
const aes256 = require("../utils/security/aes256.util");

/**
 * SHA-256 암호화 객체
 */
const sha256 = require("sha256");

/**
 * 사용자 비밀번호 찾기
 */
router.post("/find/password/", userFindPasswordValid, (req, res) => {
  logger.info("POST /api/user/find/password/");
  const valid = validationResult(req);

  if (!valid.isEmpty()) {
    res.status(400).json({
      success: false,
      param: valid.errors[0].param,
      message: valid.errors[0].msg
    });
  } else {
    const id = req.query.id;
    const email = req.query.email;

    models.user.findOne({
      where: {
        id: id,
        email: aes256.encrypt(email)
      }
    }).then((user) => {
      if (!user) {
        res.status(400).json({
          success: false,
          message: "회원정보가 존재하지 않습니다\n다시 시도해주세요"
        });
      } else {
        const password = generatePassword();

        models.user.update({
          password: sha256(password)
        }, {
          where: {
            id: user.id
          }
        }).then(() => {
          sendFindPasswordMail({
            id: user.id,
            password: password,
            name: user.name,
            to: aes256.decrypt(user.email)
          });
          res.status(200).json({
            success: true,
            message: "조회가 완료 되었습니다",
            id: user.id
          });
        }).catch((err) => {
          logger.error(err.message);
          res.status(500).json({
            success: false,
            message: "에러가 발생하였습니다\n다시 시도해주세요"
          });
        });
      }
    }).catch((err) => {
      logger.error(err.message);
      res.status(500).json({
        success: false,
        message: "에러가 발생하였습니다\n다시 시도해주세요"
      });
    });
  }
});

/**
 * 패스워드 랜덤 생성
 * @return string 패스워드
 */
const generatePassword = () => {
  const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*";
  const stringLength = 8;

  var randomString = "";
  for (let i = 0; i < stringLength; i++) {
    let randomNum = Math.floor(Math.random() * chars.length);
    randomString += chars.substring(randomNum, randomNum + 1);
  }

  return randomString;
}

/**
 * 임시 비밀번호 메일 발송
 * @param {*} data 
 */
const sendFindPasswordMail = (data) => {
  logger.info("Send mail for passsword find.");
  mail.send({
    to: data.to,
    subject: "[TODO] 임시 비밀번호 발송",
    html: `<h2>${data.name} 님</h2>` +
          `임시 비밀번호: ${data.password}<br />` +
          `<a href="http://localhost:9000/">로그인하러 가기</a>`
  });
}

module.exports = router;

위 코드는 USER API 중, 임시 비밀번호를 발급하는 API만 가지고 온 것이다.

저희가 구현하던 사이트 CNU-Steam의 보완 목표 중 하나였던 메일 발송을 통해 비밀번호 찾기를 구현하기 위해

npm의 모듈 중 하나인 nodemailer를 이용하여 forgot password를 구현하였습니다.

우선 npm install nodemailer crypto로 2가지의 모듈을 깔아줍니다.

nodemailer는 메일 발송을 할 수 있게 해주는 모듈이고 , crypto는 임시페이지를 만들 때 사용할 토큰을 만들어주는 모듈입니다.

Node js 아이디 찾기 - Node js aidi chajgi

authorization.js 에 lostpw 라우터를 수정했습니다.

기존에는 그냥 e_mail의 정보가 일치하기만 한다면 바로 패스워드를 업데이트 할 수 있도록 하였다면

이제는 e_mail에 저장된 메일로 이메일을 보낸 후 메일의 주소에 따라 비밀번호를 바꿀 수 있는 페이지로 연결되도록 하였습니다.

우선 async.waterfall을 사용합니다. function 들을 순차적으로 사용하면서, 앞에서 사용한 값을 넘겨줄 수 잇는 async.waterfall은 이해하기는 까다롭지만, 사용에 익숙해지면 편리합니다.

첫번째 function에서는 crypto를 이용하여 randombytes를 생성합니다. 조건은 20자, 'hex'인 string으로 생성합니다.

이후 done으로 이 값을 다음 function으로 넘기고, 페이지에서 입력받은 e-mail과 일치하는 유저를 찾습니다.

해당 유저의 값을 초기화 한 후, 다시 done을 이용하여 다음 function으로 값을 넘깁니다.

Node js 아이디 찾기 - Node js aidi chajgi

이후 gmail을 이용하여 메일을 발송할 준비를 합니다.

nodemailer와 gmail을 사용하는 방법은

https://nubiz.tistory.com/703

위의 블로그를 ㅊ

위의 블로그를 참고하였습니다.

gmail을 이용하기 위해서는 인증을 받아와야하기 때문에, 위의 블로그를 참고하시면 됩니다.

Node js 아이디 찾기 - Node js aidi chajgi

mailOptions를 이용하여 메일에 적을 내용을 설정합니다.

from에는 보낸 사람에 관한 정보를 적을 수 있고

to는 보낼 사람에 대해 적을 수 있습니다.

각각 알맞은 정보를 입력한 후

subject 로 보낼 메일의 제목을 결정할 수 있고,

text로 메일의 내용을 정할 수 있습니다.

이때 중요한 것은 위에서 생성한 token을 메일에 보내주어야 합니다.

expire를 3600으로 정해놨기 때문에 한시간이 지나면 만료되는 임시 주소값이 생성된 것입니다.

사용자들은 위의 주소를 통해 비밀번호를 재설정하는 페이지로 접근할 수 있습니다.

Node js 아이디 찾기 - Node js aidi chajgi

비밀번호를 재설정하는 페이지의 라우터입니다.

마찬가지로 async.waterfall을 사용합니다.

우선 바꿔야할 user의 정보를 가져옵니다.

시퀼라이즈의 findOne 함수를 통해 db에서 바꿀 유저의 정보를 가져옵니다.

find.then을 사용하지 않으면 해당 값을 찾지 못하는 경우가 발생하는데, 정확한 이유를 모르겠습니다.

페이지에서 받아온 새로운 password를 통해 user의 정보를 업데이트 한 후,

bcrypto를 이용하여 다시 암호화 한 후 db에 저장합니다.

시퀼라이즈의 update구문을 이용하여 이를 해결할 수 있습니다.

Node js 아이디 찾기 - Node js aidi chajgi

router.get 구문은 별다른 내용이 없습니다.

페이지를 받아오고, 해당 pug 파일에 렌더링 해주고, user 값을 가져올 뿐입니다.

단 /reset:=token 의 경우, token에 맞는 값을 받았을때만 다이렉션 하도록 설정해주어야하므로

라우터의 get 부분이나 post 부분을 명확하게 설정해주어야합니다.

인터넷의 여러 사이트를 참고하여 만들었고, 아직 nodejs의 동기/비동기를 정확하게 이해하지 못하였기 때문에

미숙한 부분이 많고(위의 구문도 에러가 생길 수 잇는 부분을 많이 무시하고 작성하였습니다.)

혹시라도 지적하실만한 사항이 있다면 댓글로 부탁드리겠습니다.

감사합니다.