본문 바로가기

카테고리 없음

React)나이스아이디 with react(typescript), node.js (nice id)

최근 프로젝트에 나이스아이디를 적용시킬 기회가 생겼다.

많은 기업들이 나이스아이디를 적용시켰고 코드 또한 인터넷에 널렸다고 생각했지만 막상 찾아보니 나이스아이디에서 제공하는 node.js등의 모듈의 언어들 밖에 없었다.

자바스크립트 코드는 모두 node.js의 서버사이드 렌더링 밖에 없었고 node.js는 만진지 오래되었기에 타 블로그의 내용을 참조했다.

(해피쿠 님의 블로그를 참조했습니다.)

우선 node.js의 코드부터 살펴보자

 

기본셋팅

우선 기본적인 node.js의 express를 설정한다

$ mkdir node-server
$ npm init
# webserver
$ npm i express

# CORS 정책 설정(cross-domain)
$ npm i cors

# cmd 실행
$ npm i child-process

# session 생성
$ npm i express-session

# cookie 파싱
$ npm i cookie-parser

#.env
$ npm i dotenv

 

다음 config.js를 생성해 다음 코드를 적는다.

//config.js

require("dotenv").config();

module.exports = {
  secret: "nicetest!!",
  pgInfo: {
    nice: {
      siteCode: process.env.SITECODE,
      sitePw: process.env.SITEPW,
    },
  },
};

해피쿠님의 블로그에 의하면 "config.sj를 생성해 session생성에 secret으로 사용할 값"이라 나와있다.

그 다음으로 nice에서 발급받은 sitecode와 site password를 적어둔다.

 

app.js

app.js는 나이스로부터 받은 모듈의 코드를 대부분 참조했으나 나 역시 해피쿠님의 블로그를 상당부분 참조했다.

//app.js
const express      = require('express'),
      cors         = require('cors'),
      session      = require('express-session'),
      cookieParser = require('cookie-parser');

const config = require('./config');

const app = express();

//CORS 허용
app.use(cors({ 
  origin(origin, callback) {
    callback(null, true)
  },
  credentials : true 
}));

//application/json 형태의 데이터 req.body에 저장
app.use(express.json());
//www-form-urlencode 형태의 데이터 req.body에 저장
app.use(express.urlencoded({ extended: false }));

//쿠키 파싱
app.use(cookieParser(config.secret));
//session 생성
app.use(session({
  key : 'nicetest',
  resave : false,
  saveUninitialized : false,
  secret : config.secret
}));

//Error Handler
app.use((err, req, res, next) => {
  console.log('++++++++++++++Error!!!!!+++++++++++++', err.message);
  console.log(err.stack);
  res.status(err.status || 500);
  res.json({ code : err.code, msg : err.message, status : err.status });
});

module.exports = app;

 

다음으로 index.js를 생성한다.

//index.js
const app = require('./app');

app.listen(7777, () => {
  console.log("####################");
  console.log("### Server Start ###");
  console.log("####################");
});

다음으로 서버를 실행시켜보자.

$ nodemon index.js
#or
$ node index.js

서버가 잘 실행된다면 다음이 중요하다.

 

암호화 키 생성

nice id의 팝업창을 호출하기 위해선 암호화 키가 필요하다.

암호화키는 sitecode와 몇몇가지를 조합해 생성할 수 있는데 생성 방법은 nice id에서 제공받은 .exe파일을 실행시키면 된다.

* 나 같은 경우엔 실행파일의 이름 때문에 30분을 해맸었다.. 잘 확인하도록하자.

다음 코드를 app.js에 추가한다.

app.use('/nice', niceRouter);

 

nice-router.js

다음 루트폴더에 routes/nice-router.js 파일을 생성해 다음을 적는다

const express = require("express"),
  router = express.Router();

const path = require("path"),
  qs = require("querystring"),
  exec = require("child_process").exec;

const { pgInfo } = require("../config");

