api 없이 무료로 휴대폰 본인인증을 구현하는 방법

    일반적으로 휴대폰 인증은 SMS메시지를 사용자에게 보내는 방식으로 구현하죠, 사용자가 문자를 발송하게 하고, 서비스를 제공하는 측에서는 문자 발송 비용 대신 회선(전화번호) 유지 비용만 내는 것도 가능하겠네요. 실제로 금융권에서는 문자를 보내서 휴대폰 인증을 하기도 하니까요.

    무료로 휴대폰 인증을 구현하는 원리

    여기에서 조금 더 머리를 쓰면 휴대폰 인증을 무료로 사용할 수 있습니다. 사용자가 이메일 주소로 MMS를 발송하게 하면 되는 것인데요. 통신사에서는 MMS를 이메일 주소로 보내면 자동으로 이메일로 전송해주는 기능을 가지고 있습니다.

    MMS로 발송한 이메일

    MMS로 이메일을 발송하면 다음과 같이 오게 됩니다. 전화번호가 이메일의 @ 앞쪽 username으로 위치하게 되고, 도메인은 통신사마다 달라지는데요. SKT의 경우 vmms.nate.com도메인으로, KT의 경우 ktfmms.magicn.com, LG U+ 의 경우 lguplus.com도메인으로 메일이 옵니다.

    그럼 이를 활용해 사용자에게 특정 이메일 주소로 MMS를 보내게끔 인증 프로세스를 만들게 되면, 무료로 휴대폰 인증이 가능해지겠네요.

    실제로 구현하기

    이번 글에서는 expressnode-imap, sqlite3 모듈을 사용해 인증 프로세스를 만들어 보겠습니다. express가 아닌 다른 서버에서도 코드는 다르지만 같은 과정으로 따라하시면 됩니다.

    초기설정

    npm을 사용해 express, node-imap, sqlite3, express-session, session-file-store 모듈을 설치합니다.

    npm i express node-imap sqlite3 express-session session-file-store

    index.js 파일에서 불러오고 초기화를 진행합니다.

    // index.js
    const express = require("express");
    const session = require("express-session");
    const fileStore = require("session-file-store")(session);
    const sqlite3 = require("sqlite3").verbose();
    const Imap = require("imap");
    
    const app = express();
    app.use(
        session({
            secret: "sessionSECRET", // 서비스에 맞게 변경
            resave: false,
            saveUninitialized: false,
            cookie: {
                maxAge: 1000 * 60 * 60 * 24, // 1 day 서비스에 맞게 변경
                httpOnly: true,
            },
            store: new fileStore(),
        })
    );
    const imap = new Imap({
        user: "gmailUsername", // 서비스에 맞게 변경
        password: "gmailPassword", // 서비스에 맞게 변경
        host: "imap.gmail.com", // 서비스에 맞게 변경
        port: 993, // 서비스에 맞게 변경
        tls: true,
        tlsOptions: { servername: "imap.gmail.com" }, // 서비스에 맞게 변경
    });
    imap.once("ready", function () {
        console.log("imap ready");
    });
    imap.connect();
    const db = new sqlite3.Database("DB파일의 path", (err) => {
        if (err) {
            console.error(err.message);
        }
        console.log("Connected to the main database.");
    });

    위 코드에서 세션 시크릿, imap서버 설정, db연결 설정을 각자 사정에 맞게 변경해 적용해주세요.

    DB에 인증용 랜덤 문자열 올리기

    먼저, /signup 으로 접속할 경우 랜덤 문자열을 만든 후 이를 DB에 시간과 함께 저장합니다. 만약 개인을 특정하기 위한 세션이 없는 경우, 세션에 랜덤 문자열을 저장합니다. 이미 개인을 특정하기 위한 세션이 있는 경우 그 세션의 데이터를 DB에 랜덤 문자열과 함께 저장합니다.

    router.get("/signup", (req, res) => {
        ranstr = randomString(500);
        db.serialize(() => {
            db.run("INSERT INTO dbTABLE이름 (randomString, date) VALUES (?, ?, ?, ?)", [ranstr, Number(new Date())]);
        });
        req.session.randomString = ranstr
        res.render("signup.ejs", { ranstr })

    서버에서 프론트로 랜덤 문자열을 전달할 때에는 위 코드와 같이 EJS를 사용하거나 아니면 이 글처럼 replace를 통해 해결할 수도 있습니다.

    프론트엔드 설계

    다음과 같은 a 태그를 html에 삽입하고 누르게 되면 [email protected] 으로 “인증 메시지” 라는 내용의 메시지가 자동으로 작성됩니다. 사용자는 보내기 버튼만 누르면 되는 것이죠.

    <a href="sms:[email protected]?body=인증 메시지">인증문자 보내기</a>

    인증에 사용하기 위해서는 “인증 메시지” 대신 /signup 에 접속할 때 생성한 랜덤 문자열을 사용해야 합니다. “인증 메시지” 를 사용하게 되면 여러명이 동시에 인증을 진행할 때 누가 누구인지 특정할 수 없으니까요.
    필자의 경우 ejs를 통해 데이터를 전달받았으므로 다음과 같이 코드를 작성하였습니다.

    <a href="sms:[email protected]?body=<%= ranstr %>">인증문자 보내기</a>

    서버측에서 검증하기

    사용자가 메시지까지 전송했다면 서버측에서 확인이 되어야겠죠.

    const userVerifyOnEmail = (verify_body) => {
        return new Promise((resolve, reject) => {
    
            imap.openBox("INBOX", true, (err, box) => {
                if (err) return reject(err);
                imap.search(["ALL", ["BODY", verify_body]], function (err, results) {
                    if (err) return reject(err);
                    if (results.length == 0) return reject("err");
                    let f = imap.fetch(results, { bodies: "HEADER" });
                    f.on("message", function (msg, seqno) {
                        let prefix = "(#" + seqno + ") ";
                        msg.on("body", function (stream, info) {
                            let buffer = "",
                                count = 0;
                            stream.on("data", function (chunk) {
                                count += chunk.length;
                                buffer += chunk.toString("utf8");
                            });
                            stream.once("end", function () {
                                if (info.which !== "TEXT") {
                                    let fromAddress = Imap.parseHeader(buffer, false).from[0];
                                    let fromPhone = fromAddress.substring(fromAddress.indexOf("<0") + 1, fromAddress.indexOf("@"));
    
                                    let fromDomain = fromAddress
                                        .substring(fromAddress.indexOf("@") + 1, fromAddress.indexOf("m>")+1)
                                    console.log(fromPhone + " ||| " + fromDomain);
                                    if (fromDomain != "vmms.nate.com" && fromDomain != "ktfmms.magicn.com" && fromDomain != "lguplus.com") {
                                        reject("err");
                                    }
                                    resolve(fromPhone);
                                }
                            });
                        });
                    });
                    f.once("error", function (err) {
                        reject("err");
                    });
                    f.once("end", function () {});
                });
            });
            // });
        });
    };

    이렇게 이 userVerifyOnEmail 함수에 찾고자 하는 이메일의 Body를 넣으면 해당하는 이메일을 찾아, 발신자의 전화번호와 통신사를 알아낼 수 있습니다. 현재 이 함수는 한국 통신3사만 사용 가능하도록 되어 있지만, if문 조건을 변형해 해외 통신사에서도 사용 가능하게 변경할 수 있습니다.

    클라이언트로부터 /signup/verify로 요청이 날아오면 세션에 저장된 값으로 DB에서 10분 이내에 저장된 랜덤 문자열을 찾습니다. 랜덤 문자열을 찾았다면 이를 Body로 해서 받은 이메일이 있는지 확인하고 발신자를 특정하면 휴대폰 인증 절차가 끝납니다.

    router.post("/signup/verify", noChaceMiddlewrae, (req, res) => {
        if (!req.session.randomString) return res.status(401).json({ result: "unauthorized" });
        db.serialize(() => {
            db.all(
                "SELECT * FROM verification WHERE randomString = ? and date > ? ORDER BY date desc LIMIT 1;",
                [req.session.randomString, Number(new Date()) - 10 * 60 * 1000],
                (err, rows) => {
                    if (err) {
                        console.error(err);
                        return res.status(401).json({ result: "unauthorized" });
                    } else if (rows.length == 0) {
                        console.log("no row");
                        return res.status(401).json({ result: "unauthorized" });
                    }
                    userVerifyOnEmail(rows[0].randomString)
                        .then((phoneNumber) => {
                            // 발신자 번호 특정 완료
                            console.log(phoneNumber)
                        });
                        .catch((error) => {
                            console.log(error);
                            return res.send({ result: "wait" });
                        });
                }
            );
        });
    });
    

    추가: QR코드

    sms: 로 링크되어 있는 a태그는 컴퓨터에서 작동하지 않기 때문에 모바일 브라우저에서 작동시켜야 합니다. 모바일 앱으로만 출시한다면 아무 문제가 없겠지만, 컴퓨터로 접속한다면 QR코드를 표시하여 휴대폰으로 인증을 계속하라는 메시지를 띄워야 합니다.
    안드로이드의 경우 QR코드에 다음과 같이 데이터를 입력하면 된다.

    sms:[email protected]?body=인증 메시지

    iOS의 경우 QR코드에 다음과 같이 데이터를 입력하면 된다.

    sms:[email protected];body=인증 메시지

    왜 iOS의 경우 ? 대신 ;을 써야하는지 모르겠지만, 특정 iOS버전부터 바뀐 것 같습니다. 웹 브라우저에서 작동시킬 경우 프론트엔드 설계에서 작성한 코드가 안드로이드와 iOS모두에서 잘 작동합니다.
    그래서 필자는 다음과 같이 웹을 통해 인증하도록 설계하였다. QR코드로 바로 sms로 연결되면 좋지만, 그 전에 안드로이드와 iOS를 선택하는 것 보다는 웹을 거치는 것이 더 낫다고 판단했기 때문입니다. 이를 구현하는 것에 대해서는 이 글에서 따로 서술하지 않겠습니다.

    무료 휴대폰 인증 - 컴퓨터
    무료 휴대폰 인증 - 휴대폰에서 이어서 인증

    마치며

    SMS를 보내서 인증하는 것 보다 2배 넘게 로직이 복잡한것 같습니다. 다만 인증문자 한통마다 10원 가까이 나가는 돈과, 문자를 보내기 위해 업체에 돈을 충전해야 한다는 불편이 없기 때문에 이렇게 구현해 사용할 가치가 충분하다 생각합니다.
    스타트업이나, 개인 프로젝트에 적용시켜 사용하기 적합해 보입니다.


    게시됨

    카테고리

    작성자

    태그:

    Obtuse의 테크 블로그 더 알아보기

    이 블로그에 새 글이 나올 때 마다 이메일로 알림을 받아보는 건 어때요?


    ※구독 버튼을 클릭하면 obtuse.kr의 개인정보 처리방침의 광고성 정보 수신에 동의하는 것으로 간주합니다.

    댓글

    “api 없이 무료로 휴대폰 본인인증을 구현하는 방법” 에 하나의 답글

    1. 채성우 아바타

      좋은 글 감사합니다!

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

    이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.