//암호화된 데이터(키) 발급
router.get("/encrypt/data", (req, res, next) => {
  //   callback 때 redirect 할 URL
  req.session.redirectUrl = req.query.redirectUrl;

  const niceInfo = pgInfo.nice,
    d = new Date(),
    moduleName = "CPClient.exe",
    //nice 모듈 위치
    modulePath = path.join(__dirname, "..", moduleName),
    //site code
    sSiteCode = niceInfo.siteCode,
    //site password
    sSitePW = niceInfo.sitePw,
    //CP 요청 번호 -> 저희 WAS에서 사용할 수 있는 유일한 값(세션 등에서 이용가능)
    sCPRequest = `${sSiteCode}_${d.getTime()}`;

  let sPlaincData = "", //암호화할 요청 데이터
    sEncData = "", //암호화된 요청 데이터 -> response로 보낼 값
    sAuthType = "M", //없으면 기본 선택화면, X: 공인인증서, M: 핸드폰, C: 카드
    sPopGubun = "N", //Y : 취소버튼 있음 / N : 취소버튼 없음
    sRtnMSG = "", //에러메시지
    sReturnUrl = req.query.returnUrl,
    sErrorUrl = req.query.returnUrl;

  //요청 규격에 맞게 정리
  sPlaincData =
    `7:REQ_SEQ${sCPRequest.length}:${sCPRequest}` +
    `8:SITECODE${sSiteCode.length}:${sSiteCode}` +
    `9:AUTH_TYPE${sAuthType.length}:${sAuthType}` +
    `7:RTN_URL${sReturnUrl.length}:${sReturnUrl}` +
    `7:ERR_URL${sErrorUrl.length}:${sErrorUrl}` +
    `11:POPUP_GUBUN${sPopGubun.length}:${sPopGubun}`;

  //nice 모듈을 사용하여 암호화하는 명령
  const cmd = `${modulePath} ENC ${sSiteCode} ${sSitePW} ${sPlaincData}`;
  // console.log()
  //cmd 실행(데이터 암호화)
  const child = exec(cmd, { encoding: "euc-kr" });
  child.stdout.on("data", (data) => {
    sEncData += data;
  });

  child.on("close", () => {
    if (sEncData == "-1") {
      sRtnMSG = "암/복호화 시스템 오류입니다.";
    } else if (sEncData == "-2") {
      sRtnMSG = "암호화 처리 오류입니다.";
    } else if (sEncData == "-3") {
      sRtnMSG = "암호화 데이터 오류 입니다.";
    } else if (sEncData == "-9") {
      sRtnMSG =
        "입력값 오류 : 암호화 처리시, 필요한 파라미터 값을 확인해 주시기 바랍니다.";
    } else {
      sRtnMSG = "";
    }

    //오류 발생 시 -> error handler
    if (sRtnMSG) {
      next(new Error(sRtnMSG));
    } else {
      //암호화된 데이터 return
      res.send(sEncData);
      //   console.log(sEncData);
      //   console.log(sRtnMSG);
    }
  });
});

module.exports = router;

//callback & redirect
router.all("/decrypt/data", (req, res, next) => {
  //암호화된 인증 결과 데이터(PC 인 경우 method GET, mobile인 경우 method POST)
  const sEncData =
      req.method === "GET" ? req.query.EncodeData : req.body.EncodeData,
    //위에서 redirect URL(클라이언트에서 전달한 값)
    redirectUrl = req.session.redirectUrl;

  const niceInfo = pgInfo.nice,
    moduleName = "CPClient.exe",
    modulePath = path.join(__dirname, "..", moduleName),
    sSiteCode = niceInfo.siteCode,
    sSitePW = niceInfo.sitePw;

  let cmd = "";
  //redirect할 때 전달할 데이터
  let response = {};

  if (/^0-9a-zA-Z+\/=/.test(sEncData) == true) {
    response.msg = "입력값 오류";
  }

  if (sEncData != "") {
    //nice 모듈을 사용하여 복호화하는 명령
    cmd = `${modulePath} DEC ${sSiteCode} ${sSitePW} ${sEncData}`;
  }

  //복호화된 데이터
  let sDecData = "";

  //cmd 실행(데이터 복호화)
  const child = exec(cmd, { encoding: "euc-kr" });
  child.stdout.on("data", (data) => {
    sDecData += data;
  });

  function parseNiceData(plaindata, key) {
    let arrData = plaindata.split(":");
    let value = "";
    for (let i in arrData) {
      let item = arrData[i];
      if (item.indexOf(key) == 0) {
        let valLen = parseInt(item.replace(key, ""));
        arrData[i++];
        value = arrData[i].substr(0, valLen);
        break;
      }
    }
    return value;
  }

  child.on("close", () => {
    //처리 결과 확인
    if (sDecData == "-1") {
      response.msg = "암/복호화 시스템 오류";
    } else if (sDecData == "-4") {
      response.msg = "복호화 처리 오류";
    } else if (sDecData == "-5") {
      response.msg = "HASH값 불일치 - 복호화 데이터는 리턴됨";
    } else if (sDecData == "-6") {
      response.msg = "복호화 데이터 오류";
    } else if (sDecData == "-9") {
      response.msg = "입력값 오류";
    } else if (sDecData == "-12") {
      response.msg = "사이트 비밀번호 오류";
    } else {
      /* 인증 성공 -> 인증 결과 파싱 */

      //CP 요청 번호 -> 저희 WAS에서 사용할 수 있는 유일한 값(세션 등에서 이용가능)
      (response.requestnumber = decodeURIComponent(
        parseNiceData(sDecData, "REQ_SEQ")
      )),
        //nice가 생성해준 고유 번호
        (response.responsenumber = decodeURIComponent(
          parseNiceData(sDecData, "RES_SEQ")
        )),
        //인증수단
        (response.authtype = decodeURIComponent(
          parseNiceData(sDecData, "AUTH_TYPE")
        )),
        //이름
        (response.name = decodeURIComponent(
          parseNiceData(sDecData, "UTF8_NAME")
        )),
        //생년월일(YYYYMMDD)
        (response.birthdate = decodeURIComponent(
          parseNiceData(sDecData, "BIRTHDATE")
        )),
        //성별
        (response.gender = decodeURIComponent(
          parseNiceData(sDecData, "GENDER")
        )),
        //내.외국인정보
        (response.nationalInfo = decodeURIComponent(
          parseNiceData(sDecData, "NATIONALINFO")
        )),
        //중복가입값(64byte)
        (response.dupInfo = decodeURIComponent(parseNiceData(sDecData, "DI"))),
        //연계정보 확인값(88byte)
        (response.connInfo = decodeURIComponent(parseNiceData(sDecData, "CI"))),
        //휴대폰번호(계약된 경우)
        (response.mobile = decodeURIComponent(
          parseNiceData(sDecData, "MOBILE_NO")
        )),
        //통신사(계약된 경우)
        (response.mobileCo = decodeURIComponent(
          parseNiceData(sDecData, "MOBILE_CO")
        ));

      console.log(response);

      //redirect (데이터는 query param으로 전달)
      // res.redirect(`${redirectUrl}?${qs.stringify(response)}`);
      res.json(response);
    }

    next(new Error(response.msg));
  });
});

 

이제 서버를 실행하고 /nice/encrypt/data를 확인하면 암호화 된 키가 잘 나오는 것을 확인할 수 있다.

 

이제 클라이언트로 넘어가보자

 

React

우선 전체적인 동작 방식은 다음과 같다.

사용자가 "인증하기" 같은 버튼을 누르면 암호화 키를 발급, 암호화키를 form데이터로 전송하면 nice 팝업창이 열린다.

사용자가 인증을 완료하면 설정해둔 returnUrl로 결과값을 받을 수 있다.

우선 Verification이라는 컴포넌트를 생성한다.

그리고 다음과 같은 form을 넣자.

      <form
        name="form_chk"
        method="post"
        id="form"
        action="https://nice.checkplus.co.kr/CheckPlusSafeModel/checkplus.cb
  "
      >
        <input type="hidden" name="m" value="checkplusService" />
        <input type="hidden" name="EncodeData" value="" id="enc_data" />
      </form>
      <button onClick={onClickCertify}> 인증 </button>

이제 onClilck이벤트 함수를 작성해보자

const onClickCertify = async () => {
    
    const left = 100;
    const top = 100;
    
    // // 데이터 설정
    let enc = null;
    await axios
     
      .get("http://localhost:7777/nice/encrypt/data", {
        params: {
          redirectUrl: "http://127.0.0.1:7777/nice/decrypt/data",
        
          returnUrl: "http://localhost:3000/result",
         
        },
      })
      .then((res) => {
        enc = res.data;
      });

    if (!enc) return;
    const form = document.getElementById("form") as HTMLFormElement;

    if (form !== null) {
      window.open(
        "state",
        "popupChk",
        "width=500, height=550, top=100, left=100, fullscreen=no, menubar=no, status=no, toolbar=no, titlebar=yes, location=yes, scrollbar=no"
      );
      // 데이터 설정
      const encDataInput = document.getElementById(
        "enc_data"
      ) as HTMLInputElement;
     

      // 데이터 설정 예시
      const encDataValue = enc; // 실제 데이터 값으로 대체해야 합니다.
      
      encDataInput.value = encDataValue;
     
      // form 데이터 전송
      form.target = "popupChk";
      form.submit();
    }
  };

팝업창은 window.open으로 열고 nice/encrypt/data를 통해 암호화 키를 받고 이를 form데이터에 담아 전송하는 형태이다.

사실 클라이언트 사이드에서는 별거 없다.

다만 window.open()으로 팝업창은 연만큼 window.close()로 다시 닫아주어야한다는 점이 참 불편하다.

차라리 서버사이드였다면 좀 더 편했을 것 같다.